Exact Circuit Synthesis¶
QECC provides an SMT/SAT-based exact synthesis engine for finding provably optimal Clifford circuits. It supports both CSS and non-CSS stabilizer codes and can optimize for gate count or circuit depth.
The entry point is synthesize_isometry_exact from mqt.qecc.circuit_synthesis.exact.
1from mqt.qecc.circuit_synthesis.exact import (
2 synthesize_isometry_exact,
3 Objective,
4 SynthesisStatus,
5 TargetKind,
6)
CSS Encoding Circuits¶
For CSS codes, encoding circuits consist only of CNOT gates (plus ancilla initialization). The target is specified as a CheckMatrix representing the stabilizer generators, and the logicals are provided as a CheckMatrix of logical operators.
Let us synthesize an encoder for the \([[4,2,2]]\) iceberg code — a small CSS code that encodes two logical qubits.
1from mqt.qecc.codes import construct_iceberg_code
2from mqt.qecc.codes.pauli import CheckMatrix
3
4code = construct_iceberg_code(2) # [[4,2,2]] iceberg code
5print(f"[[{code.n},{code.k},{code.distance}]] iceberg code")
6print(f"Hx =\n{code.Hx}")
7print(f"Lx =\n{code.Lx}")
[[4,2,2]] iceberg code
Hx =
[[1 1 1 1]]
Lx =
[[1 1 0 0]
[1 0 1 0]]
To synthesize a gate-count-optimal encoder, pass the X-check matrix as target and the logical X operators:
1hx = CheckMatrix(code.Hx, pauli_type="X")
2lx = CheckMatrix(code.Lx, pauli_type="X")
3
4result = synthesize_isometry_exact(
5 target=hx,
6 target_kind=TargetKind.CSS_ISOMETRY,
7 objective=Objective.GATE_COUNT,
8 x_logicals=lx,
9 lower_bound=0,
10 upper_bound=6,
11 timeout=60,
12)
13
14print(f"Status: {result.status.value}")
15print(f"Gate count: {result.gate_count}")
16print(f"Depth: {result.depth}")
17print(f"Proven optimal: {result.proven_optimal}")
18print(f"Verified: {result.verified}")
Status: success
Gate count: 4
Depth: 2
Proven optimal: True
Verified: True
The synthesized circuit is a CNOTCircuit and can be drawn or serialized:
1result.circuit.draw("mpl")
To optimize for depth instead, use Objective.DEPTH:
1result_depth = synthesize_isometry_exact(
2 target=hx,
3 target_kind=TargetKind.CSS_ISOMETRY,
4 objective=Objective.DEPTH,
5 x_logicals=lx,
6 lower_bound=0,
7 upper_bound=6,
8 timeout=60,
9)
10
11print(f"Status: {result_depth.status.value}")
12print(f"Gate count: {result_depth.gate_count}")
13print(f"Depth: {result_depth.depth}")
14print(f"Proven optimal: {result_depth.proven_optimal}")
Status: success
Gate count: 4
Depth: 2
Proven optimal: True
Non-CSS Encoding Circuits¶
For non-CSS stabilizer codes, encoding circuits are built from the full Clifford gate set. The target is specified as a StabilizerTableau, and the logicals are provided as StabilizerTableau objects.
The gate set for non-CSS synthesis defaults to \(\{H, S, \text{CX}\}\) (standard Clifford). The extended gate set \(\{H, S, \sqrt{X}, \text{CX}, \text{CZ}\}\) can find shorter circuits:
1from mqt.qecc.codes.pauli import StabilizerTableau
2from mqt.qecc.circuit_synthesis.exact import get_clifford_extended_gate_set
3
4stabs = ["XZZXI", "IXZZX", "XIXZZ", "ZXIXZ"]
5x_log = ["XXXXX"]
6z_log = ["ZZZZZ"]
7
8stab_tab = StabilizerTableau.from_pauli_strings(stabs)
9x_log_tab = StabilizerTableau.from_pauli_strings(x_log)
10z_log_tab = StabilizerTableau.from_pauli_strings(z_log)
11
12result = synthesize_isometry_exact(
13 target=stab_tab,
14 target_kind=TargetKind.CLIFFORD_ISOMETRY,
15 objective=Objective.DEPTH,
16 x_logicals=x_log_tab,
17 z_logicals=z_log_tab,
18 gate_set=get_clifford_extended_gate_set(),
19 lower_bound=3,
20 upper_bound=7,
21 use_symmetry_breaking=True,
22 timeout=120,
23)
24
25print(f"Status: {result.status.value}")
26print(f"Two-qubit depth: {result.depth}")
27print(f"Total gates: {result.gate_count}")
28print(f"Proven optimal: {result.proven_optimal}")
29print(f"Verified: {result.verified}")
Status: success
Two-qubit depth: 5
Total gates: 15
Proven optimal: True
Verified: True
The synthesized circuit is a CliffordIsometry:
1result.circuit.draw("mpl")
CSS State Synthesis¶
Exact synthesis also prepares CSS stabilizer states directly. For a state target only the check matrix is needed (no logicals).
The GHZ state \(|GHZ\rangle \propto |000\rangle + |111\rangle\) is the cat (\(|+\rangle_L\)) state of the three-qubit repetition code: it is stabilized by the code’s \(Z\)-checks \(\{ZZI, IZZ\}\) together with \(X_L = XXX\). We therefore pass those \(Z\)-checks as a CSS state target and find the shortest-depth preparation circuit:
1import numpy as np
2
3# Z-checks of the 3-qubit repetition code (ZZI and IZZ)
4ghz_checks = CheckMatrix(np.array([[1, 1, 0], [0, 1, 1]], dtype=np.int8), pauli_type="Z")
5
6result = synthesize_isometry_exact(
7 target=ghz_checks,
8 target_kind=TargetKind.CSS_STATE,
9 objective=Objective.DEPTH,
10 lower_bound=0,
11 upper_bound=5,
12 timeout=30,
13)
14
15print(f"Status: {result.status.value}")
16print(f"Depth: {result.depth}")
17print(f"Gate count: {result.gate_count}")
18print(f"Proven optimal: {result.proven_optimal}")
19result.circuit.draw("mpl")
Status: success
Depth: 2
Gate count: 2
Proven optimal: True
Gate Sets¶
The synthesis engine supports several Clifford gate sets for non-CSS targets. CSS synthesis always uses CNOT-only circuits and ignores the gate_set parameter.
Factory function |
Gates |
Notes |
|---|---|---|
|
\(H, S, \text{CX}\) |
Default for non-CSS synthesis |
|
\(H, \sqrt{X}, \text{CX}\) |
Replaces \(S\) with \(\sqrt{X} = HSH\) |
|
\(H, S, \text{CX}, \text{CZ}\) |
Adds \(\text{CZ}\) to the standard set |
|
\(H, S, \sqrt{X}, \text{CX}, \text{CZ}\) |
Full extended set |
A larger gate set gives the solver more freedom and can yield shorter circuits, at the cost of a larger search space per depth/count bound.
1from mqt.qecc.circuit_synthesis.exact import (
2 get_standard_clifford_gate_set,
3 get_clifford_cz_gate_set,
4 get_clifford_extended_gate_set,
5)
6
7print("Standard gate set:", list(get_standard_clifford_gate_set().keys()))
8print("CZ gate set: ", list(get_clifford_cz_gate_set().keys()))
9print("Extended gate set:", list(get_clifford_extended_gate_set().keys()))
Standard gate set: ['H', 'S', 'CX', 'ID']
CZ gate set: ['H', 'S', 'CX', 'CZ', 'ID']
Extended gate set: ['H', 'S', 'SX', 'CX', 'CZ', 'ID']
Target Kinds¶
The TargetKind enum selects the synthesis problem:
Value |
Target type |
Required arguments |
|---|---|---|
|
CSS encoding isometry |
|
|
CSS stabilizer state |
|
|
Full Clifford encoding isometry |
|
|
Full \(n\)-qubit Clifford unitary |
|
|
Stabilizer state (any) |
|
Search Configuration¶
Bounds and timeouts¶
synthesize_isometry_exact performs an exhaustive search from lower_bound to upper_bound (inclusive). The first feasible bound returns a solution. If all bounds are infeasible the result has status UNSAT. A per-bound solver timeout can be set in seconds:
result = synthesize_isometry_exact(
...,
lower_bound=0,
upper_bound=20,
timeout=60, # give each bound up to 60 seconds
)
If the solver times out at any bound, the search returns immediately with status TIMEOUT.
Symmetry breaking¶
Symmetry-breaking constraints prune the SAT search space by forbidding obviously redundant gate sequences (adjacent identical self-inverse gates, unnecessary idle slots). Enable it with use_symmetry_breaking=True:
result = synthesize_isometry_exact(
...,
use_symmetry_breaking=True,
)
Symmetry breaking is most effective for larger problems where the raw SAT instance is expensive. It is safe to combine with any gate set.
Exponential-backoff search¶
For large instances where a single per-bound timeout is too aggressive, the exponential-backoff strategy can find good solutions faster:
result = synthesize_isometry_exact(
...,
use_exponential_backoff=True,
min_timeout=1, # start with 1 second per bound
timeout=3600, # maximum per-bound budget
)
The strategy works in two phases:
Ascending phase — scan from
lower_boundtoupper_boundwithmin_timeoutper bound. Bounds proven UNSAT are dropped permanently. Timed-out bounds are retried with a doubled budget after each pass, up totimeout.Descending phase — once a SAT solution is found at bound \(b\), descend from \(b{-}1\) with the maximum budget to tighten the result.
result.proven_optimal is True only when all smaller bounds were confirmed UNSAT.
Interpreting SynthesisResult¶
The SynthesisResult object returned by synthesize_isometry_exact carries all relevant metadata:
Attribute |
Type |
Description |
|---|---|---|
|
|
|
|
|
Synthesized circuit, or |
|
|
Total non-identity non-Pauli gate count |
|
|
Two-qubit-gate depth |
|
|
|
|
|
|
|
|
Human-readable status message |
1result = synthesize_isometry_exact(
2 target=hx,
3 target_kind=TargetKind.CSS_ISOMETRY,
4 objective=Objective.GATE_COUNT,
5 x_logicals=lx,
6 lower_bound=0,
7 upper_bound=6,
8 timeout=60,
9)
10
11print(f"status: {result.status}")
12print(f"gate_count: {result.gate_count}")
13print(f"depth: {result.depth}")
14print(f"proven_optimal: {result.proven_optimal}")
15print(f"verified: {result.verified}")
16print(f"message: {result.message}")
status: SynthesisStatus.SUCCESS
gate_count: 4
depth: 4
proven_optimal: True
verified: True
message: Found solution with 4 gates
Secondary Two-Qubit Gate Minimization¶
Depth-optimal synthesis may leave room to reduce the two-qubit gate count while keeping the depth fixed. The max_two_qubit_gates parameter bounds the number of two-qubit gates at a fixed depth, enabling a descent that finds the depth-optimal circuit with fewest two-qubit gates:
1# Step 1: start from the depth-optimal circuit (depth 5 was established above)
2depth_result = synthesize_isometry_exact(
3 target=stab_tab,
4 target_kind=TargetKind.CLIFFORD_ISOMETRY,
5 objective=Objective.DEPTH,
6 x_logicals=x_log_tab,
7 z_logicals=z_log_tab,
8 gate_set=get_clifford_extended_gate_set(),
9 lower_bound=5,
10 upper_bound=5,
11 use_symmetry_breaking=True,
12 timeout=30,
13)
14
15d_star = depth_result.depth
16tq_count = depth_result.circuit.num_two_qubit_gates()
17best_result = depth_result
18
19print(f"Depth-optimal circuit: depth={d_star}, TQ gates={tq_count}")
20
21# Step 2: descend on two-qubit gate count at fixed depth d_star
22tq_proven_optimal = False
23for max_tq in range(tq_count - 1, -1, -1):
24 tq_result = synthesize_isometry_exact(
25 target=stab_tab,
26 target_kind=TargetKind.CLIFFORD_ISOMETRY,
27 objective=Objective.DEPTH,
28 x_logicals=x_log_tab,
29 z_logicals=z_log_tab,
30 gate_set=get_clifford_extended_gate_set(),
31 lower_bound=d_star,
32 upper_bound=d_star,
33 max_two_qubit_gates=max_tq,
34 timeout=6,
35 )
36 if tq_result.status == SynthesisStatus.SUCCESS:
37 best_result = tq_result
38 tq_count = max_tq
39 elif tq_result.status == SynthesisStatus.UNSAT:
40 tq_proven_optimal = True
41 break
42 else:
43 break # timeout: keep current best
44
45print(f"\nMinimized circuit: depth={d_star}, TQ gates={tq_count}")
46print(f"TQ count proven optimal: {tq_proven_optimal}")
Depth-optimal circuit: depth=5, TQ gates=8
Minimized circuit: depth=5, TQ gates=7
TQ count proven optimal: False
Storing and Reloading Circuits¶
Synthesized circuits can be serialized to Stim circuit strings for storage (e.g., in JSONL files or databases) and reloaded later:
1from mqt.qecc.circuit_synthesis.circuits import CliffordIsometry, CNOTCircuit
2import stim
3
4# Serialize
5circuit_str = str(best_result.circuit.to_stim_circuit())
6print("Serialized circuit:")
7print(circuit_str)
Serialized circuit:
R 0 1 2 3
X 3 4
H 1
SQRT_X_DAG 2
CX 4 3
S_DAG 0
SQRT_X_DAG 3
CX 2 4
S_DAG 2
CX 1 4 3 0
SQRT_X_DAG 0 3 4
CZ 1 2
SQRT_X_DAG 4
CX 0 1 3 2
1# Reload a CliffordIsometry
2stim_circ = stim.Circuit(circuit_str)
3reloaded = CliffordIsometry.from_stim_circuit(stim_circ)
4
5print(f"Reloaded: depth={reloaded.depth()}, TQ gates={reloaded.num_two_qubit_gates()}")
6reloaded.draw("mpl")
Reloaded: depth=5, TQ gates=7
CSS circuits (returned as CNOTCircuit) use the same interface:
1cnot_str = str(result.circuit.to_stim_circuit())
2reloaded_cnot = CNOTCircuit.from_stim_circuit(stim.Circuit(cnot_str))
3print(f"Reloaded CSS circuit: depth={reloaded_cnot.depth()}, CNOTs={reloaded_cnot.num_cnots()}")
Reloaded CSS circuit: depth=4, CNOTs=4