Circuit compilation & optimization

MIMIQ provides tools to optimize and transform quantum circuits.

Cleaning circuits

Remove unused

The remove_unused() function cleans up a circuit by removing unused qubits, classical bits, and Z-variables.

new_circuit, qubit_map, bit_map, zvar_map = remove_unused(circuit)

It returns:

  1. The new optimized Circuit.

  2. A mapping (dict) of old qubit indices to new ones.

  3. A mapping (dict) of old bit indices to new ones.

  4. A mapping (dict) of old Z-variable indices to new ones.

Example:

>>> from mimiqcircuits import *
>>> c = Circuit()
>>> c.push(GateH(), 0)
>>> c.push(GateCX(), 0, 2)  # Qubit 1 is unused
>>> new_c, qmap, bmap, zmap = remove_unused(c)
>>> new_c
2-qubit circuit with 2 instructions:
├── H @ q[0]
└── CX @ q[0], q[1]

Optimization passes

Remove SWAPs

The remove_swaps() function eliminates SWAP gates by tracking qubit permutations and remapping subsequent operations.

new_circuit, final_permutation = remove_swaps(circuit, recursive=False)

Arguments:

  • circuit: The input circuit.

  • recursive (default False): If True, recursively removes swaps from nested blocks.

It returns:

  1. A new Circuit without SWAP gates.

  2. A list representing the final physical permutation of qubits.

Example:

>>> 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 = remove_swaps(c)
>>> new_c
4-qubit circuit with 2 instructions:
├── H @ q[1]
└── CX @ q[1], q[3]

>>> perm
[0, 2, 1, 3]

Pauli-based computation

In Pauli-based computation a circuit is expressed with two kinds of operations only: \(\pi/8\) rotations about multi-qubit Pauli operators, and measurements of multi-qubit Pauli operators. This is the form used by lattice-surgery architectures for surface codes, where every logical operation is a Pauli product measurement.

The pauli_based_computation() function (also available as pauli_based_computation()) performs this rewrite:

new_circuit = pauli_based_computation(circuit, basis=None, magic_states=False)

Arguments:

  • circuit: The input circuit.

  • basis (default CliffordTBasis): The basis used to decompose the circuit into Clifford+T gates first. Measurements, resets and conditional Pauli operators are kept whole, whatever the basis.

  • magic_states (default False): If True, each rotation is further replaced by the injection of a magic state.

The circuit is decomposed into Clifford+T, then every Clifford gate is moved to the end of the circuit. On the way it changes each T gate into an RPauli rotation of angle \(\pm\pi/4\) and each measurement of a Pauli product (Measure, MeasureX, MeasureXX, MeasurePauli and the like) into a MeasurePauli. The Cliffords that end up after the last measurement do not change any outcome, so they are dropped. The new circuit gives the same measurement statistics as the original one, but not the same final state.

Example:

Prepare a Bell pair, apply a T gate to qubit 1 and measure both qubits:

>>> from mimiqcircuits import *
>>> c = Circuit()
>>> _ = c.push(GateH(), 0)
>>> _ = c.push(GateCX(), 0, 1)
>>> _ = c.push(GateT(), 1)
>>> _ = c.push(Measure(), range(2), range(2))
>>> pauli_based_computation(c)
2-qubit, 2-bit circuit with 3 instructions:
├── R("XZ", (1/4)*pi) @ q[0:1]
├── MP(X) @ q[0], c[0]
└── MP(XZ) @ q[0:1], c[1]

The Hadamard and CX gates before the T gate map \(Z_1\) to \(X_0 Z_1\), so the T gate becomes a \(\pi/8\) rotation about \(X_0 Z_1\) (RPauli with angle \(\pi/4\)). The same gates turn the measurements of \(Z_0\) and \(Z_1\) into measurements of \(X_0\) and \(X_0 Z_1\), written to the original bits.

Conditional operations and resets

An IfStatement whose body contains only Pauli operators, directly or inside a PauliString, Block, Parallel, Repeat or GateCall, applies one Pauli product. It becomes one conditional PauliString, with the Clifford gates before it moved past it like past a measurement. A conditional T or T† gate becomes a conditional RPauli rotation.

A reset (Reset, ResetX, ResetY) measures its qubit into a new bit, placed after the bits of the circuit, then applies a conditional Pauli that brings the qubit back to the reset state. The measure-and-reset operations do the same with the bit they are given.

