Encoding Circuit Synthesis

Encoder Circuit Synthesis for CSS Codes

QECC provides functionality for synthesizing encoding circuits of arbitrary Stabilizer codes. An encoder for an \([[n,k,d]]\) code is an isometry that encodes \(k\) logical qubits into \(n\) physical qubits.

Let’s consider the synthesis of the encoding circuit of the \([[7,1,3]]\) Steane code.

 1from mqt.qecc import CSSCode
 2from mqt.qecc.circuit_synthesis import (
 3    depth_optimal_encoding_circuit,
 4    gate_optimal_encoding_circuit
 5)
 6
 7def print_code_operators(code, label="Code"):
 8    """Helper function to print stabilizers and logical operators of a code."""
 9    print(f"{label} Stabilizers:")
10    for stab in code.stabs_as_pauli_strings():
11        print(f"  {stab}")
12    print(f"\n{label} X Logicals:")
13    for x_log in code.x_logicals_as_pauli_strings():
14        print(f"  {x_log}")
15    print(f"{label} Z Logicals:")
16    for z_log in code.z_logicals_as_pauli_strings():
17        print(f"  {z_log}")
18
19steane_code = CSSCode.from_code_name("steane")
20
21print_code_operators(steane_code, "Steane Code")
Steane Code Stabilizers:
  XXIIXXI
  XIXIXIX
  IIIXXXX
  ZZIIZZI
  ZIZIZIZ
  IIIZZZZ

Steane Code X Logicals:
  XXXIIII
Steane Code Z Logicals:
  ZZZIIII

There is not a unique encoding circuit but usually we would like to obtain an encoding circuit that is optimal with respect to some metric. QECC has functionality for synthesizing gate- or depth-optimal encoding circuits.

Under the hood, this uses the SMT solver z3. Of course this method scales only up to a few qubits. Synthesizing depth-optimal circuits is usually faster than synthesizing gate-optimal circuits.

1depth_opt = depth_optimal_encoding_circuit(steane_code, max_timeout=2)
2q_enc = depth_opt.inputs()
3
4print(f"Encoding qubits are qubits {q_enc}.")
5print(f"Circuit has depth {depth_opt.depth()}.")
6print(f"Circuit has {depth_opt.num_cnots()} CNOTs.")
7
8depth_opt.draw('mpl')
Encoding qubits are qubits [2].
Circuit has depth 3.
Circuit has 9 CNOTs.
_images/ff36c6d25005e7fa01a39fab5c09469c4b1c505026d630363e269819a1aaca7c.svg
1gate_opt = gate_optimal_encoding_circuit(steane_code, max_timeout=2)
2q_enc = gate_opt.inputs()
3
4print(f"Encoding qubits are qubits {q_enc}.")
5print(f"Circuit has depth {gate_opt.depth()}.")
6print(f"Circuit has {gate_opt.num_cnots()} CNOTs.")
7
8gate_opt.draw('mpl')
Encoding qubits are qubits [0].
Circuit has depth 6.
Circuit has 9 CNOTs.
_images/66343cb1bea1af4bf45b3734fb470ace1259760e2249bbca6124e6983dc673e8.svg

QECC obtains optimal solutions for circuits by iteratively trying out different parameters to close in on the optimum. Each run will only be run until the number of seconds specified by max_timeout. If a solution is found in this time it is returned. Otherwise, None will be returned.

In addition to the circuit, the synthesis methods also return the encoding qubits. All other qubits are assumed to be initialized in the \(|0\rangle\) or \(|+\rangle\) states.

For larger codes, synthesizing optimal circuits is not feasible. For this case, QECC provides more scalable heuristic synthesis methods that can target the optimization of two-qubit gates or depth.

 1from mqt.qecc.circuit_synthesis import (
 2    synthesize_encoding_circuit,
 3)
 4
 5heuristic_circ = synthesize_encoding_circuit(steane_code)
 6q_enc = heuristic_circ.inputs()
 7
 8print(f"Encoding (logical input) qubits: {q_enc}")
 9print(f"Circuit has depth {heuristic_circ.depth()}.")
