def test_append_and_remove(self) -> None: dimension = 2 identity = Unitary.identity(dimension) sequence = UnitarySequence(dimension) assert sequence.get_length() == 0 assert sequence.product().close_to(identity) sequence.append_first( UnitarySequenceEntry(UnitaryDefinitions.sigmax(), [0])) assert sequence.get_length() == 1 assert sequence.product().close_to(UnitaryDefinitions.sigmax()) sequence.append_last( UnitarySequenceEntry(UnitaryDefinitions.sigmay(), [0])) assert sequence.get_length() == 2 assert sequence.product().close_to(UnitaryDefinitions.sigmaz()) sequence.append_first( UnitarySequenceEntry(UnitaryDefinitions.sigmaz(), [0])) assert sequence.get_length() == 3 assert sequence.product().close_to(identity) sequence.remove_last() assert sequence.get_length() == 2 assert sequence.product().close_to(UnitaryDefinitions.sigmay()) sequence.remove_first() assert sequence.get_length() == 1 assert sequence.product().close_to(UnitaryDefinitions.sigmax()) sequence.remove_first() assert sequence.get_length() == 0 assert sequence.product().close_to(identity)
def test_sequence_output_formats(self) -> None: dimension = 2 rx_entry = UnitarySequenceEntry(UnitaryDefinitions.rx(np.pi / 3), [0]) ry_entry = UnitarySequenceEntry(UnitaryDefinitions.ry(np.pi / 3), [0]) sequence = UnitarySequence(dimension, [rx_entry, ry_entry]) assert sequence.get_qasm() assert sequence.get_jaqal() assert sequence.get_display_output()
def test_identity(self) -> None: dimension = 2 entry = UnitarySequenceEntry(Unitary.identity(dimension), [0]) assert entry.get_dimension() == dimension assert np.array_equal(entry.get_apply_to(), [0]) for system_dimension in [2, 4, 8, 16]: full_unitary = entry.get_full_unitary(system_dimension) assert full_unitary.close_to(Unitary.identity(system_dimension))
def test_cnot_swapped(self) -> None: dimension = 4 entry = UnitarySequenceEntry(UnitaryDefinitions.cnot(), [1, 0]) system_dimension = 4 full_unitary = entry.get_full_unitary(system_dimension) assert full_unitary.close_to( Unitary( dimension, np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]])))
def test_inverse(self) -> None: dimension = 2 rx_entry = UnitarySequenceEntry(UnitaryDefinitions.rx(np.pi / 3), [0]) ry_entry = UnitarySequenceEntry(UnitaryDefinitions.ry(np.pi / 3), [0]) sequence = UnitarySequence(dimension, [rx_entry, ry_entry]) product = sequence.product() inverse_sequence = sequence.inverse() inverse_product = inverse_sequence.product() assert inverse_product.close_to(product.inverse()) inverse_sequence.sequence_product = None inverse_product = inverse_sequence.product() assert inverse_product.close_to(product.inverse())
def test_combine(self) -> None: dimension = 2 t = Unitary(dimension, np.array([[1, 0], [0, np.exp(1j * np.pi / 4)]])) t_entry = UnitarySequenceEntry(t, [0]) sequence_1 = UnitarySequence(dimension, np.repeat(t_entry, 3)) sequence_2 = UnitarySequence( dimension, [UnitarySequenceEntry(UnitaryDefinitions.sigmay(), [0])]) combined_sequence = UnitarySequence.combine(sequence_1, sequence_2) assert (combined_sequence.get_length() == sequence_1.get_length() + sequence_2.get_length()) assert combined_sequence.product().close_to( sequence_1.product().left_multiply(sequence_2.product()))
def get_ideal_sequence(self, time: float, num_steps: int) -> UnitarySequence: ''' Returns a sequence of identical unitaries, where each unitary is the time-evolution operator under this Hamiltonian for time / num_steps, and the length of the sequence is num_steps. :param time: The total time to evolve the system. :type time: float :param num_steps: The number of steps in which to break up the time evolution of the system. :type num_steps: int :return: A sequence of num_steps identical unitaries implementing the time evolution of the system. :rtype: UnitarySequence ''' sequence_entries = [] time_per_step = time / num_steps u = self.get_time_evolution_operator(time_per_step) apply_to = list(range(self.get_qubit_count())) for _ in range(num_steps): entry = UnitarySequenceEntry(u, apply_to) sequence_entries.append(entry) return UnitarySequence(self.get_dimension(), sequence_entries)
def get_qdrift_sequence(self, time: float, num_repetitions: int) -> UnitarySequence: ''' Returns a sequence of unitaries using a QDRIFT decomposition of the time-evolution under this Hamiltonian, as per Campbell, PRL 123, 070503 (2019). The sequence approximately implements the ideal time evolution of the system. :param time: The total time to evolve the system. :type time: float :param num_repetitions: The number of QDRIFT repetitions to use. :type num_repetitions: int :return: [description] :return: A sequence of unitaries implementing the QDRIFT decomposition of the time evolution of the system. :rtype: UnitarySequence ''' sequence_entries = [] coefficients = [term.get_coefficient() for term in self.terms] sum_coefficients = np.sum(coefficients) prob_coefficients = coefficients / sum_coefficients time_per_step = sum_coefficients * time / num_repetitions apply_to = list(range(self.get_qubit_count())) for _ in range(num_repetitions): term = np.random.choice(self.terms, p=prob_coefficients) display_suffix = str(self.terms.index(term)) + ' λ/N' u = UnitaryDefinitions.time_evolution(term.get_normalized_matrix(), time_per_step, display_suffix) entry = UnitarySequenceEntry(u, apply_to) sequence_entries.append(entry) return UnitarySequence(self.get_dimension(), sequence_entries)
def compile_rav_sequence( self, time: float, max_t_step: float, threshold: float, allow_simultaneous_terms: bool = False ) -> CompilerResult: ''' Returns a randomized analog verification (RAV) sequence as per Shaffer et al., arXiv:2003.04500 (2020). The sequence of unitaries is built from terms of this Hamiltonian by first generating a random sequence and then using STOQ to compile the inverse such that the full sequence approximately implements the identity operation. :param time: The total time to evolve the system in the initial randomly-generated sequence. :type time: float :param max_t_step: The maximum time to use for a single Hamiltonian term at each step of the sequence. :type max_t_step: float :param threshold: The overlap with the target unitary at which to stop the STOQ compilation, defaults to None. A value of 1.0 implies an exact compilation. :type threshold: float :param allow_simultaneous_terms: Whether to allow multiple Hamiltonian terms to be executed simultaneously in the resulting sequence, defaults to False. :type allow_simultaneous_terms: bool, optional :return: A sequence of unitaries implementing RAV. :rtype: CompilerResult ''' # Generate a random sequence, mostly forward in time forward_probability = 0.8 unitary_primitives = self._get_unitary_primitives( max_t_step, allow_simultaneous_terms) apply_to = list(range(self.get_qubit_count())) random_sequence = UnitarySequence(self.get_dimension()) total_time = 0.0 while total_time < time: t_step = (max_t_step * np.random.random_sample()) * ( 1 if np.random.random_sample() < forward_probability else -1) u_step = np.random.choice( unitary_primitives).get_unitary().as_unitary([t_step]) random_sequence.append_last(UnitarySequenceEntry(u_step, apply_to)) total_time += np.abs(t_step) # Calculate the product of this sequence and invert it target_unitary = random_sequence.product().inverse() # Call _compile_stoq_sequence_from_unitary to compile a new sequence # implementing the inverse result = self._compile_stoq_sequence_for_target_unitary( target_unitary, max_t_step, threshold, allow_simultaneous_terms) # Return the CompilerResult with the combined sequence result.compiled_sequence = UnitarySequence.combine( random_sequence, result.compiled_sequence) return result
def create_random_sequence_entry( dimension: int, unitary_primitives: List[UnitaryPrimitive], probabilities: Optional[List[float]] = None ) -> UnitarySequenceEntry: ''' Creates and returns a randomly-generated unitary sequence entry. :param dimension: The dimension of the state space. For an n-qubit system, dimension should be set to 2**n. :type dimension: int :param unitary_primitives: The unitary primitives from which to choose when randomly generating a sequence entry. :type unitary_primitives: List[UnitaryPrimitive] :param probabilities: The probability for STOQ to choose each of the primitives specified in unitary_primitives when proposing new gates at each step of the compilation process, defaults to None. If not specified, each unitary primitive is chosen with uniform probability. :type probabilities: Optional[List[float]], optional :return: A sequence entry specifying a randomly-chosen unitary with randomly-chosen parameters and applied to a randomly-chosen set of qubits. :rtype: UnitarySequenceEntry ''' # choose from unitary_primitives where the allowed_apply_to list # is not empty unitary_primitives = [ u for u in unitary_primitives if (u.get_allowed_apply_to() is None or len(u.get_allowed_apply_to()) > 0) ] # randomly choose one of the unitary primitives and assign random # parameter values new_unitary_primitive = np.random.choice(unitary_primitives, p=probabilities) new_unitary = new_unitary_primitive.get_unitary() if isinstance(new_unitary, ParameterizedUnitary): random_parameter_values = [ p.random_value() for p in new_unitary.get_parameters() ] new_unitary = new_unitary.as_unitary(random_parameter_values) assert isinstance(new_unitary, Unitary) # randomly choose the qubit(s) to which to apply the unitary num_system_qubits = int(np.log2(dimension)) if new_unitary_primitive.get_allowed_apply_to() is not None: allowed_apply_to = new_unitary_primitive.get_allowed_apply_to() apply_to = allowed_apply_to[np.random.choice( len(allowed_apply_to))] else: num_unitary_qubits = int(np.log2(new_unitary.get_dimension())) apply_to = np.random.choice(list(range(num_system_qubits)), num_unitary_qubits, replace=False) return UnitarySequenceEntry(new_unitary, apply_to)
def test_identity_roots_incorrect(self) -> None: dimension = 2 t = Unitary(dimension, np.array([[1, 0], [0, np.exp(1j * np.pi / 4)]])) t_entry = UnitarySequenceEntry(t, [0]) sequence = UnitarySequence(dimension, np.repeat(t_entry, 7)) assert sequence.get_dimension() == dimension assert sequence.get_length() == 7 assert not sequence.product().close_to(np.identity(dimension))
def test_undo(self) -> None: dimension = 2 identity = Unitary.identity(dimension) sequence = UnitarySequence(dimension) assert sequence.get_length() == 0 with pytest.raises(Exception): sequence.undo() sequence.append_first( UnitarySequenceEntry(UnitaryDefinitions.sigmax(), [0])) assert sequence.get_length() == 1 assert sequence.product().close_to(UnitaryDefinitions.sigmax()) sequence.undo() assert sequence.get_length() == 0 assert sequence.product().close_to(identity) with pytest.raises(Exception): sequence.undo() sequence.append_first( UnitarySequenceEntry(UnitaryDefinitions.sigmay(), [0])) sequence.append_first( UnitarySequenceEntry(UnitaryDefinitions.sigmay(), [0])) assert sequence.get_length() == 2 assert sequence.product().close_to(identity) sequence.remove_last() assert sequence.get_length() == 1 assert sequence.product().close_to(UnitaryDefinitions.sigmay()) sequence.undo() assert sequence.get_length() == 2 assert sequence.product().close_to(identity) with pytest.raises(Exception): sequence.undo()
def test_compile_two_qubits(self) -> None: num_qubits = 2 system_dimension = qubit_dimension**num_qubits unitary_primitives = [ UnitaryPrimitive(UnitaryDefinitions.rx(np.pi / 2)), UnitaryPrimitive(UnitaryDefinitions.cnot()) ] compiler = Compiler(system_dimension, unitary_primitives) # Ensure determinism by setting the random seed np.random.seed(123456) target_unitary = UnitarySequence(system_dimension, [ UnitarySequenceEntry(UnitaryDefinitions.cnot(), [0, 1]), UnitarySequenceEntry(UnitaryDefinitions.rx(np.pi), [0]) ]).product() result = compiler.compile(target_unitary) assert result.compiled_sequence.product().close_to(target_unitary) assert result.compiled_sequence.get_qasm() assert result.compiled_sequence.get_display_output() assert isinstance(result.cost_by_step, list) assert result.total_elapsed_time >= 0.0
def get_trotter_sequence( self, time: float, num_trotter_steps: int, randomize: bool = False ) -> UnitarySequence: ''' Returns a sequence of unitaries using a Suzuki-Trotter decomposition of the time-evolution under this Hamiltonian. The sequence approximately implements the ideal time evolution of the system. :param time: The total time to evolve the system. :type time: float :param num_trotter_steps: The number of Trotter steps to use. :type num_trotter_steps: int :param randomize: Whether to randomize the order of Hamiltonian terms in each step of the Suzuki-Trotter decomposition, defaults to False. :type randomize: bool, optional :return: A sequence of unitaries implementing the Suzuki-Trotter decomposition of the time evolution of the system. :rtype: UnitarySequence ''' sequence_entries = [] time_per_step = time / num_trotter_steps apply_to = list(range(self.get_qubit_count())) term_indices = list(range(len(self.terms))) for _ in range(num_trotter_steps): if randomize: random.shuffle(term_indices) for term_index in term_indices: term = self.terms[term_index] u = UnitaryDefinitions.time_evolution( term.get_matrix(), time_per_step, term_index) entry = UnitarySequenceEntry(u, apply_to) sequence_entries.append(entry) return UnitarySequence(self.get_dimension(), sequence_entries)
def test_cnot(self) -> None: entry = UnitarySequenceEntry(UnitaryDefinitions.cnot(), [0, 1]) with pytest.raises(Exception): system_dimension = 2 full_unitary = entry.get_full_unitary(system_dimension) system_dimension = 4 full_unitary = entry.get_full_unitary(system_dimension) assert full_unitary.close_to(UnitaryDefinitions.cnot()) system_dimension = 8 full_unitary = entry.get_full_unitary(system_dimension) assert full_unitary.close_to(UnitaryDefinitions.cnot().tensor( Unitary.identity(2))) system_dimension = 16 full_unitary = entry.get_full_unitary(system_dimension) assert full_unitary.close_to(UnitaryDefinitions.cnot().tensor( Unitary.identity(4))) assert full_unitary.left_multiply(full_unitary).close_to( Unitary.identity(system_dimension))