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.
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:
using MimiqCircuits
ghz = Circuit()
push!(ghz, GateH(), 1)
for i in 2:4
push!(ghz, GateCX(), 1, i)
end
draw(ghz)
┌─┐
q[1]: ╶┤H├─●──●──●─╴
└─┘┌┴┐ │ │
q[2]: ╶───┤X├─┼──┼─╴
└─┘┌┴┐ │
q[3]: ╶──────┤X├─┼─╴
└─┘┌┴┐
q[4]: ╶─────────┤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 quantum register. When printing or drawing a circuit (with the function
draw
) the quantum registers will be denoted asq[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 letterc
. - 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 letterz
.
For the three registers operators can be applied on an arbitrary index starting from 1 (as does Julia in general contrary to python). 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:
using MimiqCircuits
# create empty circuit
circuit = Circuit()
# add X to the first qubit of the Quantum register
push!(circuit, GateX(), 1)
# compute Expectation value of qubit 1 and store complex number on the first Z-Register
ev = ExpectationValue(GateZ())
push!(circuit, ev, 1, 1)
# Measure the qubit state and store bit into the first classical register
push!(circuit, Measure(), 1, 1)
#drw the circuit
draw(circuit)
┌─┐┌───┐┌─┐
q[1]: ╶┤X├┤⟨Z⟩├┤M├╴
└─┘└─╥─┘└╥┘
║ ║
c: ══════╬═══╩═
║ 1
z: ══════╩═════
1
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 quantum 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 numqubits
, numbits
and numzvars
.
numqubits(circuit), numbits(circuit), numzvars(circuit)
(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 corresponding 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()
empty 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 Julia session:
?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 Julia we will mainly be using the push!
function. 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
push!(circuit, GateX(), 1)
1-qubit circuit with 1 instructions:
└── X @ q[1]
The text representation H @ q[1]
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:
push!(circuit, GateCX(), 1, 2)
2-qubit circuit with 1 instructions:
└── CX @ q[1], q[2]
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:
push!(circuit, GateX(), 1:10)
10-qubit circuit with 10 instructions:
├── 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:
# Adds 3 CX gates using respectively 1, 2 & 3 as the control qubits and 4 as the target qubit for all
push!(circuit, GateCX(), 1:3, 4)
# Adds 3 CX gates using respectively 2, 3 & 4 qubits as the target and 1 as the control qubit for all
push!(circuit, GateCX(), 1, 2:4)
# adds 3 CX gates using respectively the couples (1, 4), (2, 5), (3, 6) as the control and target qubits
push!(circuit, GateCX(), 1:3, 4:6)
draw(circuit)
q[1]: ╶─●────────●──●──●──●───────╴
│ ┌┴┐ │ │ │
q[2]: ╶─┼──●────┤X├─┼──┼──┼──●────╴
│ │ └─┘┌┴┐ │ │ │
q[3]: ╶─┼──┼──●────┤X├─┼──┼──┼──●─╴
┌┴┐┌┴┐┌┴┐ └─┘┌┴┐┌┴┐ │ │
q[4]: ╶┤X├┤X├┤X├──────┤X├┤X├─┼──┼─╴
└─┘└─┘└─┘ └─┘└─┘┌┴┐ │
q[5]: ╶─────────────────────┤X├─┼─╴
└─┘┌┴┐
q[6]: ╶────────────────────────┤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()
# Adds only 3 CX gates
push!(circuit, GateCX(), 1:3, 4:18)
draw(circuit)
q[1]: ╶─●───────╴
│
q[2]: ╶─┼──●────╴
│ │
q[3]: ╶─┼──┼──●─╴
┌┴┐ │ │
q[4]: ╶┤X├─┼──┼─╴
└─┘┌┴┐ │
q[5]: ╶───┤X├─┼─╴
└─┘┌┴┐
q[6]: ╶──────┤X├╴
└─┘
You can also use tuples or vectors in the exact same fashion:
push!(circuit, GateCX(), (1, 2), (3, 4))
push!(circuit, GateCX(), [1, 3], [2, 4])
draw(circuit)
q[1]: ╶─●─────●────╴
│ ┌┴┐
q[2]: ╶─┼──●─┤X├───╴
┌┴┐ │ └─┘
q[3]: ╶┤X├─┼─────●─╴
└─┘┌┴┐ ┌┴┐
q[4]: ╶───┤X├───┤X├╴
└─┘ └─┘
Insert
You can also insert an operation at a given index in the circuit using the insert! function:
circuit = Circuit()
push!(circuit, GateX(), 1)
push!(circuit, GateZ(), 1)
# Insert the gate at a specific index
insert!(circuit, 2, GateY(), 1)
circuit
1-qubit circuit with 3 instructions:
├── X @ q[1]
├── Y @ q[1]
└── Z @ 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()
push!(circuit1, GateX(), 1:3)
# Build a second circuit
circuit2 = Circuit()
push!(circuit2, GateY(), 1:3)
# Append the second circuit to the first one
append!(circuit1, circuit2)
circuit1
3-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.
draw(circuit)
┌─┐
q[1]: ╶┤X├────────────╴
└─┘┌─┐
q[2]: ╶───┤X├─────────╴
└─┘┌─┐
q[3]: ╶──────┤X├──────╴
└─┘┌─┐
q[4]: ╶─────────┤X├───╴
└─┘┌─┐
q[5]: ╶────────────┤X├╴
└─┘
Information such as the depth
and the width (numqubits
) can be extracted from the circuit:
depth(circuit), numqubits(circuit)
(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()
push!(circuit, GateX(), 1)
# decompose the circuit
decompose(circuit)
1-qubit circuit with 1 instructions:
└── U(π,0,π) @ q[1]