10print(f"Circuit has {heuristic_circ.num_cnots()} CNOTs.")
11
12heuristic_circ.draw('mpl')
Encoding (logical input) qubits: [2]
Circuit has depth 7.
Circuit has 9 CNOTs.
_images/925ec49af437e15ec080b2627ea62be444c6a376425f3d68020c2990fd390805.svg

By default the heuristic synthesis tries to optimize for two-qubit gate count. We can also tell the synthesis to optimize for depth.

 1from mqt.qecc.circuit_synthesis import (
 2    SynthesisConfig
 3)
 4
 5config = SynthesisConfig(optimization_criterion="depth")
 6heuristic_circ = synthesize_encoding_circuit(steane_code, config=config)
 7q_enc = heuristic_circ.inputs()
 8
 9print(f"Encoding (logical input) qubits: {q_enc}")
10print(f"Circuit has depth {heuristic_circ.depth()}.")
11print(f"Circuit has {heuristic_circ.num_cnots()} CNOTs.")
12
13heuristic_circ.draw('mpl')
Encoding (logical input) qubits: [1]
Circuit has depth 3.
Circuit has 9 CNOTs.
_images/9cfcec1a188003127397776650d536c5deeb3c4e578a439bda22c8af9d9da05b.svg

The inputs() method returns a list of physical qubit indices representing the encoded logical information. All other qubits are ancillas initialized in the \(|0\rangle\) or \(|+\rangle\) state.

Extracting the Code from an Encoding Circuit

Given an encoding circuit, we can extract the stabilizer code it implements using the get_code() method. This is useful for verifying that a synthesized circuit correctly implements the desired code.

 1encoder = synthesize_encoding_circuit(steane_code)
 2circuit_code = encoder.get_code()
 3
 4print(f"Original code: n={steane_code.n}, k={steane_code.k}")
 5print(f"Circuit code: n={circuit_code.n}, k={circuit_code.k}")
 6print(f"\nCodes are equivalent: {steane_code.is_equivalent(circuit_code)}")
 7
 8print("\n" + "="*60)
 9print_code_operators(steane_code, "Original Steane Code")
10print("\n" + "="*60)
11print_code_operators(circuit_code, "Circuit-Extracted Code")
Original code: n=7, k=1
Circuit code: n=7, k=1

Codes are equivalent: True

============================================================
Original Steane Code Stabilizers:
  XXIIXXI
  XIXIXIX
  IIIXXXX
  ZZIIZZI
  ZIZIZIZ
  IIIZZZZ

Original Steane Code X Logicals:
  XXXIIII
Original Steane Code Z Logicals:
  ZZZIIII

============================================================
Circuit-Extracted Code Stabilizers:
  XIXIXIX
  XXIIXXI
  IIIXXXX
  ZIZIZIZ
  ZIZZIZI
  ZZIZIIZ

Circuit-Extracted Code X Logicals:
  XXXIIII
Circuit-Extracted Code Z Logicals:
  ZZZIIII

The is_equivalent method checks whether two codes have the same stabilizer group and logical basis (up to stabilizer equivalence).

Mapping Logical Qubits to Physical Inputs

For codes with multiple logical qubits (\(k > 1\)), it’s important to understand which physical input qubit corresponds to which logical qubit of the code. The synthesized encoding circuit may permute the logical qubits, so the order of physical inputs returned by inputs() may not directly correspond to the order of logical operators in the code definition.

Let’s demonstrate this with the \([[15, 7, 3]]\) quantum Hamming code, which encodes 7 logical qubits.

 1from mqt.qecc.codes import construct_quantum_hamming_code
 2hamming_code = construct_quantum_hamming_code(4) # [[15,7,3]] quantum Hamming code
 3
 4print(f"Code parameters: n={hamming_code.n}, k={hamming_code.k}, d={hamming_code.distance}")
 5print_code_operators(hamming_code, "Hamming Code")
 6
 7encoder = synthesize_encoding_circuit(hamming_code)
 8physical_inputs = encoder.inputs()
 9
10print(f"\nPhysical input qubits: {physical_inputs}")
11print(f"Number of physical inputs: {len(physical_inputs)}")
Code parameters: n=15, k=7, d=3
Hamming Code Stabilizers:
  IIIIIIIXXXXXXXX
  IIIXXXXIIIIXXXX
  IXXIIXXIIXXIIXX
  XIXIXIXIXIXIXIX
  IIIIIIIZZZZZZZZ
  IIIZZZZIIIIZZZZ
  IZZIIZZIIZZIIZZ
  ZIZIZIZIZIZIZIZ

