Circuits#
On this page you can find all the information needed to build a circuit using MIMIQ. Every useful function will be presented below, accompanied by an explanation of their purpose and examples of use.
Contents#
What is a circuit and what are instructions#
A quantum circuit, similar to a classical circuit, represents a sequence of quantum gates applied to qubits, which are the carriers of quantum information. Quantum circuits are essential for designing quantum algorithms. The complexity of a quantum circuit is typically measured by two key metrics: width and depth. Width refers to the number of qubits in the circuit, while depth indicates the maximum number of sequential gates applied to any single qubit.
Here is a representation of a simple GHZ circuit on 4 qubits:
from mimiqcircuits import *
>>> ghz = Circuit()
>>> ghz.push(GateH(), 0)
1-qubit circuit with 1 instructions:
└── H @ q[0]
>>> ghz.push(GateCX(), 0, range(1, 4))
4-qubit circuit with 4 instructions:
├── H @ q[0]
├── CX @ q[0], q[1]
├── CX @ q[0], q[2]
└── CX @ q[0], q[3]
>>>
>>> ghz.draw()
┌─┐
q[0]: ╶┤H├─●──●──●────────────────────────────────────────────────────────────╴
└─┘┌┴┐ │ │
q[1]: ╶───┤X├─┼──┼────────────────────────────────────────────────────────────╴
└─┘┌┴┐ │
q[2]: ╶──────┤X├─┼────────────────────────────────────────────────────────────╴
└─┘┌┴┐
q[3]: ╶─────────┤X├───────────────────────────────────────────────────────────╴
└─┘
In this representation, each qubit is depicted by a horizontal line labeled q[x], where x is the qubit’s index. The circuit is read from left to right, with each ‘block’ or symbol along a line representing an operation applied to that specific qubit.
Circuits & Instructions in MIMIQ#
MIMIQ implements a circuit using the Circuit
structure, in essence this structure is a wrapper for a vector of Instruction
to be applied on the qubits in the order of the vector. Since it is a vector a circuit can be manipulated as such, for example you can use for loops to iterate over the different instructions of the circuit, do vector comprehension or access all common vector attributes such as the length.
An Instruction
is composed of the quantum operation to be applied to the qubits, and the targets on which to apply it. There are many types of quantum operations, as discussed in the unitary gates, non-unitary operations and other pages of the manual. The targets can be qubits, as well as boolean or complex number vectors where classical information can be stored.
You will generally not need to interact with the Instruction
class directly (for exceptions, see special operations), but it is useful to understand how MIMIQ works.
See the following sections to learn how to add operations to your circuit.
Registers: quantum/classical/Z-register#
Before explaining how to build a circuit it is important to make a distinction between the different target registers your operations will be applied to.
The circuits in MIMIQ are composed of three registers that can be used by the instructions:
* The Quantum Register: Used to store the qubits state. Most of the operators in MIMIQ will interact with the quatum register. When printing or drawing a circuit (with the function draw()
) the quantum registers will be denoted as q[x] with x being the index of the qubit in the quantum register.
* The classical register: Used to store the bits state. Some gates will need to interact with classical bits (ex: Measure
) and the state of the classical bits is stored in the classical register, which is a vector of booleans. When printing or drawing a circuit the classical register will be denoted by the letter c.
* The Z-register: Used to store the result of some specific operations when the expected result is a complex number (ex: ExpectationValue
). The Z-register is basically a vector of complex numbers. When printing or drawing a circuit the Z-Register will be denoted by the letter z.
For the three registers operators can be applied on an arbitrary index starting from 0 (as does Python in general contrary to Julia). When possible you should always use the minimal index available as going for an arbitrary high index N
will imply that N
qubits will be simulated and might result in a loss of performance and will also make the circuit drawing more complex to understand.
Here is a circuit interacting with all registers:
from mimiqcircuits import *
>>> # create empty circuit
>>> circuit = Circuit()
>>> # add X to the first qubit of the Quantum register
>>> circuit.push(GateX(), 0)
1-qubit circuit with 1 instructions:
└── X @ q[0]
>>> # compute Expectation value of qubit 1 and store complex number on the first Z-Register
>>> ev = ExpectationValue(GateZ())
>>> circuit.push(ev, 0, 0)
1-qubit circuit with 2 instructions:
├── X @ q[0]
└── ⟨Z⟩ @ q[0], z[0]
>>> # Measure the qubit state and store bit into the first classical register
>>> circuit.push(Measure(), 0, 0)
1-qubit circuit with 3 instructions:
├── X @ q[0]
├── ⟨Z⟩ @ q[0], z[0]
└── M @ q[0], c[0]
>>> # draw the circuit
>>> circuit.draw()
┌─┐┌─────────┐┌──────┐
q[0]: ╶┤X├┤ ⟨Z⟩ ├┤ M ├─────────────────────────────────────────────────╴
└─┘└────╥────┘└───╥──┘
║ ║
║ ║
c: ═════════╬═════════╩═════════════════════════════════════════════════════
║ 0
z: ═════════╩═══════════════════════════════════════════════════════════════
0
As you can see in the code above the indexing of the different registers always starts by the quantum register. If your operator interacts with the three registers the index will have to be provided in the following order: #. Index of the qantum register. #. Index of the classical register. #. Index of the z-register.
Be careful when writing information to the z-register or to the classical register as the information can be easily overwritten if the same index is used multiple times. For example if you measure two different qubits and store both in the same classical bit the results of the sampling will only report the last measurement.
To retrieve information on the number of element of each register you can use the num_qubits()
, num_bits()
and numz_vars()
.
>>> circuit.num_qubits(), circuit.num_bits(), circuit.num_zvars()
(1, 1, 1)
In the following sections you will learn in details how to build a circuit in MIMIQ.
Creating a circuit#
The first step in executing quantum algorithm on MIMIQ always consists in implementing the corresonding quantum circuit, a sequence of quantum operations (quantum gates, measurements, resets, etc…) that acts on a set of qubits. In MIMIQ we always start by defining an empty circuit
>>> circuit = Circuit()
There is no need to give any arguments. Not even the number of qubits, classical or Z-registers is necessary as it will be directly inferred from the operations added to the circuit.
Adding Gates#
Once a circuit is instantiated operations can be added to it.
To see the list of gates available head to OPERATIONS
, GATES
, NOISECHANNELS
and GENERALIZED
or enter the following command in your Python session:
help(Gates)
To know more about the types of operations you can use in a circuit head to the unitary gates, non-unitary operations, noise, symbolic operations and special operations pages.
push#
To add gates to circuits in Python we will mainly be using the push()
method. The arguments needed by push()
can vary, but in general it expects the following:
#. The circuit to add the operation to.
#. The operator to be added.
#. As many targets as needed by the operator (qubits/bits/zvars).
For instance you can add the gate X by simply running the following command:
>>> circuit.push(GateX(), 0)
1-qubit circuit with 1 instructions:
└── X @ q[0]
The text representation `H @ q[0]`
informs us that there is an instruction which applies the Hadamard gate to the qubit of index 1.
Some gates require multiple target qubits such as the CX gate. Here is how to add such a gate to the circuit:
>>> circuit = Circuit()
>>> circuit.push(GateCX(), 0, 1)
2-qubit circuit with 1 instructions:
└── CX @ q[0], q[1]
This will add the gate GateCX
using the qubit number 1
as the control qubit and number 2
as the target qubit in the circuit
.
push specifics#
push()
is very versatile, it can be used to add multiple operators to multiple targets at once using iterators.
To add one type of gate to multiple qubits use:
>>> circuit = Circuit()
>>> circuit.push(GateX(), range(0, 11))
11-qubit circuit with 11 instructions:
├── X @ q[0]
├── X @ q[1]
├── X @ q[2]
├── X @ q[3]
├── X @ q[4]
├── X @ q[5]
├── X @ q[6]
├── X @ q[7]
├── X @ q[8]
├── X @ q[9]
└── X @ q[10]
This will add one X gate on each qubit from number 1 to 10.
This also works on 2-qubit gates:
>>> circuit = Circuit()
>>> circuit.push(GateID(), 0) # For documentation purpose, ignore this line
1-qubit circuit with 1 instructions:
└── ID @ q[0]
>>> # Adds 3 CX gates using respectively 1, 2 & 3 as the control qubits and 4 as the target qubit for all
>>> circuit.push(GateCX(), range(0, 3), 3)
4-qubit circuit with 4 instructions:
├── ID @ q[0]
├── CX @ q[0], q[3]
├── CX @ q[1], q[3]
└── CX @ q[2], q[3]
>>> # Adds 3 CX gates using respectively 2, 3 & 4 qubits as the target and 1 as the control qubit for all
>>> circuit.push(GateCX(), 0, range(1, 4))
4-qubit circuit with 7 instructions:
├── ID @ q[0]
├── CX @ q[0], q[3]
├── CX @ q[1], q[3]
├── CX @ q[2], q[3]
├── CX @ q[0], q[1]
├── CX @ q[0], q[2]
└── CX @ q[0], q[3]
>>> # adds 3 CX gates using respectively the couples (1, 4), (2, 5), (3, 6) as the control and target qubits
>>> circuit.push(GateCX(), range(0, 3), range(3, 6))
6-qubit circuit with 10 instructions:
├── ID @ q[0]
├── CX @ q[0], q[3]
├── CX @ q[1], q[3]
├── CX @ q[2], q[3]
├── CX @ q[0], q[1]
├── CX @ q[0], q[2]
├── CX @ q[0], q[3]
├── CX @ q[0], q[3]
├── CX @ q[1], q[4]
└── CX @ q[2], q[5]
>>> circuit.draw()
┌──┐
q[0]: ╶┤ID├─●────────●──●──●──●───────────────────────────────────────────────╴
└──┘ │ ┌┴┐ │ │ │
q[1]: ╶─────┼──●────┤X├─┼──┼──┼──●────────────────────────────────────────────╴
│ │ └─┘┌┴┐ │ │ │
q[2]: ╶─────┼──┼──●────┤X├─┼──┼──┼──●─────────────────────────────────────────╴
┌┴┐┌┴┐┌┴┐ └─┘┌┴┐┌┴┐ │ │
q[3]: ╶────┤X├┤X├┤X├──────┤X├┤X├─┼──┼─────────────────────────────────────────╴
└─┘└─┘└─┘ └─┘└─┘┌┴┐ │
q[4]: ╶─────────────────────────┤X├─┼─────────────────────────────────────────╴
└─┘┌┴┐
q[5]: ╶────────────────────────────┤X├────────────────────────────────────────╴
└─┘
Be careful when using vectors for both control and target, if one of the two vectors in longer than the other only the N first element of the vector will be accounted for with N = min(length.(vector1, vector2))
.
See the output of the code below to see the implication in practice:
>>> circuit = Circuit()
>>> circuit.push(GateID(), 0) # For documentation purpose, ignore this line
1-qubit circuit with 1 instructions:
└── ID @ q[0]
>>> # Adds only 3 CX gates
>>> circuit.push(GateCX(), range(0, 3), range(3, 18))
6-qubit circuit with 4 instructions:
├── ID @ q[0]
├── CX @ q[0], q[3]
├── CX @ q[1], q[4]
└── CX @ q[2], q[5]
>>> circuit.draw()
┌──┐
q[0]: ╶┤ID├─●─────────────────────────────────────────────────────────────────╴
└──┘ │
q[1]: ╶─────┼──●──────────────────────────────────────────────────────────────╴
│ │
q[2]: ╶─────┼──┼──●───────────────────────────────────────────────────────────╴
┌┴┐ │ │
q[3]: ╶────┤X├─┼──┼───────────────────────────────────────────────────────────╴
└─┘┌┴┐ │
q[4]: ╶───────┤X├─┼───────────────────────────────────────────────────────────╴
└─┘┌┴┐
q[5]: ╶──────────┤X├──────────────────────────────────────────────────────────╴
└─┘
You can also use tuples or vectors in the exact same fashion:
>>> circuit = Circuit()
>>> circuit.push(GateID(), 0) # For documentation purpose, ignore this line
1-qubit circuit with 1 instructions:
└── ID @ q[0]
>>> circuit.push(GateCX(), (0, 1), (2, 3))
4-qubit circuit with 3 instructions:
├── ID @ q[0]
├── CX @ q[0], q[2]
└── CX @ q[1], q[3]
>>> circuit.push(GateCX(), [0, 2], [1, 3])
4-qubit circuit with 5 instructions:
├── ID @ q[0]
├── CX @ q[0], q[2]
├── CX @ q[1], q[3]
├── CX @ q[0], q[1]
└── CX @ q[2], q[3]
>>> circuit.draw()
┌──┐
q[0]: ╶┤ID├─●─────●───────────────────────────────────────────────────────────╴
└──┘ │ ┌┴┐
q[1]: ╶─────┼──●─┤X├──────────────────────────────────────────────────────────╴
┌┴┐ │ └─┘
q[2]: ╶────┤X├─┼─────●────────────────────────────────────────────────────────╴
└─┘┌┴┐ ┌┴┐
q[3]: ╶───────┤X├───┤X├───────────────────────────────────────────────────────╴
└─┘ └─┘
Insert#
You can also insert an operation at a given index in the circuit using the insert()
function:
>>> circuit = Circuit()
>>> circuit.push(GateX(), 1)
2-qubit circuit with 1 instructions:
└── X @ q[1]
>>> circuit.push(GateZ(), 1)
2-qubit circuit with 2 instructions:
├── X @ q[1]
└── Z @ q[1]
>>> # Insert the gate at a specific index
>>> circuit.insert(2, GateY(), 1)
2-qubit circuit with 3 instructions:
├── X @ q[1]
├── Z @ q[1]
└── Y @ q[1]
>>> circuit
2-qubit circuit with 3 instructions:
├── X @ q[1]
├── Z @ q[1]
└── Y @ q[1]
This will insert GateY
applied on qubit `1`
at the second position in the circuit.
Append#
To append one circuit to another you can use the append()
function:
>>> # Build a first circuit
>>> circuit1 = Circuit()
>>> circuit1.push(GateX(), range(1, 4))
4-qubit circuit with 3 instructions:
├── X @ q[1]
├── X @ q[2]
└── X @ q[3]
>>> # Build a second circuit
>>> circuit2 = Circuit()
>>> circuit2.push(GateY(), range(1, 4))
4-qubit circuit with 3 instructions:
├── Y @ q[1]
├── Y @ q[2]
└── Y @ q[3]
>>> # Append the second circuit to the first one
>>> circuit1.append(circuit2)
>>> circuit1
4-qubit circuit with 6 instructions:
├── X @ q[1]
├── X @ q[2]
├── X @ q[3]
├── Y @ q[1]
├── Y @ q[2]
└── Y @ q[3]
This will modify circuit1 by appending all the operations from circuit2.
This function is particularly useful for building circuits by combining smaller circuit blocks.
Visualizing circuits#
To visualize a circuit use the draw()
method.
ere is a representation of a sim
.. doctest:: python
>>> circuit = Circuit()
>>> circuit.push(GateX(), range(0, 5))
5-qubit circuit with 5 instructions:
├── X @ q[0]
├── X @ q[1]
├── X @ q[2]
├── X @ q[3]
└── X @ q[4]
>>> circuit.draw()
┌─┐
q[0]: ╶┤X├────────────────────────────────────────────────────────────────────╴
└─┘┌─┐
q[1]: ╶───┤X├─────────────────────────────────────────────────────────────────╴
└─┘┌─┐
q[2]: ╶──────┤X├──────────────────────────────────────────────────────────────╴
└─┘┌─┐
q[3]: ╶─────────┤X├───────────────────────────────────────────────────────────╴
└─┘┌─┐
q[4]: ╶────────────┤X├────────────────────────────────────────────────────────╴
└─┘
Information such as the depth()
and the width (num_qubits()
) can be extracted from the circuit:
>>> circuit.depth(), circuit.num_qubits()
(1, 5)
Decompose#
Most gates can be decomposed into a combination of U and CX gates, the decompose()
function extracts such decomposition from a given circuit:
>>> circuit = Circuit()
>>> circuit.push(GateX(), 0)
1-qubit circuit with 1 instructions:
└── X @ q[0]
>>> # decompose the circuit
>>> circuit.decompose()
1-qubit circuit with 1 instructions:
└── U(pi, 0, pi, 0.0) @ q[0]