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:
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, 1-zvar 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, 1-bit, 1-zvar 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]
Reference¶
- class mimiqcircuits.Circuit(instructions=None)[source]
Bases:
objectRepresentation of a quantum circuit.
Operation can be added one by one to a circuit with the
c.push(operation, targets...)function- Parameters:
instructions (list of Instruction) – Instructiuons to add at construction.
- Raises:
TypeError – If initialization list contains non-Instruction objects.
Examples
>>> from mimiqcircuits import * >>> from symengine import pi
Create a new circuit object
>>> c = Circuit()
Add a GateX (Pauli-X) gate on qubit 0
>>> c.push(GateX(), 0) 1-qubit circuit with 1 instruction: └── X @ q[0]
Add a Controlled-NOT (CX) gate with control qubit 0 and target qubit 1
>>> c.push(GateCX(), 0, 1) 2-qubit circuit with 2 instructions: ├── X @ q[0] └── CX @ q[0], q[1]
Add a Parametric GateRX gate with parameters pi/4
>>> c.push(GateRX(pi / 4),0) 2-qubit circuit with 3 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] └── RX((1/4)*pi) @ q[0]
Add a Reset gate on qubit 0
>>> c.push(Reset(), 0) 2-qubit circuit with 4 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] ├── RX((1/4)*pi) @ q[0] └── Reset @ q[0]
Add a Barrier gate on qubits 0 and 1
>>> c.push(Barrier(2), 0, 1) 2-qubit circuit with 5 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] ├── RX((1/4)*pi) @ q[0] ├── Reset @ q[0] └── Barrier @ q[0:1]
Add a Measurement gate on qubit 0, storing the result in bit 0.
>>> c.push(Measure(), 0, 0) 2-qubit, 1-bit circuit with 6 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] ├── RX((1/4)*pi) @ q[0] ├── Reset @ q[0] ├── Barrier @ q[0:1] └── M @ q[0], c[0]
Add a Control gate with GateX as the target gate. The first 3 qubits are the control qubits.
>>> c.push(Control(3, GateX()), 0, 1, 2, 3) 4-qubit, 1-bit circuit with 7 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] ├── RX((1/4)*pi) @ q[0] ├── Reset @ q[0] ├── Barrier @ q[0:1] ├── M @ q[0], c[0] └── C₃X @ q[0:2], q[3]
Add a 3-qubit Parallel gate with GateX
>>> c.push(Parallel(3,GateX()),0, 1, 2) 4-qubit, 1-bit circuit with 8 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] ├── RX((1/4)*pi) @ q[0] ├── Reset @ q[0] ├── Barrier @ q[0:1] ├── M @ q[0], c[0] ├── C₃X @ q[0:2], q[3] └── ⨷ ³ X @ q[0], q[1], q[2]
To add operations without constructing them first, use the c.emplace(…) function.
Available operations¶
Gates
- Single qubit gates
GateX()GateY()GateZ()GateH()GateS()GateSDG()GateT()GateTDG()GateSX()GateSXDG()GateID()- Single qubit gates (parametric)
GateU()GateP()GateRX()GateRY()GateRZ()GateP()- Two qubit gates
GateCX()GateCY()GateCZ()GateCH()GateSWAP()GateISWAP()GateCS()GateCSX()GateECR()GateDCX()- Two qubit gates (parametric)
GateCU()GateCP()GateCRX()GateCRY()GateCRZ()GateRXX()GateRYY()GateRZZ()GateXXplusYY()GateXXminusYY()- Other
GateCustom()- No-ops
Barrier()- Non-unitary operations
Measure()Reset()- Composite operations
Control()Parallel()- Power & Inverse operations
Power()Inverse()- Generalized gates
QFT()PhaseGradient()
- __init__(instructions=None)[source]
- num_qubits()[source]
Returns the number of qubits in the circuit.
- num_bits()[source]
Returns the number of bits in the circuit.
- getparams()[source]
- listvars()[source]
- num_zvars()[source]
Returns the number of z-variables in the circuit.
- empty()[source]
Checks if the circuit is empty.
- push(operation, *args)[source]
Adds an Operation or an Instruction to the end of the circuit.
- Parameters:
operation (Operation or Instruction) – the quantum operation to add.
args (integers or iterables) – Target qubits and bits for the operation (not instruction), given as variable number of arguments.
- Raises:
TypeError – If operation is not an Operation object.
ValueError – If the number of arguments is incorrect or the target qubits specified are invalid.
Examples
Adding multiple operations to the Circuit (The args can be integers or integer-valued iterables)
>>> from mimiqcircuits import * >>> from symengine import pi >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] >>> c.push(GateT(), 0) 1-qubit circuit with 2 instructions: ├── H @ q[0] └── T @ q[0] >>> c.push(GateH(), [0,2]) 3-qubit circuit with 4 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] └── H @ q[2] >>> c.push(GateS(), 0) 3-qubit circuit with 5 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] └── S @ q[0] >>> c.push(GateCX(), [2, 0], 1) 3-qubit circuit with 7 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] └── CX @ q[0], q[1] >>> c.push(GateH(), 0) 3-qubit circuit with 8 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] └── H @ q[0] >>> c.push(Barrier(3), *range(3)) # equivalent to c.push(Barrier(3), 0, 1, 2) 3-qubit circuit with 9 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] ├── H @ q[0] └── Barrier @ q[0:2] >>> c.push(Measure(), range(3), range(3)) 3-qubit, 3-bit circuit with 12 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] ├── H @ q[0] ├── Barrier @ q[0:2] ├── M @ q[0], c[0] ├── M @ q[1], c[1] └── M @ q[2], c[2] >>> c 3-qubit, 3-bit circuit with 12 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] ├── H @ q[0] ├── Barrier @ q[0:2] ├── M @ q[0], c[0] ├── M @ q[1], c[1] └── M @ q[2], c[2]
- emplace(op, *regs)[source]
Constructs and adds an Operation to the end of the circuit.
It is useful to add to the circuit operations that are dependent on the number of qubits.
- Parameters:
Examples
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.emplace(GateX(), [0]) 1-qubit circuit with 1 instruction: └── X @ q[0] >>> c.emplace(GateRX(0.2), [0]) 1-qubit circuit with 2 instructions: ├── X @ q[0] └── RX(0.2) @ q[0] >>> c.emplace(QFT(), range(10)) 10-qubit circuit with 3 instructions: ├── X @ q[0] ├── RX(0.2) @ q[0] └── QFT @ q[0:9]
- insert(index, operation, *args)[source]
Inserts an operation or another circuit at a specific index in the circuit.
- Parameters:
index (int) – The index at which the operation should be inserted.
operation (Operation or Instruction) – the quantum operation to add.
args (integers or iterables) – Target qubits and bits for the operation (not instruction), given as variable number of arguments.
- Raises:
TypeError – If operation is not an Operation object.
ValueError – If the number of arguments is incorrect or the target qubits specified are invalid.
Examples
Inserting an operation to the specify index of the circuit
>>> from mimiqcircuits import * >>> c= Circuit() >>> c.push(GateX(), 0) 1-qubit circuit with 1 instruction: └── X @ q[0] >>> c.push(GateCX(),0,1) 2-qubit circuit with 2 instructions: ├── X @ q[0] └── CX @ q[0], q[1] >>> c.insert(1, GateH(), 0) 2-qubit circuit with 3 instructions: ├── X @ q[0] ├── H @ q[0] └── CX @ q[0], q[1]
- append(other)[source]
Appends all the gates of the given circuit at the end of the current circuit.
- Parameters:
other (Circuit) – the circuit to append.
- remove(index)[source]
Removes an instruction at a specific index from the circuit.
- Parameters:
index (int) – The index of the gate to remove.
- Raises:
IndexError – If index is out of range.
- inverse()[source]
Returns the inverse of the circuit.
- decompose(basis=None)[source]
Decompose all operations in the circuit to a target basis.
- Parameters:
basis – The target decomposition basis or rewrite rule. Can be a
DecompositionBasisorRewriteRule. Defaults toCanonicalBasis(GateU + GateCX).- Returns:
- A new circuit with all operations decomposed to the
target basis.
- Return type:
Examples
Decompose to canonical basis (default): >>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] <BLANKLINE> >>> c.push(GateCCX(), 0, 1, 2) 3-qubit circuit with 2 instructions: ├── H @ q[0] └── C₂X @ q[0:1], q[2] <BLANKLINE> >>> decomposed = c.decompose() >>> # All gates are now GateU or GateCX
Decompose to Clifford+T:
>>> from mimiqcircuits import CliffordTBasis >>> decomposed = c.decompose(CliffordTBasis())
Use a specific rewrite rule:
>>> from mimiqcircuits import ZYZRewrite >>> decomposed = c.decompose(ZYZRewrite())
See also
mimiqcircuits.decompose(): Module-level decompose functionmimiqcircuits.CanonicalBasis: Default decomposition basismimiqcircuits.CliffordTBasis: Clifford+T decomposition
- evaluate(d)[source]
- depth()[source]
Computes the depth of the quantum circuit, including qubits, bits, and z-registers.
- copy()[source]
- Creates a shallow copy of the circuit.
To create a full copy use deepcopy() instead.
- Returns:
A new Circuit object containing references to the same attributes as the original circuit
- Return type:
- deepcopy()[source]
Creates a copy of the object and for all its attributes
- Returns:
A new Circuit object fully identical the original circuit
- Return type:
- get_on_qubits(target_qubits)[source]
Get instructions that involve the specified target qubits.
- saveproto(file)[source]
Saves the circuit as a protobuf (binary) file.
- Parameters:
filename (str) – The name of the file to save the circuit to.
- Returns:
The number of bytes written to the file.
- Return type:
Examples
>>> from mimiqcircuits import * >>> from symengine import * >>> import tempfile >>> x, y = symbols("x y") >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] >>> c.push(GateXXplusYY(x**2, y),0,1) 2-qubit circuit with 2 instructions: ├── H @ q[0] └── XXplusYY(x**2, y) @ q[0:1] >>> c.push(Measure(),0,0) 2-qubit, 1-bit circuit with 3 instructions: ├── H @ q[0] ├── XXplusYY(x**2, y) @ q[0:1] └── M @ q[0], c[0] >>> tmpfile = tempfile.NamedTemporaryFile(suffix=".pb", delete=True) >>> c.saveproto(tmpfile.name) 64 >>> c.loadproto(tmpfile.name) 2-qubit, 1-bit circuit with 3 instructions: ├── H @ q[0] ├── XXplusYY(x**2, y) @ q[0:1] └── M @ q[0], c[0]
- Note:
This example uses a temporary file to demonstrate the save and load functionality. You can save your file with any name at any location using:
c.saveproto("example.pb") c.loadproto("example.pb")
- static loadproto(file)[source]
Loads a circuit from a protobuf (binary) file.
- Parameters:
filename (str) – The name of the file to load the circuit from.
- Returns:
The circuit loaded from the file.
- Return type:
Note
Look for example in
Circuit.saveproto()
- draw()[source]
Draws the entire quantum circuit on the ASCII canvas and handles the layout of various quantum operations.
This method iterates through all instructions in the circuit, determines the required width for each operation, and delegates the drawing of each operation to the appropriate specialized method based on the operation type. If an operation’s width exceeds the available space in the current row of the canvas, the canvas is printed and reset to continue drawing from a new starting point.
The method manages different operation types including control, measurement, reset, barrier, parallel, and conditional (if) operations using specific drawing methods from the AsciiCircuit class.
- Raises:
TypeError – If any item in the circuit’s instructions is not an instance of Instruction.
ValueError – If an operation cannot be drawn because it exceeds the available canvas width even after a reset.
- Prints:
The current state of the ASCII canvas, either incrementally after each operation if space runs out, or entirely at the end of processing all instructions.
- Returns:
None
- specify_operations()[source]
Summarizes the types and numbers of operations in the circuit.
This function inspects each instruction in the circuit and categorizes it by the number of qubits, bits, and z-variables involved in the operation. It then prints a summary of the total number of operations in the circuit and a breakdown of the number of operations grouped by their type.
Examples
>>> from mimiqcircuits import * >>> c = Circuit()
Add a Pauli-X (GateX) gate on qubit 0
>>> c.push(GateX(), 0) 1-qubit circuit with 1 instruction: └── X @ q[0]
Add a Controlled-NOT (CX) gate with control qubit 0 and target qubit 1
>>> c.push(GateCX(), 0, 1) 2-qubit circuit with 2 instructions: ├── X @ q[0] └── CX @ q[0], q[1]
Add a Measurement operation on qubit 0, storing the result in bit 0
>>> c.push(Measure(), 0, 0) 2-qubit, 1-bit circuit with 3 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] └── M @ q[0], c[0]
Add an ExpectationValue operation with GateX on qubit 1, storing the result in z-variable 2.
>>> c.push(ExpectationValue(GateX()), 1, 2) 2-qubit, 1-bit, 3-zvar circuit with 4 instructions: ├── X @ q[0] ├── CX @ q[0], q[1] ├── M @ q[0], c[0] └── ⟨X⟩ @ q[1], z[2]
Print a summary of the types and numbers of operations
>>> c.specify_operations() Total number of operations: 4 ├── 1 x 1_qubits ├── 1 x 2_qubits ├── 1 x 1_qubits & 1_bits └── 1 x 1_qubits & 1_zvars
- is_symbolic()[source]
Check whether the circuit contains any symbolic (unevaluated) parameters.
This method examines each instruction in the circuit to determine if any parameter remains symbolic (i.e., unevaluated). It recursively checks through each instruction and its nested operations, if any.
- Returns:
True if any parameter is symbolic (unevaluated), False if all parameters are fully evaluated.
- Return type:
Examples
>>> from mimiqcircuits import * >>> from symengine import * >>> x, y = symbols("x y") >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] >>> c.is_symbolic() False >>> c.push(GateP(x), 0) 1-qubit circuit with 2 instructions: ├── H @ q[0] └── P(x) @ q[0] >>> c.is_symbolic() True >>> c = c.evaluate({x: 1, y: 2}) >>> c 1-qubit circuit with 2 instructions: ├── H @ q[0] └── P(1) @ q[0] >>> c.is_symbolic() False
- decorate_on_match_single(g, decoration, before=False)[source]
Adds a decoration operation (e.g. noise channel or gate) before or after every instance of a given operation g.
The decoration operation decoration acts on the same qubits as the operation g to which it is being added.
- Parameters:
- Raises:
ValueError – If the decoration is the same as g (to avoid recursion).
- Returns:
The modified circuit (mutated in place).
- Return type:
Example
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] >>> c.decorate_on_match_single(GateH(), AmplitudeDamping(0.2)) 1-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.2) @ q[0] >>> c 1-qubit circuit with 1 instruction: └── H @ q[0]
- decorate_on_match_parallel(g, decoration, before=False)[source]
Adds a block of decoration operations (e.g. noise channels or gates) before or after each transversal block of a given operation g.
This method identifies blocks of consecutive, non-overlapping operations of the same type as g (e.g. multiple H gates acting on disjoint qubits) and inserts a corresponding block of decorations (e.g. noise gates) before or after them.
- Parameters:
- Raises:
ValueError – If the decoration is the same operation as g, to avoid recursion.
- Returns:
The modified circuit (mutated in place).
- Return type:
Example
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), range(3)) 3-qubit circuit with 3 instructions: ├── H @ q[0] ├── H @ q[1] └── H @ q[2] >>> c.decorate_on_match_parallel(GateH(), AmplitudeDamping(0.2)) 3-qubit circuit with 6 instructions: ├── H @ q[0] ├── H @ q[1] ├── H @ q[2] ├── AmplitudeDamping(0.2) @ q[0] ├── AmplitudeDamping(0.2) @ q[1] └── AmplitudeDamping(0.2) @ q[2]
- add_noise(g, kraus, before=False, parallel=False)[source]
Adds a noise operation kraus to every instance of the operation g in the circuit.
The noise operation kraus can be a Kraus channel or a gate and will act on the same qubits as the operation g to which it is being added.
The operations g and kraus must act on the same number of qubits.
- Parameters:
g (Operation or list of Operation) – The operation(s) to which the noise will be added.
kraus (krauschannel or list of krauschannel) – The noise operation(s) to be added.
before (bool or list of bool, optional) – If True, the noise is added before the operation. Default is False.
parallel (bool or list of bool, optional) – If True, noise is added as a block. Default is False.
- Raises:
ValueError – If g and kraus are not of the same length, or if their number of qubits differ.
TypeError – If before or parallel are not a bool or a list of bool.
- Returns:
The modified circuit with the noise added.
- Return type:
Examples:
Adding noise sequentially (not parallel):
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), [1,2,3]) 4-qubit circuit with 3 instructions: ├── H @ q[1] ├── H @ q[2] └── H @ q[3] >>> c.add_noise(GateH(), AmplitudeDamping(0.2)) 4-qubit circuit with 6 instructions: ├── H @ q[1] ├── AmplitudeDamping(0.2) @ q[1] ├── H @ q[2] ├── AmplitudeDamping(0.2) @ q[2] ├── H @ q[3] └── AmplitudeDamping(0.2) @ q[3]
Adding noise in parallel:
>>> c = Circuit() >>> c.push(GateH(), [1, 2, 3]) 4-qubit circuit with 3 instructions: ├── H @ q[1] ├── H @ q[2] └── H @ q[3] >>> c.add_noise(GateH(), AmplitudeDamping(0.2), parallel=True) 4-qubit circuit with 6 instructions: ├── H @ q[1] ├── H @ q[2] ├── H @ q[3] ├── AmplitudeDamping(0.2) @ q[1] ├── AmplitudeDamping(0.2) @ q[2] └── AmplitudeDamping(0.2) @ q[3]
Parallel will not work if gates aren’t transversal.
>>> c = Circuit() >>> c.push(GateCZ(), 1, range(2,5)) 5-qubit circuit with 3 instructions: ├── CZ @ q[1], q[2] ├── CZ @ q[1], q[3] └── CZ @ q[1], q[4] >>> c.add_noise(GateCZ(), Depolarizing2(0.1), parallel=True) 5-qubit circuit with 6 instructions: ├── CZ @ q[1], q[2] ├── Depolarizing(0.1) @ q[1:2] ├── CZ @ q[1], q[3] ├── Depolarizing(0.1) @ q[1,3] ├── CZ @ q[1], q[4] └── Depolarizing(0.1) @ q[1,4]
Adding noise before measurement (The before=True option is mostly used for Measure):
>>> c = Circuit() >>> c.push(Measure(), [1, 2, 3], [1, 2, 3]) 4-qubit, 4-bit circuit with 3 instructions: ├── M @ q[1], c[1] ├── M @ q[2], c[2] └── M @ q[3], c[3] >>> c.add_noise(Measure(), PauliX(0.1), before=True) 4-qubit, 4-bit circuit with 6 instructions: ├── PauliX(0.1) @ q[1] ├── M @ q[1], c[1] ├── PauliX(0.1) @ q[2] ├── M @ q[2], c[2] ├── PauliX(0.1) @ q[3] └── M @ q[3], c[3]
Adding unitary gates as noise in the same way:
>>> c = Circuit() >>> c.push(GateH(), [1, 2, 3]) 4-qubit circuit with 3 instructions: ├── H @ q[1] ├── H @ q[2] └── H @ q[3] >>> c.add_noise(GateH(), GateRX(0.01)) 4-qubit circuit with 6 instructions: ├── H @ q[1] ├── RX(0.01) @ q[1] ├── H @ q[2] ├── RX(0.01) @ q[2] ├── H @ q[3] └── RX(0.01) @ q[3]
- sample_mixedunitaries(rng=None, ids=False)[source]
sample_mixedunitaries(rng=None, ids=False)
Samples one unitary gate for each mixed unitary Kraus channel in the circuit.
This is possible because for mixed unitary noise channels, the probabilities of each Kraus operator are fixed (state-independent).
Note: This function is internally called (before applying any gate) when executing a circuit with noise using trajectories. It can also be used to generate samples of circuits without running them.
See also
krauschannel.ismixedunitary()MixedUnitary
- Parameters:
rng (optional) – Random number generator. If not provided, Python’s default random number generator is used.
ids (optional) – Boolean, default=False. Determines whether to include identity Kraus operators in the sampled circuit. If True, identity gates are added to the circuit; otherwise, they are omitted. Usually, most selected Kraus operators will be identity gates.
- Returns:
A copy of the circuit with every mixed unitary Kraus channel replaced by one of the unitary gates of the channel. Identity gates are omitted unless ids=True.
- Return type:
Examples
Gates and non-mixed-unitary Kraus channels remain unchanged:
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), [1, 2, 3]) 4-qubit circuit with 3 instructions: ├── H @ q[1] ├── H @ q[2] └── H @ q[3] >>> c.push(Depolarizing1(0.5), [1, 2, 3]) 4-qubit circuit with 6 instructions: ├── H @ q[1] ├── H @ q[2] ├── H @ q[3] ├── Depolarizing(0.5) @ q[1] ├── Depolarizing(0.5) @ q[2] └── Depolarizing(0.5) @ q[3] >>> c.push(AmplitudeDamping(0.5), [1, 2, 3]) 4-qubit circuit with 9 instructions: ├── H @ q[1] ├── H @ q[2] ├── H @ q[3] ├── Depolarizing(0.5) @ q[1] ├── Depolarizing(0.5) @ q[2] ├── Depolarizing(0.5) @ q[3] ├── AmplitudeDamping(0.5) @ q[1] ├── AmplitudeDamping(0.5) @ q[2] └── AmplitudeDamping(0.5) @ q[3]
>>> rng = random.Random(42)
>>> new_circuit = c.sample_mixedunitaries(rng=rng, ids=True) >>> print(new_circuit) 4-qubit circuit with 9 instructions: ├── H @ q[1] ├── H @ q[2] ├── H @ q[3] ├── X @ q[1] ├── I @ q[2] ├── I @ q[3] ├── AmplitudeDamping(0.5) @ q[1] ├── AmplitudeDamping(0.5) @ q[2] └── AmplitudeDamping(0.5) @ q[3]
By default, identities are not included:
>>> new_circuit = c.sample_mixedunitaries(rng=rng) >>> print(new_circuit) 4-qubit circuit with 8 instructions: ├── H @ q[1] ├── H @ q[2] ├── H @ q[3] ├── Y @ q[2] ├── Y @ q[3] ├── AmplitudeDamping(0.5) @ q[1] ├── AmplitudeDamping(0.5) @ q[2] └── AmplitudeDamping(0.5) @ q[3]
Different calls to the function generate different results:
>>> new_circuit = c.sample_mixedunitaries(rng=rng) >>> print(new_circuit) 4-qubit circuit with 7 instructions: ├── H @ q[1] ├── H @ q[2] ├── H @ q[3] ├── Z @ q[1] ├── AmplitudeDamping(0.5) @ q[1] ├── AmplitudeDamping(0.5) @ q[2] └── AmplitudeDamping(0.5) @ q[3]
>>> new_circuit = c.sample_mixedunitaries(rng=rng) >>> print(new_circuit) 4-qubit circuit with 7 instructions: ├── H @ q[1] ├── H @ q[2] ├── H @ q[3] ├── X @ q[3] ├── AmplitudeDamping(0.5) @ q[1] ├── AmplitudeDamping(0.5) @ q[2] └── AmplitudeDamping(0.5) @ q[3]
- remove_unused()[source]
Removes unused qubits, bits, and zvars from the given circuit. Returns (new_circuit, qubit_map, bit_map, zvar_map).
Examples
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateX(), 10) 11-qubit circuit with 1 instruction: └── X @ q[10] >>> c.push(GateSWAP(), 1, 20) 21-qubit circuit with 2 instructions: ├── X @ q[10] └── SWAP @ q[1,20] >>> c.push(GateH(), 4) 21-qubit circuit with 3 instructions: ├── X @ q[10] ├── SWAP @ q[1,20] └── H @ q[4] >>> c.remove_unused() (4-qubit circuit with 3 instructions: ├── X @ q[2] ├── SWAP @ q[0,3] └── H @ q[1] , {1: 0, 4: 1, 10: 2, 20: 3}, {}, {})
- remove_swaps(recursive=False)[source]
Remove all SWAP gates from the circuit by tracking qubit permutations and remapping subsequent operations to their correct physical qubits.
Returns a tuple of: - new_circuit: Circuit with SWAP gates removed and operations remapped - qubit_permutation: List where qubit_permutation[i] gives the physical qubit location of logical qubit i
- Parameters:
recursive – If True, recursively remove swaps from nested blocks/subcircuits. Default is False.
- Details:
When a SWAP gate is encountered on qubits (i, j), instead of keeping the gate:
1. The qubit mapping is updated to track that logical qubits i and j have exchanged physical positions 2. All subsequent gates are automatically remapped to operate on the correct physical qubits
This transformation preserves circuit semantics while eliminating SWAP overhead.
Examples
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), 1) 2-qubit circuit with 1 instruction: └── H @ q[1] >>> c.push(GateSWAP(), 1, 2) 3-qubit circuit with 2 instructions: ├── H @ q[1] └── SWAP @ q[1:2] >>> c.push(GateCX(), 2, 3) 4-qubit circuit with 3 instructions: ├── H @ q[1] ├── SWAP @ q[1:2] └── CX @ q[2], q[3] >>> new_c, perm = c.remove_swaps() >>> new_c 4-qubit circuit with 2 instructions: ├── H @ q[1] └── CX @ q[1], q[3] >>> perm [0, 2, 1, 3] >>> # Logical qubit 1 is at physical position 2 >>> # Logical qubit 2 is at physical position 1
Multiple swaps example:
>>> c2 = Circuit() >>> c2.push(GateSWAP(), 1, 2) 3-qubit circuit with 1 instruction: └── SWAP @ q[1:2] >>> c2.push(GateSWAP(), 2, 3) 4-qubit circuit with 2 instructions: ├── SWAP @ q[1:2] └── SWAP @ q[2:3] >>> c2.push(GateCX(), 1, 3) 4-qubit circuit with 3 instructions: ├── SWAP @ q[1:2] ├── SWAP @ q[2:3] └── CX @ q[1], q[3] >>> new_c2, perm2 = c2.remove_swaps() >>> new_c2 3-qubit circuit with 1 instruction: └── CX @ q[2], q[1] >>> perm2 [0, 2, 3, 1] >>> # After swaps: logical 1 -> physical 3, logical 3 -> physical 1
See also
mimiqcircuits.circuit_extras.remove_swaps: Standalone function version
- isunitary()[source]
Check if all instructions in the circuit are unitary.
- Returns:
True if all instructions are unitary, False otherwise.
- Return type:
- push_expval(hamiltonian, *qubits, firstzvar=None)
Push an expectation value estimation circuit for a given Hamiltonian.
This operation measures the expectation value of a Hamiltonian and stores the result in a Z-register, combining individual Pauli term evaluations.
For each term \(c_j P_j\), the circuit performs:
\[\langle \psi | c_j P_j | \psi \rangle\]and sums the contributions in a new z-register.
- Parameters:
hamiltonian (Hamiltonian) – The Hamiltonian to evaluate.
qubits (int) – The qubit mapping to use.
firstzvar (int, optional) – Index of the first Z-register to use.
- Returns:
The modified circuit.
- Return type:
Examples
>>> from mimiqcircuits import * >>> c = Circuit() >>> h = Hamiltonian() >>> h.push(1.0, PauliString("ZZ"), 0, 1) 2-qubit Hamiltonian with 1 terms: └── 1.0 * ZZ @ q[0,1] >>> c.push_expval(h, 1, 2) 3-qubit, 1-zvar circuit with 3 instructions: ├── ⟨ZZ⟩ @ q[1:2], z[0] ├── z[0] *= 1.0 └── z[0] += 0.0
See also
ExpectationValue,Multiply,Add
- push_lietrotter(h, qubits, t, steps)
Apply a Lie-Trotter expansion of the Hamiltonian
hto the circuitselffor the qubitsqubitsover total timetwithstepssteps.The Lie-Trotter expansion is a first-order approximation of the time evolution operator for a Hamiltonian composed of non-commuting terms. It decomposes the exponential of a sum of operators into a product of exponentials:
\[e^{-i H t} \approx \left[ \prod_{j=1}^m e^{-i c_j P_j \Delta t} \right]^n\]- where:
\(H = \sum_{j=1}^m c_j P_j\) is the Hamiltonian
\(P_j\) are Pauli strings
\(\Delta t = t / n\) is the time step size
\(n\) is the number of steps
This method is particularly useful for simulating quantum systems and time-evolving quantum states in quantum algorithms such as VQE or QAOA.
See also
push_suzukitrotter(),GateDeclExamples
>>> from mimiqcircuits import * >>> c = Circuit() >>> h = Hamiltonian() >>> h.push(1.0, PauliString("ZZ"), 0, 1) 2-qubit Hamiltonian with 1 terms: └── 1.0 * ZZ @ q[0,1] >>> c.push_lietrotter(h, (0, 1), t=1.0, steps=3) 2-qubit circuit with 3 instructions: ├── trotter(0.3333333333333333) @ q[0:1] ├── trotter(0.3333333333333333) @ q[0:1] └── trotter(0.3333333333333333) @ q[0:1]
- push_suzukitrotter(h, qubits, t, steps, order=2)
Apply Suzuki-Trotter expansion of the Hamiltonian
hto the circuitselffor the qubitsqubitsover timetwithstepssteps.The Suzuki-Trotter expansion approximates the time evolution operator of a quantum Hamiltonian using a sequence of exponentiated subterms. This is particularly useful for simulating quantum systems where the Hamiltonian is composed of non-commuting parts.
The expansion performed is a
2k-th order expansion according to the Suzuki construction.The second-order expansion is given by:
\[e^{-i H t} \approx \left[ \prod_{j=1}^{m} e^{-i \frac{\Delta t}{2} H_j} \prod_{j=m}^{1} e^{-i \frac{\Delta t}{2} H_j} \right]^n\]where the Hamiltonian \(H\) is a sum of terms:
\[H = \sum_{j=1}^{m} H_j\]and the Trotter step size is:
\[\Delta t = \frac{t}{n}\]Higher-order expansions follow the Suzuki recursion relation:
\[S_{2k}(\lambda) = [S_{2k-2}(p_k \lambda)]^2 \cdot S_{2k-2}((1 - 4p_k)\lambda) \cdot [S_{2k-2}(p_k \lambda)]^2\]with:
\[p_k = \left(4 - 4^{1/(2k - 1)}\right)^{-1}\]See also
push_lietrotter(),GateDeclExamples
>>> from mimiqcircuits import * >>> c = Circuit() >>> h = Hamiltonian() >>> h.push(1.0, PauliString("XX"), 0, 1) 2-qubit Hamiltonian with 1 terms: └── 1.0 * XX @ q[0,1] >>> c.push_suzukitrotter(h, (0, 1), t=1.0, steps=5, order=2) 2-qubit circuit with 5 instructions: ├── suzukitrotter_2(0.2) @ q[0:1] ├── suzukitrotter_2(0.2) @ q[0:1] ├── suzukitrotter_2(0.2) @ q[0:1] ├── suzukitrotter_2(0.2) @ q[0:1] └── suzukitrotter_2(0.2) @ q[0:1]
- push_yoshidatrotter(h, qubits, t, steps, order=4)
Apply Yoshida-Trotter expansion of the Hamiltonian
hto the circuitselffor the qubitsqubitsover timetwithstepssteps.The Yoshida-Trotter expansion approximates the time evolution operator of a quantum Hamiltonian using a symmetric composition of second-order Trotter formulas. This technique improves accuracy by canceling higher-order error terms in the Baker–Campbell–Hausdorff expansion.
The Yoshida expansion performed is a
2k-th order expansion using the symmetric structure:\[S_{2(k+1)}(t) = S_{2k}(w_1 \cdot t) \cdot S_{2k}(w_2 \cdot t) \cdot S_{2k}(w_1 \cdot t)\]where the weights are:
\[w_1 = \frac{1}{2 - 2^{1/(2k+1)}}, \quad w_2 = -\frac{2^{1/(2k+1)}}{2 - 2^{1/(2k+1)}}\]and the base case is the standard second-order Strang splitting:
\[S_2(\Delta t) = \prod_{j=1}^{m} e^{-i \Delta t H_j / 2} \prod_{j=m}^{1} e^{-i \Delta t H_j / 2}\]This method is particularly useful for simulating quantum systems where the Hamiltonian is composed of non-commuting parts, and is a computationally efficient alternative to full recursive Suzuki methods.
See also
push_suzukitrotter(),GateDecl- Parameters:
h (Hamiltonian) – The Hamiltonian object.
qubits (tuple) – Tuple of qubit indices.
t (float) – Total simulation time.
steps (int) – Number of Trotter steps to apply.
order (int) – Desired even expansion order (must be ≥ 2 and even).
- Returns:
The modified circuit.
- Return type:
Examples
>>> from mimiqcircuits import * >>> c = Circuit() >>> h = Hamiltonian() >>> h.push(1.0, PauliString("XY"), 0, 1) 2-qubit Hamiltonian with 1 terms: └── 1.0 * XY @ q[0,1] >>> h.push(0.5, PauliString("Z"), 0) 2-qubit Hamiltonian with 2 terms: ├── 1.0 * XY @ q[0,1] └── 0.5 * Z @ q[0] >>> c.push_yoshidatrotter(h, (0, 1), t=1.0, steps=3, order=4) 2-qubit circuit with 3 instructions: ├── yoshida_4(0.3333333333333333) @ q[0:1] ├── yoshida_4(0.3333333333333333) @ q[0:1] └── yoshida_4(0.3333333333333333) @ q[0:1]
- class mimiqcircuits.Instruction(operation, qubits=None, bits=None, zvars=None)[source]
Bases:
objectInitializes an instruction of a quantum circuit.
- Parameters:
- Raises:
TypeError – If operation is not a subclass of Gate or qubits is not a tuple.
ValueError – If qubits contains less than 1 or more than 2 elements.
Examples
>>> from mimiqcircuits import * >>> Instruction(GateX(),(0,),()) X @ q[0] >>> Instruction(Barrier(4),(0,1,2,3),()) Barrier @ q[0:3]
- __init__(operation, qubits=None, bits=None, zvars=None)[source]
- property operation
- property qubits
- property bits
- property zvars
- get_qubits()[source]
- get_bits()[source]
- get_zvars()[source]
- num_qubits()[source]
- num_bits()[source]
- num_zvars()[source]
- get_operation()[source]
- getparams()[source]
- listvars()[source]
- asciiwidth()[source]
- inverse()[source]
- isunitary()[source]
Check if Instruction is unitary.
- Returns:
True if Instruction is unitary, False otherwise.
- Return type:
- copy()[source]
- Creates a shallow copy of the instruction.
To create a full copy use deepcopy() instead.
- Returns:
A new Instruction object containing references to the same attributes as the original circuit
- Return type:
- deepcopy()[source]
Creates a copy of the object and for all its attributes
- Returns:
A new Instruction object fully identical the original circuit
- Return type:
- decompose()[source]
- evaluate(d)[source]
- matrix(nq=None)[source]
Return the matrix of this instruction, expanded and reordered to act on nq qubits if specified.
- mimiqcircuits.Circuit.push(self, operation, *args)
Adds an Operation or an Instruction to the end of the circuit.
- Parameters:
operation (Operation or Instruction) – the quantum operation to add.
args (integers or iterables) – Target qubits and bits for the operation (not instruction), given as variable number of arguments.
- Raises:
TypeError – If operation is not an Operation object.
ValueError – If the number of arguments is incorrect or the target qubits specified are invalid.
Examples
Adding multiple operations to the Circuit (The args can be integers or integer-valued iterables)
>>> from mimiqcircuits import * >>> from symengine import pi >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] >>> c.push(GateT(), 0) 1-qubit circuit with 2 instructions: ├── H @ q[0] └── T @ q[0] >>> c.push(GateH(), [0,2]) 3-qubit circuit with 4 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] └── H @ q[2] >>> c.push(GateS(), 0) 3-qubit circuit with 5 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] └── S @ q[0] >>> c.push(GateCX(), [2, 0], 1) 3-qubit circuit with 7 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] └── CX @ q[0], q[1] >>> c.push(GateH(), 0) 3-qubit circuit with 8 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] └── H @ q[0] >>> c.push(Barrier(3), *range(3)) # equivalent to c.push(Barrier(3), 0, 1, 2) 3-qubit circuit with 9 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] ├── H @ q[0] └── Barrier @ q[0:2] >>> c.push(Measure(), range(3), range(3)) 3-qubit, 3-bit circuit with 12 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] ├── H @ q[0] ├── Barrier @ q[0:2] ├── M @ q[0], c[0] ├── M @ q[1], c[1] └── M @ q[2], c[2] >>> c 3-qubit, 3-bit circuit with 12 instructions: ├── H @ q[0] ├── T @ q[0] ├── H @ q[0] ├── H @ q[2] ├── S @ q[0] ├── CX @ q[2], q[1] ├── CX @ q[0], q[1] ├── H @ q[0] ├── Barrier @ q[0:2] ├── M @ q[0], c[0] ├── M @ q[1], c[1] └── M @ q[2], c[2]
- mimiqcircuits.Circuit.insert(self, index, operation, *args)
Inserts an operation or another circuit at a specific index in the circuit.
- Parameters:
index (int) – The index at which the operation should be inserted.
operation (Operation or Instruction) – the quantum operation to add.
args (integers or iterables) – Target qubits and bits for the operation (not instruction), given as variable number of arguments.
- Raises:
TypeError – If operation is not an Operation object.
ValueError – If the number of arguments is incorrect or the target qubits specified are invalid.
Examples
Inserting an operation to the specify index of the circuit
>>> from mimiqcircuits import * >>> c= Circuit() >>> c.push(GateX(), 0) 1-qubit circuit with 1 instruction: └── X @ q[0] >>> c.push(GateCX(),0,1) 2-qubit circuit with 2 instructions: ├── X @ q[0] └── CX @ q[0], q[1] >>> c.insert(1, GateH(), 0) 2-qubit circuit with 3 instructions: ├── X @ q[0] ├── H @ q[0] └── CX @ q[0], q[1]
- mimiqcircuits.Circuit.append(self, other)
Appends all the gates of the given circuit at the end of the current circuit.
- Parameters:
other (Circuit) – the circuit to append.
- mimiqcircuits.Circuit.draw(self)
Draws the entire quantum circuit on the ASCII canvas and handles the layout of various quantum operations.
This method iterates through all instructions in the circuit, determines the required width for each operation, and delegates the drawing of each operation to the appropriate specialized method based on the operation type. If an operation’s width exceeds the available space in the current row of the canvas, the canvas is printed and reset to continue drawing from a new starting point.
The method manages different operation types including control, measurement, reset, barrier, parallel, and conditional (if) operations using specific drawing methods from the AsciiCircuit class.
- Raises:
TypeError – If any item in the circuit’s instructions is not an instance of Instruction.
ValueError – If an operation cannot be drawn because it exceeds the available canvas width even after a reset.
- Prints:
The current state of the ASCII canvas, either incrementally after each operation if space runs out, or entirely at the end of processing all instructions.
- Returns:
None
- mimiqcircuits.Circuit.decompose(self, basis=None)
Decompose all operations in the circuit to a target basis.
- Parameters:
basis – The target decomposition basis or rewrite rule. Can be a
DecompositionBasisorRewriteRule. Defaults toCanonicalBasis(GateU + GateCX).- Returns:
- A new circuit with all operations decomposed to the
target basis.
- Return type:
Examples
Decompose to canonical basis (default): >>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] <BLANKLINE> >>> c.push(GateCCX(), 0, 1, 2) 3-qubit circuit with 2 instructions: ├── H @ q[0] └── C₂X @ q[0:1], q[2] <BLANKLINE> >>> decomposed = c.decompose() >>> # All gates are now GateU or GateCX
Decompose to Clifford+T:
>>> from mimiqcircuits import CliffordTBasis >>> decomposed = c.decompose(CliffordTBasis())
Use a specific rewrite rule:
>>> from mimiqcircuits import ZYZRewrite >>> decomposed = c.decompose(ZYZRewrite())
See also
mimiqcircuits.decompose(): Module-level decompose functionmimiqcircuits.CanonicalBasis: Default decomposition basismimiqcircuits.CliffordTBasis: Clifford+T decomposition