Hamming Code X Logicals:
  IXXXXIIIIIIIIII
  IIXIXXIIIIIIIII
  IIIXXXXIIIIIIII
  XIXIXIXIIIIIIII
  XXXXXIIXXIIIIII
  XIXXXXXXIXIIIII
  IIIIIIXXXXIXIII
Hamming Code Z Logicals:
  IZIIIIIZIZIIIII
  IZIZIZIIIIIIIII
  IZIZIIIIIZIZIII
  IIZZIZIZZIIIIII
  ZIZZIZIIIIIIIII
  IZZZZIIIIIIIIII
  IIIZZZZIIIIIIII

Physical input qubits: [4, 6, 9, 0, 5, 3, 1]
Number of physical inputs: 7

The logical_to_input_mapping method returns a list where the \(i\)-th element is the physical input qubit corresponding to the \(i\)-th logical qubit of the code.

1mapping = encoder.logical_to_input_mapping(hamming_code)
2
3if mapping is not None:
4    print("Logical to Physical Input Mapping:")
5    for logical_idx, physical_qubit in enumerate(mapping):
6        print(f"  Logical qubit {logical_idx} -> Physical input qubit {physical_qubit}")
7else:
8    print("The encoder does not implement the given code.")
Logical to Physical Input Mapping:
  Logical qubit 0 -> Physical input qubit 1
  Logical qubit 1 -> Physical input qubit 5
  Logical qubit 2 -> Physical input qubit 4
  Logical qubit 3 -> Physical input qubit 0
  Logical qubit 4 -> Physical input qubit 9
  Logical qubit 5 -> Physical input qubit 3
  Logical qubit 6 -> Physical input qubit 6

This mapping tells us that to encode logical qubit \(i\), we should prepare the state on physical qubit mapping[i].

Tweaking Parameters for Heuristic Synthesis

Let’s consider a slightly larger example, the \([[23,1,7]]\) Golay code.

1code = CSSCode.from_code_name("golay")
2
3encoder = synthesize_encoding_circuit(code)
4print(f"Encoding (logical input) qubits: {encoder.inputs()}")
5print(f"Circuit has depth {encoder.depth()}.")
6print(f"Circuit has {encoder.num_cnots()} CNOTs.")
7
8encoder.draw(output='mpl', fold=False, scale=0.5)
Encoding (logical input) qubits: [10]
Circuit has depth 27.
Circuit has 52 CNOTs.
_images/028f4725bc9f8f652bfe9a34233f72b9efcff682fd71e67c34905812bc1f96b3.svg

The way the greedy synthesis works in QECC is by trying to reduce the check matrix (or stabilizer tableau) of the code in question using as few elementary matrix operations (gates) as possible. The synthesis is greedily guided by some metric computed on the check matrix - the number of remaining entries in the check matrix, for example. Since the synthesis algorithm makes local choices, the search might go down branches of the search-tree leading to sub-optimal solution. QECC also provides a costlier search procedure which tries to look ahead which candidates in the search will lead to good results by completing the entire greedy synthesis for these candidates and making a choice based on the resulting circuits (as opposed to using the check matrix as a proxy for estimating).

This search is generally costlier, but can lead to significantly better results. We can tell the synthesis to perform rollout-based synthesis by setting the appropriate flags in the config. rollout is an int parameter that determines for how many layers the search should perform the rollout. If it is set to 0, no rollout is performed (default). If it is set to 1, the num_rollout_candidates parameter determines for how many candidates per gate in the search the rollout is performed. This determines how many complete circuits are synthesized before the single gate is chosen leading to the locally best circuit. If enable_early_termination is set to True, rollout is only performed until no better solutions are found. In that case, the search returns whatever the current best circuit is. If it is set to False, rollout will be performed until the last gate of the search is placed. This will take longer but leads to better results in general:

 1from mqt.qecc.circuit_synthesis import SynthesisConfig
 2
 3config = SynthesisConfig(rollout=1, num_rollout_candidates=5, optimization_criterion="gates", enable_early_termination=False)
 4
 5encoder = synthesize_encoding_circuit(code, config=config)
 6print(f"Encoding (logical input) qubits: {encoder.inputs()}")
 7print(f"Circuit has depth {encoder.depth()}.")
 8print(f"Circuit has {encoder.num_cnots()} CNOTs.")
 9
