Skip to content

Commit c600c8c

Browse files
authored
Merge pull request #225 from BoxiLi/simulator_refactor.py
Refactor the gate level `CircuitSimulator` - Avoid expanding all propagators and then applying them one by one. Only expand when applying the unitary. This saves memory and also make it easier for further improvement, i.e., exploring other ways to perform the matrix multiplication (e.g. `einsum`). - Switch some public members to private. Those members that track the execution of the circuit evaluation should not be public. - Small changes to improve the clarity.
2 parents cded58f + 474884d commit c600c8c

File tree

4 files changed

+195
-351
lines changed

4 files changed

+195
-351
lines changed

doc/source/qip-simulator.rst

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ The :class:`.CircuitSimulator` class also enables stepping through the circuit:
169169

170170
.. testcode::
171171

172-
print(sim.step())
172+
sim.step()
173+
print(sim.state)
173174

174175
**Output**:
175176

@@ -191,44 +192,6 @@ This only executes one gate in the circuit and
191192
allows for a better understanding of how the state evolution takes place.
192193
The method steps through both the gates and the measurements.
193194

194-
Precomputing the unitary
195-
========================
196-
197-
By default, the :class:`.CircuitSimulator` class is initialized such that
198-
the circuit evolution is conducted by applying each unitary to the state interactively.
199-
However, by setting the argument ``precompute_unitary=True``, :class:`.CircuitSimulator`
200-
precomputes the product of the unitaries (in between the measurements):
201-
202-
.. testcode::
203-
204-
sim = CircuitSimulator(qc, precompute_unitary=True)
205-
206-
print(sim.ops)
207-
208-
.. testoutput::
209-
:options: +NORMALIZE_WHITESPACE
210-
211-
[Quantum object: dims = [[2, 2, 2], [2, 2, 2]], shape = (8, 8), type = oper, isherm = False
212-
Qobj data =
213-
[[ 0. 0.57735 0. -0.57735 0. 0.40825 0. -0.40825]
214-
[ 0.57735 0. -0.57735 0. 0.40825 0. -0.40825 0. ]
215-
[ 0.57735 0. 0.57735 0. 0.40825 0. 0.40825 0. ]
216-
[ 0. 0.57735 0. 0.57735 0. 0.40825 0. 0.40825]
217-
[ 0.57735 0. 0. 0. -0.8165 0. 0. 0. ]
218-
[ 0. 0.57735 0. 0. 0. -0.8165 0. 0. ]
219-
[ 0. 0. 0.57735 0. 0. 0. -0.8165 0. ]
220-
[ 0. 0. 0. 0.57735 0. 0. 0. -0.8165 ]],
221-
Measurement(M0, target=[0], classical_store=0),
222-
Measurement(M1, target=[1], classical_store=1),
223-
Measurement(M2, target=[2], classical_store=2)]
224-
225-
226-
Here, ``sim.ops`` stores all the circuit operations that are going to be applied during
227-
state evolution. As observed above, all the unitaries of the circuit are compressed into
228-
a single unitary product with the precompute optimization enabled.
229-
This is more efficient if one runs the same circuit one multiple initial states.
230-
However, as the number of qubits increases, this will consume more and more memory
231-
and become unfeasible.
232195

233196
Density Matrix Simulation
234197
=========================

src/qutip_qip/circuit/circuit.py

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@
1818
Measurement,
1919
expand_operator,
2020
GATE_CLASS_MAP,
21-
gate_sequence_product,
2221
)
2322
from .circuitsimulator import (
2423
CircuitSimulator,
2524
CircuitResult,
2625
)
27-
from qutip import basis, Qobj
26+
from qutip import Qobj, qeye
2827

2928

