From e419b912b9c0170dcab006790ab6ff66207a7cc9 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Sat, 16 Dec 2023 22:56:23 +0100 Subject: [PATCH] Remove the support of precompute_unitary This option computes the full unitary of all the gates, and caches it. It is beneficial if the circuit has a small number of qubits but a very large depth. This is a rare use case. At the same time, this feature makes the migration to the new efficient circuit evaluation very hard. If the unitary for the circuit is desired, one can just call QubitCircuit.compute_unitary to get the circuit and then apply it to different states. --- doc/source/qip-simulator.rst | 38 ------ src/qutip_qip/circuit/circuit.py | 16 +-- src/qutip_qip/circuit/circuitsimulator.py | 136 ++-------------------- tests/test_circuit.py | 48 +++----- 4 files changed, 22 insertions(+), 216 deletions(-) diff --git a/doc/source/qip-simulator.rst b/doc/source/qip-simulator.rst index deaa3430..27994873 100644 --- a/doc/source/qip-simulator.rst +++ b/doc/source/qip-simulator.rst @@ -191,44 +191,6 @@ This only executes one gate in the circuit and allows for a better understanding of how the state evolution takes place. The method steps through both the gates and the measurements. -Precomputing the unitary -======================== - -By default, the :class:`.CircuitSimulator` class is initialized such that -the circuit evolution is conducted by applying each unitary to the state interactively. -However, by setting the argument ``precompute_unitary=True``, :class:`.CircuitSimulator` -precomputes the product of the unitaries (in between the measurements): - -.. testcode:: - - sim = CircuitSimulator(qc, precompute_unitary=True) - sim.initialize() - print(sim.ops) - -.. testoutput:: - :options: +NORMALIZE_WHITESPACE - - [Quantum object: dims = [[2, 2, 2], [2, 2, 2]], shape = (8, 8), type = oper, isherm = False - Qobj data = - [[ 0. 0.57735 0. -0.57735 0. 0.40825 0. -0.40825] - [ 0.57735 0. -0.57735 0. 0.40825 0. -0.40825 0. ] - [ 0.57735 0. 0.57735 0. 0.40825 0. 0.40825 0. ] - [ 0. 0.57735 0. 0.57735 0. 0.40825 0. 0.40825] - [ 0.57735 0. 0. 0. -0.8165 0. 0. 0. ] - [ 0. 0.57735 0. 0. 0. -0.8165 0. 0. ] - [ 0. 0. 0.57735 0. 0. 0. -0.8165 0. ] - [ 0. 0. 0. 0.57735 0. 0. 0. -0.8165 ]], - Measurement(M0, target=[0], classical_store=0), - Measurement(M1, target=[1], classical_store=1), - Measurement(M2, target=[2], classical_store=2)] - - -Here, ``sim.ops`` stores all the circuit operations that are going to be applied during -state evolution. As observed above, all the unitaries of the circuit are compressed into -a single unitary product with the precompute optimization enabled. -This is more efficient if one runs the same circuit one multiple initial states. -However, as the number of qubits increases, this will consume more and more memory -and become unfeasible. Density Matrix Simulation ========================= diff --git a/src/qutip_qip/circuit/circuit.py b/src/qutip_qip/circuit/circuit.py index 35fd9815..038c4301 100644 --- a/src/qutip_qip/circuit/circuit.py +++ b/src/qutip_qip/circuit/circuit.py @@ -479,10 +479,6 @@ def run( post-selection. If specified, the measurement results are set to the tuple of bits (sequentially) instead of being chosen at random. - precompute_unitary: Boolean, optional - Specify if computation is done by pre-computing and aggregating - gate unitaries. Possibly a faster method in the case of - large number of repeat runs with different state inputs. Returns ------- @@ -497,7 +493,6 @@ def run( raise TypeError("State is not a ket or a density matrix.") sim = CircuitSimulator( self, - U_list, mode, precompute_unitary, ) @@ -518,10 +513,6 @@ def run_statistics( initialization of the classical bits. U_list: list of Qobj, optional list of predefined unitaries corresponding to circuit. - precompute_unitary: Boolean, optional - Specify if computation is done by pre-computing and aggregating - gate unitaries. Possibly a faster method in the case of - large number of repeat runs with different state inputs. Returns ------- @@ -535,12 +526,7 @@ def run_statistics( mode = "density_matrix_simulator" else: raise TypeError("State is not a ket or a density matrix.") - sim = CircuitSimulator( - self, - U_list, - mode, - precompute_unitary, - ) + sim = CircuitSimulator(self, mode, precompute_unitary) return sim.run_statistics(state, cbits) def resolve_gates(self, basis=["CNOT", "RX", "RY", "RZ"]): diff --git a/src/qutip_qip/circuit/circuitsimulator.py b/src/qutip_qip/circuit/circuitsimulator.py index 5ecb0555..9a352dfc 100644 --- a/src/qutip_qip/circuit/circuitsimulator.py +++ b/src/qutip_qip/circuit/circuitsimulator.py @@ -258,7 +258,6 @@ class CircuitSimulator: def __init__( self, qc, - U_list=None, mode="state_vector_simulator", precompute_unitary=False, ): @@ -270,9 +269,6 @@ def __init__( qc : :class:`.QubitCircuit` Quantum Circuit to be simulated. - U_list: list of Qobj, optional - list of predefined unitaries corresponding to circuit. - mode: string, optional Specify if input state (and therefore computation) is in state-vector mode or in density matrix mode. @@ -285,95 +281,20 @@ def __init__( If in density_matrix_simulator mode and given a state vector input, the output must be assumed to be a density matrix. - - precompute_unitary: Boolean, optional - Specify if computation is done by pre-computing and aggregating - gate unitaries. Possibly a faster method in the case of - large number of repeat runs with different state inputs. """ self._qc = qc self.dims = qc.dims self.mode = mode - self.precompute_unitary = precompute_unitary - self.ops = None - - if U_list: - U_list = U_list + if precompute_unitary: + warnings.warn( + "Precomputing the full unitary is no longer supported. Switching to normal simulation mode." + ) @property def qc(self): return self._qc - def _process_ops_precompute(self, circuit): - """ - Process list of gates (including measurements), aggregate - gate unitaries (by multiplying) and store them in self.ops - for further computation. The gate multiplication is carried out - only for groups of matrices in between classically controlled gates - and measurement gates. - - Examples - -------- - - If we have a circuit that looks like: - - ----|X|-----|Y|----|M0|-----|X|---- - - then self.ops = [YX, M0, X] - """ - prev_index = 0 - U_list_index = 0 - ind_list = [] - - for gate in circuit.gates: - if isinstance(gate, Measurement): - continue - else: - ind_list.append(gate.get_all_qubits()) - - U_list = circuit.propagators(expand=False, ignore_measurement=True) - - ops = [] - for operation in circuit.gates: - if isinstance(operation, Measurement): - if U_list_index > prev_index: - ops.append( - self._compute_unitary( - U_list[prev_index:U_list_index], - ind_list[prev_index:U_list_index], - ) - ) - prev_index = U_list_index - ops.append(operation) - - elif isinstance(operation, Gate): - if operation.classical_controls: - if U_list_index > prev_index: - ops.append( - self._compute_unitary( - U_list[prev_index:U_list_index], - ind_list[prev_index:U_list_index], - ) - ) - prev_index = U_list_index - ops.append((operation, U_list[prev_index])) - prev_index += 1 - U_list_index += 1 - else: - U_list_index += 1 - - if U_list_index > prev_index: - ops.append( - self._compute_unitary( - U_list[prev_index:U_list_index], - ind_list[prev_index:U_list_index], - ) - ) - prev_index = U_list_index + 1 - U_list_index = prev_index - return ops - def initialize(self, state=None, cbits=None, measure_results=None): """ Reset Simulator state variables to start a new run. @@ -386,9 +307,6 @@ def initialize(self, state=None, cbits=None, measure_results=None): cbits: list of int, optional initial value of classical bits - U_list: list of Qobj, optional - list of predefined unitaries corresponding to circuit. - measure_results : tuple of ints, optional optional specification of each measurement result to enable post-selection. If specified, the measurement results are @@ -396,12 +314,6 @@ def initialize(self, state=None, cbits=None, measure_results=None): chosen at random. """ # Initializing the unitary operators. - if self.ops is None: - if self.precompute_unitary: - self.ops = self._process_ops_precompute(self.qc) - else: - self.ops = self.qc.gates - if cbits and len(cbits) == self.qc.num_cbits: self.cbits = cbits elif self.qc.num_cbits > 0: @@ -423,35 +335,6 @@ def initialize(self, state=None, cbits=None, measure_results=None): self._measure_results = measure_results self._measure_ind = 0 - def _compute_unitary(self, U_list, ind_list): - """ - Compute unitary corresponding to a product of unitaries in U_list - and expand it to size of circuit. - - Parameters - ---------- - U_list: list of Qobj - list of predefined unitaries. - - ind_list: list of list of int - list of qubit indices corresponding to each unitary in U_list - - Returns - ------- - U: Qobj - resultant unitary - """ - - U_overall, overall_inds = _gate_sequence_product( - U_list, ind_list=ind_list - ) - - if len(overall_inds) != self.qc.N: - U_overall = expand_operator( - U_overall, dims=self.qc.dims, targets=overall_inds - ) - return U_overall - def run(self, state, cbits=None, measure_results=None): """ Calculate the result of one instance of circuit run. @@ -475,7 +358,7 @@ def run(self, state, cbits=None, measure_results=None): output state and probability. """ self.initialize(state, cbits, measure_results) - for _ in range(len(self.ops)): + for _ in range(len(self.qc.gates)): result = self.step() if result is None: # TODO This only happens if there is predefined post-selection on the measurement results and the measurement results is exactly 0. This needs to be improved. @@ -547,7 +430,7 @@ def _check_classical_control_value(operation, cbits): matched[i] = cbits[cbit_index] == control_value return all(matched) - op = self.ops[self._op_index] + op = self.qc.gates[self._op_index] current_state = self._state if isinstance(op, Measurement): state = self._apply_measurement(op, current_state) @@ -560,14 +443,9 @@ def _check_classical_control_value(operation, cbits): else: apply_gate = True if apply_gate: - state = self._evolve_state( - operation, current_state - ) + state = self._evolve_state(operation, current_state) else: state = current_state - else: - # For pre-computed unitary only, where op is a Qobj. - state = self._evolve_state(op, current_state) self._op_index += 1 self._state = state diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 346c2878..2712f50c 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -61,24 +61,6 @@ def _measurement_circuit(): return qc -def _simulators_sv(qc): - - sim_sv_precompute = CircuitSimulator(qc, mode="state_vector_simulator", - precompute_unitary=True) - sim_sv = CircuitSimulator(qc, mode="state_vector_simulator") - - return [sim_sv_precompute, sim_sv] - - -def _simulators_dm(qc): - - sim_dm_precompute = CircuitSimulator(qc, mode="density_matrix_simulator", - precompute_unitary=True) - sim_dm = CircuitSimulator(qc, mode="density_matrix_simulator") - - return [sim_dm_precompute, sim_dm] - - class TestQubitCircuit: """ A test class for the QuTiP functions for Circuit resolution. @@ -586,17 +568,16 @@ def test_runstatistics_teleportation(self): def test_measurement_circuit(self): qc = _measurement_circuit() - simulators = _simulators_sv(qc) + simulator = CircuitSimulator(qc) labels = ["00", "01", "10", "11"] for label in labels: state = bell_state(label) - for i, simulator in enumerate(simulators): - simulator.run(state) - if label[0] == "0": - assert simulator.cbits[0] == simulator.cbits[1] - else: - assert simulator.cbits[0] != simulator.cbits[1] + simulator.run(state) + if label[0] == "0": + assert simulator.cbits[0] == simulator.cbits[1] + else: + assert simulator.cbits[0] != simulator.cbits[1] def test_circuit_with_selected_measurement_result(self): qc = QubitCircuit(N=1, num_cbits=1) @@ -654,17 +635,16 @@ def test_wstate(self): _, probs_initial = fourth.measurement_comp_basis(state) - simulators = _simulators_sv(qc) + simulator = CircuitSimulator(qc) - for simulator in simulators: - result = simulator.run_statistics(state) - final_states = result.get_final_states() - result_cbits = result.get_cbits() + result = simulator.run_statistics(state) + final_states = result.get_final_states() + result_cbits = result.get_cbits() - for i, final_state in enumerate(final_states): - _, probs_final = fourth.measurement_comp_basis(final_state) - np.testing.assert_allclose(probs_initial, probs_final) - assert sum(result_cbits[i]) == 1 + for i, final_state in enumerate(final_states): + _, probs_final = fourth.measurement_comp_basis(final_state) + np.testing.assert_allclose(probs_initial, probs_final) + assert sum(result_cbits[i]) == 1 _latex_template = r""" \documentclass[border=3pt]{standalone}