10encoder.draw(output='mpl', fold=False, scale=0.5)
Encoding (logical input) qubits: [7]
Circuit has depth 17.
Circuit has 51 CNOTs.
_images/dcd93c0e8a478bed4243bb37efd849fd1d32f63cca7e9a879383af663f74c13d.svg

Encoder Circuit Synthesis for non-CSS Stabilizer Codes

QECC also supports encoding circuit synthesis for non-CSS stabilizer codes. For these codes, encoding circuits are built from two-qubit transvections (see arXiv:2503.14660 for details).

Let’s consider the \([[5,1,3]]\) code.

 1from mqt.qecc import StabilizerCode
 2from mqt.qecc.circuit_synthesis import synthesize_encoding_circuit
 3
 4stabs = ["XZZXI", "IXZZX", "XIXZZ", "ZXIXZ"]
 5x_logicals = ["XXXXX"]
 6z_logicals = ["ZZZZZ"]
 7five_qubit_code = StabilizerCode(stabs, x_logicals=x_logicals, z_logicals=z_logicals)
 8
 9print("Stabilizers:")
10for stab in stabs:
11    print(f"  {stab}")
12print("\nX Logical:")
13print(f"  {x_logicals[0]}")
14print("Z Logical:")
15print(f"  {z_logicals[0]}")
Stabilizers:
  XZZXI
  IXZZX
  XIXZZ
  ZXIXZ

X Logical:
  XXXXX
Z Logical:
  ZZZZZ

The same synthesize_encoding_circuit function works for non-CSS codes. This method returns a CliffordIsometry object.

1encoder = synthesize_encoding_circuit(five_qubit_code)
2encoding_qubits = encoder.inputs()
3
4print(f"Encoding qubits (logical to physical mapping): {encoding_qubits}")
5print(f"Circuit has two-qubit depth {encoder.depth()}.")
6print(f"Circuit has {encoder.num_two_qubit_gates()} two-qubit gates.")
7
8encoder.draw(output='mpl', fold=False)
Encoding qubits (logical to physical mapping): [0]
Circuit has two-qubit depth 5.
Circuit has 6 two-qubit gates.
_images/a735e08da97f52b2f1f680268d54d2226e6a9ed2e983791086842e7f9f40e3b7.svg

For displaying the circuit, the transvections are decomposed into CZ and single-qubit Clifford gates.

For non-CSS codes, depth-optimal synthesis is also available:

 1from mqt.qecc.circuit_synthesis import depth_optimal_encoding_circuit_non_css
 2
 3encoder = None
 4for d in range(3, 9):
 5    result = depth_optimal_encoding_circuit_non_css(five_qubit_code, max_depth=d, exact_two_qubit_count=True, max_two_qubit_gates=6)
 6
 7    if result == "UNSAT":
 8        print(f"No solution for max_depth={d}")
 9    else:
10        encoder = result
11        break
12
13if encoder is None:
14    raise RuntimeError("No non-CSS encoder found in the tested depth range.")
15encoding_qubits = encoder.inputs()
16
17print(f"Encoding qubits (logical to physical mapping): {encoding_qubits}")
18print(f"Circuit has two-qubit depth {encoder.depth()}.")
19print(f"Circuit has {encoder.num_two_qubit_gates()} two-qubit gates.")
20
21encoder.draw(output='mpl', fold=False)
No solution for max_depth=3
Encoding qubits (logical to physical mapping): [1]
Circuit has two-qubit depth 4.
Circuit has 6 two-qubit gates.
_images/5c87a74fff084cdb68e22fb8f09aa101137a9fa4fad0cbf5d171da82aa162881.svg

This method uses SMT-based synthesis to find a depth-optimal encoding circuit, similar to the CSS case. The max_depth parameter limits the search depth. If no solution is found, it returns "UNSAT".