Quick Start#
In this tutorial, we will walk you through the fundamental procedures for simulating a quantum circuit using MIMIQ. Throughout the tutorial, we will provide links to detailed documentation and examples that can provide a deeper understanding of each topic.
Contents#
Installation#
To install QLEO (by MIMIQ) use the following command:
pip install "qleo @ git+https://github.com/qperfect-io/mimiqcircuits-python.git"
Check the installation page for more details.
Simply start by importing the qleo Python module within your workspace like this:
from qleo import *
Example: Simulate a GHZ circuit#
Construct basic circuit#
A circuit is basically a sequence of quantum operations (gates, measurements, etc…) that act on a set of qubits and potentially store information in a classical or “z” register (see circuit page). The classical register is a boolean vector to store the results of measurements, and the z register is a complex vector to store the result of mathematical calculations like expectation values.
Our interface is similar to other software, but there are some important differences:
There are no hardcoded quantum registers. Qubits are simply indicated by an integer index starting at 0 (Python convention). The same for classical and z registers.
A
Circuitobject doesn’t have a fixed number of qubits. The number of qubits in a circuit is taken from looking at the qubits the gates are applied to. It is the maximum integer index used in a circuit. The same for the number of classical bits.
To construct a GHZ circuit, we start by defining an empty Circuit()
>>> circuit = Circuit()
The most important tool to build circuits is the push() function. It is used like this: circuit.push(quantum_operation, targets...). It accepts a circuit, a single quantum operation, and a series of targets, one for every qubit or bit the operation supports.
We apply a GateH on the first qubit as
>>> circuit.push(GateH(), 0)
1-qubit circuit with 1 instructions:
└── H @ q[0]
The text representation H @ q[0] informs us that there is an instruction which applies the Hadamard gate to the qubit with index 0. Note that qubits start by default in the state 0.
Multiple gates can be added at once through the same push() syntax using iterables, see circuit and unitary gates page for more information.
To prepare a 5-qubit GHZ state, we add 5 CX gates or control-X gates between the qubit 0 and all the qubits from 1 to 4.
>>> circuit.push(GateCX(), 0, range(1, 5))
4-qubit circuit with 4 instructions:
├── H @ q[0]
├── CX @ q[0], q[1]
├── CX @ q[0], q[2]
└── CX @ q[0], q[3]
Measure, add noise, and extract information#
We can extract information about the state of the system (without affecting the state) at any point in the circuit, see statistical operations page. For example, we can compute the expectation value of \(| 11 \rangle\langle 11 |\) of qubits 0 and 3, and store it in the first z register as:
>>> circuit.push(ExpectationValue(Projector11()), 0, 4, 0)
6-qubit circuit with 5 instructions:
├── H @ q[0]
├── CX @ q[0], q[1]
├── CX @ q[0], q[2]
├── CX @ q[0], q[3]
└── ⟨P₁₁(1)⟩ @ q[1,5], z[1]
We can measure the qubits and add other non-unitary operations at any point in the circuit, for example:
>>> circuit.push(Measure(), range(0, 5), range(0, 5))
6-qubit circuit with 9 instructions:
├── H @ q[0]
├── CX @ q[0], q[1]
├── CX @ q[0], q[2]
├── CX @ q[0], q[3]
├── ⟨P₁₁(1)⟩ @ q[1,5], z[1]
├── M @ q[1], c[1]
├── M @ q[2], c[2]
├── M @ q[3], c[3]
└── M @ q[4], c[4]
Here, we measure qubits 0 to 4 and store the result in classical register 0 to 4.
In general, the ordering of targets is always like circuit.push(op, quantum_targets..., classical_targets..., z_targets...).
Warning
Classical and z registers can be overwritten. If you do circuit.push(Measure(), 0, 0) followed by circuit.push(Measure(), 1, 0), the second measurement will overwrite the first one since it will be stored on the same classical register 0. To avoid this in a circuit with many measurements you can, for example, keep track of the index of the last used register.
To simulate imperfect quantum computers we can add noise to the circuit. Noise operations can be added just like any other operations using push. However, noise can also be added after the circuit has been built to all gates of a certain type using add_noise(). For example:
>>> circuit.add_noise(GateH(), AmplitudeDamping(0.01))
6-qubit circuit with 10 instructions:
├── H @ q[0]
├── AmplitudeDamping(0.01) @ q[0]
├── CX @ q[0], q[1]
├── CX @ q[0], q[2]
├── CX @ q[0], q[3]
├── ⟨P₁₁(1)⟩ @ q[1,5], z[1]
├── M @ q[1], c[1]
├── M @ q[2], c[2]
├── M @ q[3], c[3]
└── M @ q[4], c[4]
>>> circuit.add_noise(GateCX(), Depolarizing2(0.1), parallel=True)
6-qubit circuit with 13 instructions:
├── H @ q[0]
├── AmplitudeDamping(0.01) @ q[0]
├── CX @ q[0], q[1]
├── Depolarizing(0.1) @ q[0,1]
├── CX @ q[0], q[2]
├── Depolarizing(0.1) @ q[0,2]
├── CX @ q[0], q[3]
├── Depolarizing(0.1) @ q[0,3]
├── ⟨P₁₁(1)⟩ @ q[1,5], z[1]
├── M @ q[1], c[1]
├── M @ q[2], c[2]
├── M @ q[3], c[3]
└── M @ q[4], c[4]
>>> circuit.add_noise(Measure(), PauliX(0.05), before=True, parallel=True)
6-qubit circuit with 17 instructions:
├── H @ q[0]
├── AmplitudeDamping(0.01) @ q[0]
├── CX @ q[0], q[1]
├── Depolarizing(0.1) @ q[0,1]
├── CX @ q[0], q[2]
├── Depolarizing(0.1) @ q[0,2]
├── CX @ q[0], q[3]
├── Depolarizing(0.1) @ q[0,3]
├── ⟨P₁₁(1)⟩ @ q[1,5], z[1]
├── PauliX(0.05) @ q[1]
├── PauliX(0.05) @ q[1]
├── PauliX(0.05) @ q[1]
├── PauliX(0.05) @ q[1]
├── M @ q[1], c[1]
├── M @ q[2], c[2]
├── M @ q[3], c[3]
└── M @ q[4], c[4]
See symbolic operations and special operations pages for other supported operations.
The number of qubits, classical bits, and complex z-values of a circuit can be obtained from:
>>> circuit.num_qubits(), circuit.num_bits(), circuit.num_zvars()
(6, 5, 2)
A circuit behaves in many ways like a vector (of instructions, i.e. operations + targets). You can get the length as len(circuit), access elements as circuit[2], insert elements with insert(), append other circuits with append() etc. You can also visualize circuits with draw(). See circuit page for more information.
Execute circuit#
Executing a circuit on is quickly done by
>>> res = Qleo().execute(c)
This will execute a simulation of the given circuit with default parameters.
To make a histogram out of the retrieved samples, it suffices to execute
>>> res.histogram()
{frozenbitarray('01110'): 1000}
To plot the results use the following:
>>> from qleo.visualization import plothistogram
>>> plothistogram(res)
<Figure size 960x720 with 1 Axes>
OpenQASM#
OpenQASM files, defining quantum algorithms, can be executed in the same way native circuits can, simply use execute() and provide the path of the file to read.
See the import-export page for more details on how include files are handled.