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:
The new optimized
Circuit.A mapping (dict) of old qubit indices to new ones.
A mapping (dict) of old bit indices to new ones.
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(defaultFalse): IfTrue, recursively removes swaps from nested blocks.
It returns:
A new
Circuitwithout SWAP gates.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(defaultCliffordTBasis): 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(defaultFalse): IfTrue, 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:
PrepareStateTputs the ancilla (qubit 2) in the magic state.MP(XZZ)measures the rotation axis \(X_0 Z_1\) together with \(Z\) on the ancilla, into bit 2.Depending on bit 2, the ancilla is measured in X or in Y, into bit 3. The two
IfStatementlines implement this choice.The two
Xorlines record the byproduct described below in the Pauli frame.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:
- 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:
The qubit mapping is updated to track that logical qubits i and j have exchanged physical positions
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 (
RPauliwith 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, whateverbasisdoes 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 withNot.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,MeasureZZandMeasurePaulimeasure a Pauli product \(P\) and become aMeasurePauliof \(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
IfStatementwhose body is made only of Pauli operators applies one Pauli product \(P\) when its condition holds. The body can beGateX,GateY,GateZ,GateID, aPauliString, or aBlock,Parallel,RepeatorGateCallholding only such operations, at any depth. It becomes oneIfStatementof thePauliString\(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
RPaulirotation 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=Truethe rotations are removed as well. Each \(\pi/8\) rotation on \(P\) consumes a magic state \(T\ket{+}\), prepared withPrepareStateTon one ancilla qubit that is reused for every rotation:MeasurePauliof \(P \otimes Z\) on the data qubits and the ancilla, with outcome \(m_1\).MeasurePauliof \(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 twoIfStatementoperations.
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\),
Xoroperations multiply the frame by \(P\). Each later \(m_1\) or measurement outcome whose Pauli anticommutes with \(F\) is flipped in place, again withXor, 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
Xoroperations. A condition other than a single bit equal to 1 is first evaluated into one extra bit withSetBit1and 3-bitAndoperations; each condition bit compared with 0 is flipped withNotwhile 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
XororAnd, aNotor aSetBit1, 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, conditionalPauliStringandRPaulioperations, classical operations and annotations. Withmagic_states=True,PrepareStateT, conditionalMeasurePaulioperations and classical logic replace the rotations and conditional Paulis.- Return type:
- 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 aValueError.DecompositionError – if
basiscannot 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