3029
try:
@@ -481,10 +480,6 @@ def run(
481480
post-selection. If specified, the measurement results are
482481
set to the tuple of bits (sequentially) instead of being
483482
chosen at random.
484-
precompute_unitary: Boolean, optional
485-
Specify if computation is done by pre-computing and aggregating
486-
gate unitaries. Possibly a faster method in the case of
487-
large number of repeat runs with different state inputs.
488483
489484
Returns
490485
-------
@@ -499,7 +494,6 @@ def run(
499494
raise TypeError("State is not a ket or a density matrix.")
500495
sim = CircuitSimulator(
501496
self,
502-
U_list,
503497
mode,
504498
precompute_unitary,
505499
)
@@ -520,10 +514,6 @@ def run_statistics(
520514
initialization of the classical bits.
521515
U_list: list of Qobj, optional
522516
list of predefined unitaries corresponding to circuit.
523-
precompute_unitary: Boolean, optional
524-
Specify if computation is done by pre-computing and aggregating
525-
gate unitaries. Possibly a faster method in the case of
526-
large number of repeat runs with different state inputs.
527517
528518
Returns
529519
-------
@@ -537,12 +527,7 @@ def run_statistics(
537527
mode = "density_matrix_simulator"
538528
else:
539529
raise TypeError("State is not a ket or a density matrix.")
540-
sim = CircuitSimulator(
541-
self,
542-
U_list,
543-
mode,
544-
precompute_unitary,
545-
)
530+
sim = CircuitSimulator(self, mode, precompute_unitary)
546531
return sim.run_statistics(state, cbits)
547532

548533
def resolve_gates(self, basis=["CNOT", "RX", "RY", "RZ"]):
@@ -892,48 +877,46 @@ def propagators(self, expand=True, ignore_measurement=False):
892877
"Cannot compute the propagator of a measurement operator."
893878
"Please set ignore_measurement=True."
894879
)
895-
896880
for gate in gates:
897881
if gate.name == "GLOBALPHASE":
898882
qobj = gate.get_qobj(self.N)
899-
U_list.append(qobj)
900-
continue
901-
902-
if gate.name in self.user_gates:
903-
if gate.controls is not None:
904-
raise ValueError(
905-
"A user defined gate {} takes only "
906-
"`targets` variable.".format(gate.name)
907-
)
908-
func_or_oper = self.user_gates[gate.name]
909-
if inspect.isfunction(func_or_oper):
910-
func = func_or_oper
911-
para_num = len(inspect.getfullargspec(func)[0])
912-
if para_num == 0:
913-
qobj = func()
914-
elif para_num == 1:
915-
qobj = func(gate.arg_value)
916-
else:
917-
raise ValueError(
918-
"gate function takes at most one parameters."
919-
)
920-
elif isinstance(func_or_oper, Qobj):
921-
qobj = func_or_oper
922-
else:
923-
raise ValueError("gate is neither function nor operator")
883+
else:
884+
qobj = self._get_gate_unitary(gate)
924885
if expand:
925886
all_targets = gate.get_all_qubits()
926887
qobj = expand_operator(
927888
qobj, dims=self.dims, targets=all_targets
928889
)
929-
else:
930-
if expand:
931-
qobj = gate.get_qobj(self.N, self.dims)
932-
else:
933-
qobj = gate.get_compact_qobj()
934890
U_list.append(qobj)
935891
return U_list
936892

893+
def _get_gate_unitary(self, gate):
894+
if gate.name in self.user_gates:
895+
if gate.controls is not None:
896+
raise ValueError(
897+
"A user defined gate {} takes only "
898+
"`targets` variable.".format(gate.name)
899+
)
900+
func_or_oper = self.user_gates[gate.name]
901+
if inspect.isfunction(func_or_oper):
902+
func = func_or_oper
903+
para_num = len(inspect.getfullargspec(func)[0])
904+
if para_num == 0:
905+
qobj = func()
906+
elif para_num == 1:
907+
qobj = func(gate.arg_value)
908+
else:
909+
raise ValueError(
910+
"gate function takes at most one parameters."
911+
)
912+
elif isinstance(func_or_oper, Qobj):
913+
qobj = func_or_oper
914+
else:
915+
raise ValueError("gate is neither function nor operator")
916+
else:
917+
qobj = gate.get_compact_qobj()
918+
return qobj
919+
937920
def compute_unitary(self):
938921
"""Evaluates the matrix of all the gates in a quantum circuit.
939922
@@ -942,8 +925,9 @@ def compute_unitary(self):
942925
circuit_unitary : :class:`qutip.Qobj`
943926
Product of all gate arrays in the quantum circuit.
944927
"""
945-
gate_list = self.propagators()
946-
circuit_unitary = gate_sequence_product(gate_list)
928+
sim = CircuitSimulator(self)
929+
result = sim.run(qeye(self.dims))
930+
circuit_unitary = result.get_final_states()[0]
947931
return circuit_unitary
948932

949933
def latex_code(self):

0 commit comments

Comments
 (0)