Circuit Compilation & Optimization
MimiqCircuitsBase.depth — Method
depth(circuit)Compute the depth of a quantum circuit.
The depth of a quantum circuit is a metric computing the maximum time (in units of quantum gates application) between the input and output of the circuit.
MimiqCircuitsBase.remove_swaps — Function
remove_swaps(circuit; recursive=false)Remove all SWAP gates from a quantum 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 remappedqubit_permutation: Final permutation wherequbit_permutation[i]gives the physical qubit location of logical qubiti
Arguments
circuit: Input quantum circuitrecursive=false: Iftrue, 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
iandjhave 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
julia> c = Circuit()
push!(c, GateH(), 1)
push!(c, GateSWAP(), 1, 2)
push!(c, GateCX(), 2, 3)
new_c, perm = remove_swaps(c)
new_c
3-qubit circuit with 2 instructions:
├── H @ q[1]
└── CX @ q[1], q[3]
julia> perm # Logical qubit 1 is at physical position 2, logical 2 at position 1
3-element Vector{Int64}:
2
1
3julia> c = Circuit()
push!(c, GateSWAP(), 1, 2)
push!(c, GateSWAP(), 2, 3)
push!(c, GateCX(), 1, 3) # After swaps: logical 1 -> physical 3, logical 3 -> physical 1
new_c, perm = remove_swaps(c)
new_c
3-qubit circuit with 2 instructions:
├── CX @ q[2], q[1]
└── ID @ q[3]
julia> perm
3-element Vector{Int64}:
2
3
1MimiqCircuitsBase.remove_unused — Method
remove_unused(circuit)Removes unused qubits, bits, and zvars from the given quantum circuit.
Returns the modified circuit and the mappings from old indices to new indices.
Example
begin
c = Circuit()
push!(c, GateH(), 1)
push!(c, GateCX(), 1, 3:2:7)
push!(c, Measure(), 1:2:7, 1:2:7)
push!(c, ExpectationValue(GateZ()), 9, 9)
end
cr, qubit_map, bit_map, zvar_map = remove_unused(c)
cr
# output
5-qubit, 4-bit, 1-vars circuit with 9 instructions:
├── H @ q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[3]
├── CX @ q[1], q[4]
├── M @ q[1], c[1]
├── M @ q[2], c[2]
├── M @ q[3], c[3]
├── M @ q[4], c[4]
└── ⟨Z⟩ @ q[5], z[1]MimiqCircuitsBase.fuse — Method
fuse(c::Circuit; max_support::Int=2, max_diagonal_support::Int=max_support) -> CircuitFuse runs of unitary gates spanning at most max_support qubits into single GateCustom blocks, preserving the circuit's overall unitary exactly.
Raising max_support lets a block cover more wires and so emit fewer, wider blocks. Which gates end up together is decided greedily, so the result is not guaranteed to be the smallest possible circuit.
Runs where every gate is diagonal in the computational basis are budgeted separately, by max_diagonal_support, and collapse into a GateCustomDiagonal instead: such a block is 2^k numbers rather than 4^k, and it is applied without mixing amplitudes, so it is usually worth allowing wider than a dense one. A cluster keeps the diagonal budget only while all of its gates are diagonal — the first dense gate joining it brings it back under max_support.
Non-unitary or opaque operations — measurements, resets, noise channels, Barrier, IfStatement/WhileStatement, and gates with symbolic parameters — are emitted unchanged and act as fusion boundaries: no gate fuses across one on a shared wire. A cluster of a single gate is emitted as its original instruction (never rewrapped as GateCustom{1}), and qubit indices are never relabeled.
max_support defaults to 2 and max_diagonal_support to max_support, so diagonal runs are recognised but not widened unless asked. Emitting GateCustom{k} or GateCustomDiagonal{k} for k > 2 is legal; whether a backend can apply it is that backend's concern — a backend that has to decompose the block again is better served by a smaller budget.
Examples
julia> c = push!(Circuit(), GateH(), 1);
julia> push!(c, GateCX(), 1, 2); # H then CX both act on qubits {1, 2}
julia> fused = fuse(c); # ... so they collapse into one block
julia> length(fused)
1
julia> getoperation(fused[1]) isa GateCustom
true
julia> d = push!(Circuit(), GateP(0.1), 1);
julia> push!(d, GateCZ(), 1, 2); # both diagonal, so the block is too
julia> getoperation(fuse(d)[1]) isa GateCustomDiagonal
trueMimiqCircuitsBase.DEFAULT_SIMPLIFY_PASSES — Constant
DEFAULT_SIMPLIFY_PASSESThe passes simplify runs by default, in order: remove_identities, cancel_inverses, merge_rotations, resynthesize_2q, resynthesize_1q.
MimiqCircuitsBase.cancel_inverses — Method
cancel_inverses(c::Circuit) -> CircuitDelete pairs of gates that multiply to the identity, such as CX CX, H H or S SDG, also when other gates sit between them as long as the second gate commutes with all of them.
Two gates cancel when one is the inverse of the other on the same qubits, or when the product of their matrices is the identity, which also catches SWAP(1,2) SWAP(2,1). Commutation is checked on the gates' matrices, so for example Z passes a CX control and X passes a CX target; gates with symbolic parameters only pass through each other when both are diagonal.
Multi-controlled X gates (GateCX, GateCCX, GateC3X, Control(n, GateX())) follow structural rules at any width: two of them commute unless the target of one is a control of the other, they commute with diagonal gates that avoid their target and with X rotations that avoid their controls, and two on the same qubits with the same target cancel, whatever the order of the controls. Other gates wider than six qubits are never moved or cancelled.
Non-unitary operations, barriers and classically controlled gates are never moved across, and the search looks back a bounded number of instructions.
Examples
julia> c = push!(Circuit(), GateCX(), 1, 2);
julia> push!(c, GateZ(), 1); # Z commutes with the CX control...
julia> push!(c, GateCX(), 1, 2); # ... so the two CX gates cancel
julia> cancel_inverses(c)
1-qubit circuit with 1 instruction:
└── Z @ q[1]MimiqCircuitsBase.merge_rotations — Method
merge_rotations(c::Circuit) -> CircuitMerge pairs of rotations of the same kind on the same qubits into one, adding their angles: RZ(a) RZ(b) becomes RZ(a + b), and a pair whose sum is the identity is removed. The two rotations may be separated by gates that commute with the second one, as in cancel_inverses.
Merged gates are GateRX, GateRY, GateRZ, GateP, GateU1, GateR, GateRXX, GateRYY, GateRZZ, GateRZX, GateXXplusYY, GateXXminusYY and RPauli, any of them under the same Control (GateCRZ, GateCP, …). Gates with more than one parameter merge only when the others are equal. Symbolic angles are added symbolically.
Single-qubit gates with numeric parameters also merge across kinds, within families of rotations about the same axis that compose without a global phase:
GateZ,GateS,GateSDG,GateT,GateTDG,GateP,GateU1GateX,GateSX,GateSXDGGateY,GateSY,GateSYDG
so T T becomes S and S Z becomes SDG. The result is the named gate when there is one and GateP or GateU otherwise. GateRZ, GateRX and GateRY merge only with themselves, since they differ from the other members of their family by a global phase. Any single-qubit gate with the matrix of one of these, such as a GateU, is treated as that gate.
Examples
julia> @variables θ;
julia> c = push!(Circuit(), GateRZ(θ), 1);
julia> push!(c, GateCZ(), 1, 2); # diagonal gates commute
julia> push!(c, GateRZ(0.5), 1);
julia> merge_rotations(c)
2-qubit circuit with 2 instructions:
├── RZ(0.5 + θ) @ q[1]
└── CZ @ q[1], q[2]
julia> d = push!(Circuit(), GateT(), 1);
julia> push!(d, GateCX(), 1, 2); # T on the control commutes with CX
julia> push!(d, GateT(), 1);
julia> merge_rotations(d) # T T = S
2-qubit circuit with 2 instructions:
├── S @ q[1]
└── CX @ q[1], q[2]MimiqCircuitsBase.remove_identities — Method
remove_identities(c::Circuit) -> CircuitRemove gates whose matrix is the identity, such as GateID, GateRZ(0) or GateU(0, 0, 0).
The test is on the exact matrix, so a gate that is the identity only up to a global phase, like GateRX(2π), is kept. Gates with symbolic parameters, Delay and non-unitary operations are always kept.
Examples
julia> c = push!(Circuit(), GateRZ(0), 1);
julia> push!(c, GateH(), 1);
julia> remove_identities(c)
1-qubit circuit with 1 instruction:
└── H @ q[1]MimiqCircuitsBase.resynthesize_1q — Method
resynthesize_1q(c::Circuit) -> CircuitReplace every run of two or more single-qubit gates on the same qubit by one GateU, or remove it when the run multiplies to the identity.
A run ends at any instruction touching the qubit that is not a single-qubit gate with numeric parameters. Single gates are left as they are.
Examples
julia> c = push!(Circuit(), GateH(), 1);
julia> push!(c, GateS(), 1);
julia> push!(c, GateH(), 1);
julia> length(resynthesize_1q(c))
1MimiqCircuitsBase.resynthesize_2q — Method
resynthesize_2q(c::Circuit) -> CircuitRegroup the gates of c into blocks acting on at most two qubits, as fuse does, and rewrite each two-qubit block with the fewest GateCX it needs, between zero and three, found from its KAK decomposition [2] and the circuits of [3].
A block is rewritten only when that lowers its number of two-qubit gates: a SWAP written as three CX gates stays as it is, since it needs all three, while any longer sequence on the same pair of qubits comes down to three or fewer. The rewritten block uses GateCX, GateU and GateRX, GateRY, GateRZ, GateH; follow with resynthesize_1q to merge the single-qubit gates it leaves behind.
Gates with symbolic parameters and non-unitary operations are left alone and end the blocks around them.
Examples
julia> c = Circuit();
julia> for _ in 1:2 # three CX with rotations in between...
push!(c, GateCX(), 1, 2)
push!(c, GateRX(0.3), 1)
end
julia> push!(c, GateCX(), 1, 2);
julia> count(inst -> numqubits(inst) == 2, resynthesize_2q(c)) # ...need only one
1MimiqCircuitsBase.resynthesize_linear — Method
resynthesize_linear(c::Circuit) -> CircuitRewrite every block made only of GateX, GateCX and GateSWAP gates, of any width, by resynthesizing the map it computes.
Such a block sends the basis state $x$ to $A x \oplus b$ for an invertible Boolean matrix $A$, so it is a classical linear (affine) function of the bits. The block is rebuilt from $A$ with Patel-Markov-Hayes elimination [4], which needs $O(n^2/\log n)$ CX gates for $n$ qubits, followed by one GateX for each set bit of $b$. The rewrite is kept only when it has fewer two-qubit gates than the block, or as many and fewer gates in total. A block that computes the identity is removed.
This finds savings that local passes miss, such as CX ladders that partly undo each other over many qubits. Blocks are collected like fuse does, and end at any other instruction on their qubits.
It is not one of the DEFAULT_SIMPLIFY_PASSES: circuits where CX networks are a small part gain little from it. Pass it to simplify explicitly when they are not.
Examples
julia> c = Circuit();
julia> for _ in 1:2, q in 1:3 # a CX ladder, applied twice...
push!(c, GateCX(), q, q + 1)
end
julia> count(inst -> numqubits(inst) == 2, resynthesize_linear(c)) # ...needs 2 CX
2SymbolicUtils.simplify — Method
simplify(c::Circuit; passes=DEFAULT_SIMPLIFY_PASSES, maxiter=10) -> CircuitSimplify a circuit by running passes in order, repeatedly, until a round no longer lowers the number of two-qubit gates or, failing that, the total number of instructions. The result has the same unitary as c, global phase included, and the same measurements, resets and other non-unitary operations in the same order.
passes is any collection of functions from Circuit to Circuit. The default, DEFAULT_SIMPLIFY_PASSES, is
resynthesize_linear is not among them: it pays off on circuits with large CX networks, such as arithmetic or error-correction encoders, and costs extra rounds for little on others. Add it where it fits, for example passes=(remove_identities, cancel_inverses, merge_rotations, resynthesize_linear, resynthesize_2q, resynthesize_1q).
Gates are rewritten numerically, so single-qubit runs come out as GateU and named gates such as GateH or GateS may be replaced by it. Pass a subset to keep them, for example passes=(remove_identities, cancel_inverses, merge_rotations). Gates with symbolic parameters are only touched by the first three passes.
Examples
julia> c = push!(Circuit(), GateCX(), 1, 2);
julia> push!(c, GateRZ(0.2), 1); # RZ commutes with the CX control...
julia> push!(c, GateCX(), 1, 2); # ...so the two CX gates cancel
julia> push!(c, GateRZ(0.3), 1); # and the rotations merge
julia> simplify(c)
1-qubit circuit with 1 instruction:
└── RZ(0.5) @ q[1]