>>> c2 = Circuit()
>>> _ = c2.push(GateH(), 0)
>>> _ = c2.push(Measure(), 0, 0)
>>> _ = c2.push(GateH(), 1)
>>> _ = c2.push(IfStatement(GateZ(), BitString("1")), 1, 0)
>>> _ = c2.push(Reset(), 0)
>>> _ = c2.push(Measure(), range(2), range(1, 3))
>>> pauli_based_computation(c2)
2-qubit, 4-bit circuit with 6 instructions:
├── MP(X) @ q[0], c[0]
├── IF(c==1) X @ q[1], condition[0]
├── MP(X) @ q[0], c[3]
├── IF(c==1) Z @ q[0], condition[3]
├── MP(X) @ q[0], c[1]
└── MP(X) @ q[1], c[2]

The Hadamard gate on qubit 1 turns the conditional \(Z_1\) into a conditional \(X_1\). The reset of qubit 0 measures \(X_0\) into the new bit 3 and applies \(Z_0\) when the outcome is 1.

Other conditional operations are refused with a ValueError. After a conditional Clifford gate, such as a conditional Hadamard, the Clifford gates to move would depend on the condition bits, so the later rotations and measurements could not be computed before the circuit runs.

Magic-state injection

A \(\pi/8\) rotation can itself be carried out with Pauli product measurements by consuming a magic state \(T\ket{+}\), prepared with PrepareStateT. With magic_states=True every rotation is replaced in this way, using one extra ancilla qubit, two extra bits per rotation and two bits per data qubit for the Pauli frame:

>>> pauli_based_computation(c, magic_states=True)
3-qubit, 8-bit circuit with 8 instructions:
├── PrepareStateT @ q[2]
├── MP(XZZ) @ q[0:2], c[2]
├── IF(c==0) MP(X) @ q[2], c[3], condition[2]
├── IF(c==1) MP(Y) @ q[2], c[3], condition[2]
├── c[4] ^= c[3]
├── c[7] ^= c[3]
├── MP(X) @ q[0], c[0]
└── MP(XZ) @ q[0:1], c[1]

Reading the result line by line:

  1. PrepareStateT puts the ancilla (qubit 2) in the magic state.

  2. MP(XZZ) measures the rotation axis \(X_0 Z_1\) together with \(Z\) on the ancilla, into bit 2.

  3. Depending on bit 2, the ancilla is measured in X or in Y, into bit 3. The two IfStatement lines implement this choice.

  4. The two Xor lines record the byproduct described below in the Pauli frame.

  5. The final measurements are the same as before.

When bit 3 is 1 the rotation comes with an extra Pauli \(X_0 Z_1\) on the data qubits. It is not corrected with gates. Instead, the product of all such Paulis is kept in classical bits, the Pauli frame: bit 4 holds its X part on qubit 0 and bit 7 its Z part on qubit 1. Later outcomes that the frame would flip are corrected in place with 3-bit Xor operations. Here both final measurements commute with \(X_0 Z_1\), so no correction is needed.

With magic states a conditional Pauli is not applied either: it multiplies the frame when its condition holds. A conditional T or T† gate is refused, since a magic state cannot be injected conditionally.

Reference

