Skip to content

Commit

Permalink
Remove the support of precompute_unitary
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
BoxiLi committed Dec 16, 2023
1 parent e554aa6 commit e419b91
Show file tree
Hide file tree
Showing 4 changed files with 22 additions and 216 deletions.
38 changes: 0 additions & 38 deletions doc/source/qip-simulator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=========================
Expand Down
16 changes: 1 addition & 15 deletions src/qutip_qip/circuit/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand All @@ -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,
)
Expand All @@ -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
-------
Expand All @@ -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"]):
Expand Down
136 changes: 7 additions & 129 deletions src/qutip_qip/circuit/circuitsimulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ class CircuitSimulator:
def __init__(
self,
qc,
U_list=None,
mode="state_vector_simulator",
precompute_unitary=False,
):
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -386,22 +307,13 @@ 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
set to the tuple of bits (sequentially) instead of being
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:
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
48 changes: 14 additions & 34 deletions tests/test_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}
Expand Down

0 comments on commit e419b91

Please sign in to comment.