mimiqcircuits.circuit

Circuit class and utilities.

Classes

Circuit([instructions])

Representation of a quantum circuit.

class mimiqcircuits.circuit.Circuit(instructions=None)[source]

Bases: object

Representation 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:
  • operation (Type subclass of Operation) – the type of operation to add.

  • args (vararg of list) – A variable number of arguments compriseing a list of parameters (if the operation is parametric), one list of qubits for each quantum register, and one list of bits of every classical register supported.

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 DecompositionBasis or RewriteRule. Defaults to CanonicalBasis (GateU + GateCX).

Returns:

A new circuit with all operations decomposed to the

target basis.

Return type:

Circuit

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 function mimiqcircuits.CanonicalBasis: Default decomposition basis mimiqcircuits.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:

Circuit

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:

Circuit

get_on_qubits(target_qubits)[source]

Get instructions that involve the specified target qubits.

Parameters:

target_qubits (list or int) – Qubits for which to retrieve instructions.

Returns:

A new Circuit containing only the instructions that involve the specified qubits.

Return type:

Circuit

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:

int

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:

Circuit

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:

bool

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:
  • g (Operation) – The target operation to match (e.g. GateH()).

  • decoration (Operation) – The operation to insert (e.g. AmplitudeDamping(0.2)).

  • before (bool, optional) – If True, insert the decoration before the matched operation. Defaults to False.

Raises:

ValueError – If the decoration is the same as g (to avoid recursion).

Returns:

The modified circuit (mutated in place).

Return type:

Circuit

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:
  • g (Operation) – The operation type or instance to match (e.g. GateH()).

  • decoration (Operation) – The operation to insert (e.g. AmplitudeDamping(0.2)).

  • before (bool, optional) – If True, add the decoration before the matched block. Defaults to False.

Raises:

ValueError – If the decoration is the same operation as g, to avoid recursion.

Returns:

The modified circuit (mutated in place).

Return type:

Circuit

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:

Circuit

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:

Circuit

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:

bool

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:

Circuit

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 h to the circuit self for the qubits qubits over total time t with steps steps.

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(), GateDecl

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_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 h to the circuit self for the qubits qubits over time t with steps steps.

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(), GateDecl

Examples

>>> 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 h to the circuit self for the qubits qubits over time t with steps steps.

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:

Circuit

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]