mimiqcircuits.remove_unused(self)[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(GateH(), 0)
1-qubit circuit with 1 instruction:
└── H @ q[0]

>>> c.push(GateCX(), 0, 2)  # qubit 1 is unused
3-qubit circuit with 2 instructions:
├── H @ q[0]
└── CX @ q[0], q[2]

>>> c.push(Measure(), 2, 2)  # bits 0 & 1 are unused
3-qubit, 3-bit circuit with 3 instructions:
├── H @ q[0]
├── CX @ q[0], q[2]
└── M @ q[2], c[2]

>>> c.push(ExpectationValue(GateX()), 0, 1)  # zvar 0 is unused
3-qubit, 3-bit, 2-zvar circuit with 4 instructions:
├── H @ q[0]
├── CX @ q[0], q[2]
├── M @ q[2], c[2]
└── ⟨X⟩ @ q[0], z[1]

>>> new_c, qmap, bmap, zmap = remove_unused(c)
>>> new_c
2-qubit, 1-bit, 1-zvar circuit with 4 instructions:
├── H @ q[0]
├── CX @ q[0], q[1]
├── M @ q[1], c[0]
└── ⟨X⟩ @ q[0], z[0]
>>> qmap
{0: 0, 2: 1}
>>> bmap
{2: 0}
>>> zmap
{1: 0}
mimiqcircuits.remove_swaps(circuit, recursive=False, cache=None)[source]

Remove all SWAP gates from a quantum circuit by tracking qubit permutations and remapping subsequent operations to their correct physical qubits.

Returns:

A tuple containing:
  • new_circuit: Circuit with SWAP gates removed and operations remapped

  • qubit_permutation: Final permutation where qubit_permutation[i] gives the physical qubit location of logical qubit i

Return type:

tuple

Parameters:
  • circuit – Input quantum circuit

  • recursive – If True, recursively remove swaps from nested blocks/subcircuits

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 = remove_swaps(c)
>>> new_c
4-qubit circuit with 2 instructions:
├── H @ q[1]
└── CX @ q[1], q[3]

>>> perm
[0, 2, 1, 3]
>>> c = Circuit()
>>> c.push(GateSWAP(), 1, 2)
3-qubit circuit with 1 instruction:
└── SWAP @ q[1:2]

>>> c.push(GateSWAP(), 2, 3)
4-qubit circuit with 2 instructions:
├── SWAP @ q[1:2]
└── SWAP @ q[2:3]

>>> c.push(GateCX(), 1, 3)
4-qubit circuit with 3 instructions:
├── SWAP @ q[1:2]
├── SWAP @ q[2:3]
└── CX @ q[1], q[3]

>>> new_c, perm = remove_swaps(c)
>>> new_c
4-qubit circuit with 2 instructions:
├── CX @ q[2], q[1]
└── ID @ q[3]

>>> perm
[0, 2, 3, 1]

# Example of checking recursive removel of swaps

>>> inner = Circuit()
>>> _ = inner.push(GateSWAP(), 0, 1)
>>> _ = inner.push(GateCX(), 1, 0)
>>> Inner = GateDecl("Inner", [], inner)
>>> mid = Circuit()
>>> _ = mid.push(GateSWAP(), 0, 1)
>>> _ = mid.push(GateCall(Inner, ()), 0, 1)
>>> Mid = GateDecl("Mid", [], mid)
>>> outer = Circuit()
>>> _ = outer.push(GateSWAP(), 1, 2)
>>> _ = outer.push(GateCall(Mid, ()), 1, 2)
>>> Outer = GateDecl("Outer", [], outer)
>>> c = Circuit()
>>> _ = c.push(GateSWAP(), 1, 2)
>>> _ = c.push(GateCall(Outer, ()), 0, 1, 2)
>>> c
3-qubit circuit with 2 instructions:
├── SWAP @ q[1:2]
└── Outer() @ q[0:2]
>>> c.decompose()
3-qubit circuit with 13 instructions:
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
└── CX @ q[2], q[1]
>>> c.decompose().decompose()
3-qubit circuit with 13 instructions:
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[2]
├── CX @ q[2], q[1]
├── CX @ q[1], q[2]
└── CX @ q[2], q[1]

# Test to see that all swaps are removed recursively

>>> new_c, _ = remove_swaps(c, recursive=True)
>>> any(isinstance(i.get_operation(), GateSWAP) for i in new_c)
False
>>> Outercheck = new_c.instructions[0].get_operation()._decl
>>> any(isinstance(i.get_operation(), GateSWAP) for i in Outercheck.circuit)
False
>>> Midcheck = Outercheck.circuit.instructions[0].get_operation()._decl
>>> any(isinstance(i.get_operation(), GateSWAP) for i in Midcheck.circuit)
False
>>> Innercheck = Midcheck.circuit.instructions[0].get_operation()._decl
>>> any(isinstance(i.get_operation(), GateSWAP) for i in Innercheck.circuit)
False
mimiqcircuits.pauli_based_computation(circuit, basis=None, magic_states=False)[source]

Rewrite a circuit in Pauli-based computation form.

The result is a sequence of \(\pi/8\) Pauli product rotations (RPauli with angle \(\pm\pi/4\)) and Pauli product measurements (MeasurePauli), with no Clifford gates left.

The circuit is first decomposed with basis, which must produce Clifford+T gates, the measurements and resets listed below, and the conditional operations described below. Those measurements, resets and conditional Pauli products are kept whole, whatever basis does with them. Each Clifford \(C\) is then moved past every later T gate and measurement, conjugating them: a T gate on qubit \(q\) becomes the rotation \(\exp(-i \frac{\pi}{8} C^\dagger Z_q C)\), and a measurement of \(Z_q\) becomes a measurement of \(C^\dagger Z_q C\). When the conjugated observable has a minus sign, the rotation angle is negated, or the measured bit is flipped with Not.

The Cliffords left after the last measurement do not change any outcome and are dropped. The result therefore reproduces the measurement statistics of circuit, not its final quantum state, and each rotation is exact up to a global phase. Classical operations and annotations are kept in place; barriers and identity gates are dropped.

Measurements, resets and conditional operations:
  • Measure, MeasureX, MeasureY, MeasureXX, MeasureYY, MeasureZZ and MeasurePauli measure a Pauli product \(P\) and become a MeasurePauli of \(C^\dagger P C\).

  • A reset in the Z, X or Y basis (Reset, ResetX, ResetY) measures \(Z\), \(X\) or \(Y\) into a new bit, then applies, if the outcome is 1, the Pauli that maps the \(-1\) eigenstate to the \(+1\) one: \(X\) for the Z and Y bases, \(Z\) for the X basis. The measure-and-reset operations (MeasureReset, MeasureResetX, MeasureResetY) do the same with the bit they are given.

  • An IfStatement whose body is made only of Pauli operators applies one Pauli product \(P\) when its condition holds. The body can be GateX, GateY, GateZ, GateID, a PauliString, or a Block, Parallel, Repeat or GateCall holding only such operations, at any depth. It becomes one IfStatement of the PauliString \(C^\dagger P C\) with the same condition. The phase of \(P\) is dropped, as it only multiplies one classical branch.

  • A conditional T or T† gate becomes a conditional RPauli rotation with the same condition.

Any other conditional operation is refused. After a conditional Clifford gate, the accumulated Clifford would depend on the condition bits, so the later rotations and measurements could not be computed before the circuit runs.

Magic-state injection:

With magic_states=True the rotations are removed as well. Each \(\pi/8\) rotation on \(P\) consumes a magic state \(T\ket{+}\), prepared with PrepareStateT on one ancilla qubit that is reused for every rotation:

  1. MeasurePauli of \(P \otimes Z\) on the data qubits and the ancilla, with outcome \(m_1\).

  2. MeasurePauli of \(X\) on the ancilla if \(m_1 = 0\), of \(Y\) if \(m_1 = 1\) (swapped for a negative angle), with outcome \(m_2\). The choice is made by two IfStatement operations.

When \(m_2 = 1\) the rotation is applied together with a Pauli byproduct \(P\). Byproducts are not corrected with gates. Instead, their product \(F\) is tracked in classical bits, the Pauli frame: one bit for the X part and one for the Z part of \(F\) on each data qubit. When \(m_2 = 1\), Xor operations multiply the frame by \(P\). Each later \(m_1\) or measurement outcome whose Pauli anticommutes with \(F\) is flipped in place, again with Xor, so the bits hold the outcomes of the ideal circuit.

A conditional Pauli, including the correction of a reset, is not applied either: it multiplies the frame when its condition holds, with the same Xor operations. A condition other than a single bit equal to 1 is first evaluated into one extra bit with SetBit1 and 3-bit And operations; each condition bit compared with 0 is flipped with Not while it is read and flipped back right after. A conditional T or T† gate is refused, since a magic state cannot be injected conditionally.

Every classical operation added is therefore a 3-bit Xor or And, a Not or a SetBit1, whatever the size of the circuit. The result contains only Pauli product measurements, magic-state preparations and classical logic.

Bit layout:

Qubits and bits are counted from 0. With \(n_b\) the number of bits of the decomposed circuit and \(r\) its number of resets, the \(j\)-th reset (counting from 0) writes its outcome to bit \(n_b + j\). With magic states, let \(n'_b = n_b + r\), \(n\) be the number of qubits and \(t\) the number of T and T† gates of the decomposed circuit. The ancilla is qubit \(n\). The \(k\)-th rotation (counting from 0) writes \(m_1\) to bit \(n'_b + 2k\) and \(m_2\) to bit \(n'_b + 2k + 1\). The frame bits of qubit \(q\) follow: the X part at bit \(n'_b + 2t + q\) and the Z part at bit \(n'_b + 2t + n + q\). The condition of a conditional Pauli, when it needs its own bit, goes to bit \(n'_b + 2t + 2n\).

Parameters:
  • circuit (Circuit) – the circuit to rewrite.

  • basis (DecompositionBasis, optional) – the basis used for the first decomposition. Defaults to CliffordTBasis.

  • magic_states (bool, optional) – replace every rotation by magic-state injection. Defaults to False.

Returns:

a new circuit made of RPauli, MeasurePauli, conditional PauliString and RPauli operations, classical operations and annotations. With magic_states=True, PrepareStateT, conditional MeasurePauli operations and classical logic replace the rotations and conditional Paulis.

Return type:

Circuit

Raises:
  • ValueError – if the decomposed circuit contains anything other than Clifford+T gates, the measurements and resets listed above, the conditional operations listed above, classical operations and annotations: for example a non-Clifford gate, a conditional Clifford gate or a conditional measurement. With magic_states=True, a conditional T or T† gate also raises a ValueError.

  • DecompositionError – if basis cannot decompose an operation, such as a noise channel.

Examples

Prepare a Bell pair, apply a T gate to qubit 1 and measure both qubits:

>>> from mimiqcircuits import *
>>> c = Circuit()
>>> _ = c.push(GateH(), 0)
>>> _ = c.push(GateCX(), 0, 1)
>>> _ = c.push(GateT(), 1)
>>> _ = c.push(Measure(), range(2), range(2))
>>> pauli_based_computation(c)
2-qubit, 2-bit circuit with 3 instructions:
├── R("XZ", (1/4)*pi) @ q[0:1]
├── MP(X) @ q[0], c[0]
└── MP(XZ) @ q[0:1], c[1]

The Clifford before the T gate is \(C = \mathrm{CX}_{01} H_0\), so the T gate becomes a \(\pi/8\) rotation on \(C^\dagger Z_1 C = X_0 Z_1\). The two measurements of \(Z_0\) and \(Z_1\) become measurements of \(X_0\) and \(X_0 Z_1\), and write to the same bits as before.

With magic states, the rotation becomes an injection on the ancilla qubit 2, whose outcomes \(m_1\) and \(m_2\) go to the new bits 2 and 3:

>>> pauli_based_computation(c, magic_states=True)
3-qubit, 8-bit circuit with 8 instructions:
├── PrepareStateT @ q[2]
├── MP(XZZ) @ q[0:2], c[2]
├── IF(c==0) MP(X) @ q[2], c[3], condition[2]
├── IF(c==1) MP(Y) @ q[2], c[3], condition[2]
├── c[4] ^= c[3]
├── c[7] ^= c[3]
├── MP(X) @ q[0], c[0]
└── MP(XZ) @ q[0:1], c[1]

The ancilla is measured in X or Y depending on \(m_1\) (bit 2). The byproduct \(X_0 Z_1\) goes into the frame: bit 4 holds its X part on qubit 0 and bit 7 its Z part on qubit 1. The final measurements commute with it, so their bits are not corrected.

A conditional Pauli and a reset. The Hadamard gate before the conditional \(Z_1\) turns it into a conditional \(X_1\), and the reset of qubit 0 after its Hadamard gate measures \(X_0\) into the new bit 3, then applies \(Z_0\) if the outcome is 1:

>>> c = Circuit()
>>> _ = c.push(GateH(), 0)
>>> _ = c.push(Measure(), 0, 0)
>>> _ = c.push(GateH(), 1)
>>> _ = c.push(IfStatement(GateZ(), BitString("1")), 1, 0)
>>> _ = c.push(Reset(), 0)
>>> _ = c.push(Measure(), range(2), range(1, 3))
>>> pauli_based_computation(c)
2-qubit, 4-bit circuit with 6 instructions:
├── MP(X) @ q[0], c[0]
├── IF(c==1) X @ q[1], condition[0]
├── MP(X) @ q[0], c[3]
├── IF(c==1) Z @ q[0], condition[3]
├── MP(X) @ q[0], c[1]
└── MP(X) @ q[1], c[2]

References

  • D. Litinski, “A Game of Surface Codes: Large-Scale Quantum Computing with Lattice Surgery”, Quantum 3, 128 (2019).

  • S. Bravyi, G. Smith, J. A. Smolin, “Trading Classical and Quantum Computational Resources”, Phys. Rev. X 6, 021043 (2016).

See also

MeasurePauli, PrepareStateT, RPauli, CliffordTBasis