Gate-level circuit simulation
Run a quantum circuit
Let’s start off by defining a simple circuit which we use to demonstrate a few examples of circuit evolution. We take a circuit from OpenQASM 2
from qutip_qip.circuit import QubitCircuit
from qutip_qip.operations import (
Gate, controlled_gate, hadamard_transform)
def controlled_hadamard():
# Controlled Hadamard
return controlled_gate(
hadamard_transform(1), controls=0, targets=1, control_value=1)
qc = QubitCircuit(N=3, num_cbits=3)
qc.user_gates = {"cH": controlled_hadamard}
qc.add_gate("QASMU", targets=[0], arg_value=[1.91063, 0, 0])
qc.add_gate("cH", targets=[0,1])
qc.add_gate("TOFFOLI", targets=[2], controls=[0, 1])
qc.add_gate("X", targets=[0])
qc.add_gate("X", targets=[1])
qc.add_gate("CNOT", targets=[1], controls=0)
It corresponds to the following circuit:
We will add the measurement gates later. This circuit prepares the W-state \(\newcommand{\ket}[1]{\left|{#1}\right\rangle} (\ket{001} + \ket{010} + \ket{100})/\sqrt{3}\).
The simplest way to carry out state evolution through a quantum circuit is
providing an input state to the QubitCircuit.run()
method.
from qutip import tensor, basis
zero_state = tensor(basis(2, 0), basis(2, 0), basis(2, 0))
result = qc.run(state=zero_state)
wstate = result
print(wstate)
Output:
Quantum object: dims = [[2, 2, 2], [1, 1, 1]], shape = (8, 1), type = ket
Qobj data =
[[0. ]
[0.57735]
[0.57735]
[0. ]
[0.57735]
[0. ]
[0. ]
[0. ]]
As expected, the state returned is indeed the required W-state.
As soon as we introduce measurements into the circuit, it can lead to multiple outcomes with associated probabilities. We can also carry out circuit evolution in a manner such that it returns all the possible state outputs along with their corresponding probabilities. Suppose, in the previous circuit, we measure each of the three qubits at the end.
qc.add_measurement("M0", targets=[0], classical_store=0)
qc.add_measurement("M1", targets=[1], classical_store=1)
qc.add_measurement("M2", targets=[2], classical_store=2)
To get all the possible output states along with the respective probability of observing the
outputs, we can use the QubitCircuit.run_statistics()
function:
result = qc.run_statistics(state=tensor(basis(2, 0), basis(2, 0), basis(2, 0)))
states = result.get_final_states()
probabilities = result.get_probabilities()
for state, probability in zip(states, probabilities):
print("State:\n{}\nwith probability {:.5f}".format(state, probability))
Output:
State:
Quantum object: dims = [[2, 2, 2], [1, 1, 1]], shape = (8, 1), type = ket
Qobj data =
[[0.]
[1.]
[0.]
[0.]
[0.]
[0.]
[0.]
[0.]]
with probability 0.33333
State:
Quantum object: dims = [[2, 2, 2], [1, 1, 1]], shape = (8, 1), type = ket
Qobj data =
[[0.]
[0.]
[1.]
[0.]
[0.]
[0.]
[0.]
[0.]]
with probability 0.33333
State:
Quantum object: dims = [[2, 2, 2], [1, 1, 1]], shape = (8, 1), type = ket
Qobj data =
[[0.]
[0.]
[0.]
[0.]
[1.]
[0.]
[0.]
[0.]]
with probability 0.33333
The function returns a Result
object which contains
the output states.
The method get_results()
can be used to obtain the
possible states and probabilities.
Since the state created by the circuit is the W-state, we observe the states
\(\newcommand{\ket}[1]{\left|{#1}\right\rangle} \ket{001}\), \(\newcommand{\ket}[1]{\left|{#1}\right\rangle} \ket{010}\) and \(\newcommand{\ket}[1]{\left|{#1}\right\rangle} \ket{100}\) with equal probability.
Circuit simulator
The QubitCircuit.run()
and QubitCircuit.run_statistics()
functions
make use of the CircuitSimulator
which enables exact simulation with more
granular options. The simulator object takes a quantum circuit as an argument. It can optionally
be supplied with an initial state. There are two modes in which the exact simulator can function. The default mode is the
“state_vector_simulator” mode. In this mode, the state evolution proceeds maintaining the ket state throughout the computation.
For each measurement gate, one of the possible outcomes is chosen probabilistically
and computation proceeds. To demonstrate, we continue with our previous circuit:
from qutip_qip.circuit import CircuitSimulator
sim = CircuitSimulator(qc)
sim.initialize(zero_state)
This initializes the simulator object and carries out any pre-computation
required. There are two ways to carry out state evolution with the simulator.
The primary way is to use the CircuitSimulator.run()
and
CircuitSimulator.run_statistics()
functions just like before (only
now with the CircuitSimulator
class).
The CircuitSimulator
class also enables stepping through the circuit:
print(sim.step())
Output:
Quantum object: dims = [[2, 2, 2], [1, 1, 1]], shape = (8, 1), type = ket
Qobj data =
[[0.57735]
[0. ]
[0. ]
[0. ]
[0.8165 ]
[0. ]
[0. ]
[0. ]]
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 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
, CircuitSimulator
precomputes the product of the unitaries (in between the measurements):
sim = CircuitSimulator(qc, precompute_unitary=True)
print(sim.ops)
[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
By default, the state evolution is carried out in the “state_vector_simulator” mode (specified by the mode argument) as described before. In the “density_matrix_simulator” mode, the input state can be either a ket or a density matrix. If it is a ket, it is converted into a density matrix before the evolution is carried out. Unlike the “state_vector_simulator” mode, upon measurement, the state does not collapse to one of the post-measurement states. Rather, the new state is now the density matrix representing the ensemble of post-measurement states. In this sense, we measure the qubits and forget all the results.
To demonstrate this consider the original W-state preparation circuit which is followed just by measurement on the first qubit:
qc = QubitCircuit(N=3, num_cbits=3)
qc.user_gates = {"cH": controlled_hadamard}
qc.add_gate("QASMU", targets=[0], arg_value=[1.91063, 0, 0])
qc.add_gate("cH", targets=[0,1])
qc.add_gate("TOFFOLI", targets=[2], controls=[0, 1])
qc.add_gate("X", targets=[0])
qc.add_gate("X", targets=[1])
qc.add_gate("CNOT", targets=[1], controls=0)
qc.add_measurement("M0", targets=[0], classical_store=0)
qc.add_measurement("M0", targets=[1], classical_store=0)
qc.add_measurement("M0", targets=[2], classical_store=0)
sim = CircuitSimulator(qc, mode="density_matrix_simulator")
print(sim.run(zero_state).get_final_states()[0])
Quantum object: dims = [[2, 2, 2], [2, 2, 2]], shape = (8, 8), type = oper, isherm = True
Qobj data =
[[0. 0. 0. 0. 0. 0. 0. 0. ]
[0. 0.33333 0. 0. 0. 0. 0. 0. ]
[0. 0. 0.33333 0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0.33333 0. 0. 0. ]
[0. 0. 0. 0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 0. 0. 0. ]]
We are left with a mixed state.
Import and export quantum circuits
QuTiP supports importing and exporting quantum circuits in the OpenQASM 2.0 format, as well as exporting circuits to Quantum Intermediate Representation.
To import from and export to OpenQASM 2.0, you can use the read_qasm()
and save_qasm()
functions, respectively.
We demonstrate this functionality by loading a circuit for preparing the \(\left|W\right\rangle\)-state from an OpenQASM 2.0 file.
The following code is in OpenQASM format:
// Name of Experiment: W-state v1
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
creg c[3];
gate cH a,b {
h b;
sdg b;
cx a,b;
h b;
t b;
cx a,b;
t b;
h b;
s b;
x b;
s a;
}
u3(1.91063,0,0) q[0];
cH q[0],q[1];
ccx q[0],q[1],q[2];
x q[0];
x q[1];
cx q[0],q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];
measure q[2] -> c[2];
One can save it in a .qasm
file and import it using the following code:
from qutip_qip.qasm import read_qasm
qc = read_qasm("source/w-state.qasm")
QuTiP circuits can also be exported to QIR:
>>> from qutip_qip.circuit import QubitCircuit
>>> from qutip_qip.qir import circuit_to_qir
>>> circuit = QubitCircuit(3, num_cbits=2)
>>> msg, here, there = range(3)
>>> circuit.add_gate("RZ", targets=[msg], arg_value=0.123)
>>> circuit.add_gate("SNOT", targets=[here])
>>> circuit.add_gate("CNOT", targets=[there], controls=[here])
>>> circuit.add_gate("CNOT", targets=[here], controls=[msg])
>>> circuit.add_gate("SNOT", targets=[msg])
>>> circuit.add_measurement("Z", targets=[msg], classical_store=0)
>>> circuit.add_measurement("Z", targets=[here], classical_store=1)
>>> circuit.add_gate("X", targets=[there], classical_controls=[0])
>>> circuit.add_gate("Z", targets=[there], classical_controls=[1])
>>> print(circuit_to_qir(circuit, "text"))