Tags: algorithm error-correction
In the previous article, we implemented the 3-qubit repetition codes and Shor’s 9-qubit code. Here we move to the Steane [[7,1,3]] code, a cleaner and more structured code.
The Steane code is a CSS code built from the classical Hamming [7,4,3] code. It protects one logical qubit with seven physical qubits and corrects any single-qubit Pauli error: , , or .
We will implement three things:
Build Steane stabilizers from the Hamming code structure.
Measure six stabilizers and correct a single error from the syndrome.
Verify that seven physical Hadamard gates implement the logical Hadamard .
# Install the latest Qamomile from pip.
# !pip install qamomile
# # or
# !uv add qamomileimport qamomile.circuit as qmc
from qamomile.qiskit import QiskitTranspiler
transpiler = QiskitTranspiler()
# Create a seeded backend for reproducible documentation output
from qiskit_aer import AerSimulator
_seeded_backend = AerSimulator(seed_simulator=42, max_parallel_threads=1)
_seeded_executor = transpiler.executor(backend=_seeded_backend)
def _bits7(outcome) -> list[int]:
"""Return seven measured bits in qubit-index order."""
if isinstance(outcome, (list, tuple)):
return list(outcome)
return [(outcome >> i) & 1 for i in range(7)]
def _is_steane_zero_word(outcome) -> bool:
"""Return True when the outcome is an even Hamming codeword."""
bits = _bits7(outcome)
h_checks = [
bits[3] ^ bits[4] ^ bits[5] ^ bits[6],
bits[1] ^ bits[2] ^ bits[5] ^ bits[6],
bits[0] ^ bits[2] ^ bits[4] ^ bits[6],
]
return all(check == 0 for check in h_checks) and sum(bits) % 2 == 01. From Hamming Codes to CSS Codes¶
Use the following parity-check matrix for the classical Hamming [7,4,3] code:
Column is the binary representation of . Therefore, a 3-bit syndrome identifies the error location directly.
A CSS code creates two kinds of stabilizers from this matrix:
-type stabilizers detect errors.
-type stabilizers detect errors.
The Steane code has six generators:
| Type | Stabilizer | Detects |
|---|---|---|
| type | ||
| type | ||
| type | ||
| type | ||
| type | ||
| type |
2. Encoding ¶
The Steane logical is the superposition of the eight even-weight Hamming codewords:
The following circuit prepares from by building the three -type stabilizer patterns.
@qmc.qkernel
def encode_steane_zero(data: qmc.Vector[qmc.Qubit]) -> qmc.Vector[qmc.Qubit]:
data[3] = qmc.h(data[3])
data[3], data[4] = qmc.cx(data[3], data[4])
data[3], data[5] = qmc.cx(data[3], data[5])
data[3], data[6] = qmc.cx(data[3], data[6])
data[1] = qmc.h(data[1])
data[1], data[2] = qmc.cx(data[1], data[2])
data[1], data[5] = qmc.cx(data[1], data[5])
data[1], data[6] = qmc.cx(data[1], data[6])
data[0] = qmc.h(data[0])
data[0], data[2] = qmc.cx(data[0], data[2])
data[0], data[4] = qmc.cx(data[0], data[4])
data[0], data[6] = qmc.cx(data[0], data[6])
return data@qmc.qkernel
def encode_zero_and_measure() -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(7, name="data")
data = encode_steane_zero(data)
return qmc.measure(data)Measure the encoder output and check that all observed bitstrings are even Hamming codewords.
print("Encode and measure |0_L>")
exe = transpiler.transpile(encode_zero_and_measure)
result = exe.sample(_seeded_executor, shots=1024).result()
valid = sum(count for outcome, count in result.results if _is_steane_zero_word(outcome))
total = sum(count for _, count in result.results)
print(f" even Hamming codeword ratio: {valid / total:.3f}")
print(f" observed codewords: {len(result.results)}")Encode and measure |0_L>
even Hamming codeword ratio: 1.000
observed codewords: 8
3. Syndrome Measurement and Correction¶
The Steane code decodes and errors independently.
Measuring the -type stabilizers gives the syndrome for the component.
Measuring the -type stabilizers gives the syndrome for the component.
The syndrome table is the Hamming matrix column table:
| Error location | Syndrome |
|---|---|
| none | |
In the implementation, pass error_type as 1=X, 2=Y, or 3=Z. The error_pos parameter is the physical qubit index 0..6.
@qmc.qkernel
def steane_run(
error_type: qmc.UInt,
error_pos: qmc.UInt,
) -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(7, name="data")
anc = qmc.qubit_array(6, name="anc")
data = encode_steane_zero(data)
for i in qmc.range(7):
if (error_type == 1) & (error_pos == i): # 1 means X error.
data[i] = qmc.x(data[i])
if (error_type == 2) & (error_pos == i): # 2 means Y error.
data[i] = qmc.y(data[i])
if (error_type == 3) & (error_pos == i): # 3 means Z error.
data[i] = qmc.z(data[i])
# Z-type stabilizers: detect the X component.
data[3], anc[0] = qmc.cx(data[3], anc[0])
data[4], anc[0] = qmc.cx(data[4], anc[0])
data[5], anc[0] = qmc.cx(data[5], anc[0])
data[6], anc[0] = qmc.cx(data[6], anc[0])
sx_2 = qmc.measure(anc[0])
data[1], anc[1] = qmc.cx(data[1], anc[1])
data[2], anc[1] = qmc.cx(data[2], anc[1])
data[5], anc[1] = qmc.cx(data[5], anc[1])
data[6], anc[1] = qmc.cx(data[6], anc[1])
sx_1 = qmc.measure(anc[1])
data[0], anc[2] = qmc.cx(data[0], anc[2])
data[2], anc[2] = qmc.cx(data[2], anc[2])
data[4], anc[2] = qmc.cx(data[4], anc[2])
data[6], anc[2] = qmc.cx(data[6], anc[2])
sx_0 = qmc.measure(anc[2])
# X-type stabilizers: detect the Z component.
anc[3] = qmc.h(anc[3])
anc[3], data[3] = qmc.cx(anc[3], data[3])
anc[3], data[4] = qmc.cx(anc[3], data[4])
anc[3], data[5] = qmc.cx(anc[3], data[5])
anc[3], data[6] = qmc.cx(anc[3], data[6])
anc[3] = qmc.h(anc[3])
sz_2 = qmc.measure(anc[3])
anc[4] = qmc.h(anc[4])
anc[4], data[1] = qmc.cx(anc[4], data[1])
anc[4], data[2] = qmc.cx(anc[4], data[2])
anc[4], data[5] = qmc.cx(anc[4], data[5])
anc[4], data[6] = qmc.cx(anc[4], data[6])
anc[4] = qmc.h(anc[4])
sz_1 = qmc.measure(anc[4])
anc[5] = qmc.h(anc[5])
anc[5], data[0] = qmc.cx(anc[5], data[0])
anc[5], data[2] = qmc.cx(anc[5], data[2])
anc[5], data[4] = qmc.cx(anc[5], data[4])
anc[5], data[6] = qmc.cx(anc[5], data[6])
anc[5] = qmc.h(anc[5])
sz_0 = qmc.measure(anc[5])
if (~sx_2) & (~sx_1) & sx_0:
data[0] = qmc.x(data[0])
if (~sx_2) & sx_1 & (~sx_0):
data[1] = qmc.x(data[1])
if (~sx_2) & sx_1 & sx_0:
data[2] = qmc.x(data[2])
if sx_2 & (~sx_1) & (~sx_0):
data[3] = qmc.x(data[3])
if sx_2 & (~sx_1) & sx_0:
data[4] = qmc.x(data[4])
if sx_2 & sx_1 & (~sx_0):
data[5] = qmc.x(data[5])
if sx_2 & sx_1 & sx_0:
data[6] = qmc.x(data[6])
if (~sz_2) & (~sz_1) & sz_0:
data[0] = qmc.z(data[0])
if (~sz_2) & sz_1 & (~sz_0):
data[1] = qmc.z(data[1])
if (~sz_2) & sz_1 & sz_0:
data[2] = qmc.z(data[2])
if sz_2 & (~sz_1) & (~sz_0):
data[3] = qmc.z(data[3])
if sz_2 & (~sz_1) & sz_0:
data[4] = qmc.z(data[4])
if sz_2 & sz_1 & (~sz_0):
data[5] = qmc.z(data[5])
if sz_2 & sz_1 & sz_0:
data[6] = qmc.z(data[6])
return qmc.measure(data)Run all 21 single errors: , , and on each of the seven physical qubits. After correction, every measured bitstring should still be a codeword.
print("Steane code: correct X/Y/Z on all 7 locations")
print(f" {'err':4s} | {'pos':5s} | |0_L> codeword")
print(f" {'-' * 4}-+-{'-' * 5}-+-{'-' * 14}")
for name, error_type in [("X", 1), ("Y", 2), ("Z", 3)]:
for pos in range(7):
exe = transpiler.transpile(
steane_run,
bindings={"error_type": error_type, "error_pos": pos},
)
result = exe.sample(_seeded_executor, shots=128).result()
valid = sum(
count for outcome, count in result.results if _is_steane_zero_word(outcome)
)
total = sum(count for _, count in result.results)
print(f" {name:4s} | q[{pos}] | {valid / total:.3f}")Steane code: correct X/Y/Z on all 7 locations
err | pos | |0_L> codeword
-----+-------+---------------
X | q[0] | 1.000
X | q[1] | 1.000
X | q[2] | 1.000
X | q[3] | 1.000
X | q[4] | 1.000
X | q[5] | 1.000
X | q[6] | 1.000
Y | q[0] | 1.000
Y | q[1] | 1.000
Y | q[2] | 1.000
Y | q[3] | 1.000
Y | q[4] | 1.000
Y | q[5] | 1.000
Y | q[6] | 1.000
Z | q[0] | 1.000
Z | q[1] | 1.000
Z | q[2] | 1.000
Z | q[3] | 1.000
Z | q[4] | 1.000
Z | q[5] | 1.000
Z | q[6] | 1.000
A ratio of 1.000 means the state returns to the code space for every single-qubit Pauli error. A error triggers both the -component and -component corrections.
4. Transversal Hadamard¶
A key feature of the Steane code is that the logical Hadamard is implemented by applying physical Hadamard gates to all seven qubits:
This works because the -type and -type stabilizers have the same Hamming pattern. Transversal gates are important in fault-tolerant computation because one physical gate failure does not spread across many qubits.
@qmc.qkernel
def transversal_hadamard_to_plus_l() -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(7, name="data")
data = encode_steane_zero(data)
for i in qmc.range(7):
data[i] = qmc.h(data[i])
for i in qmc.range(7):
data[i] = qmc.h(data[i])
return qmc.measure(data)
@qmc.qkernel
def transversal_hadamard_round_trip() -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(7, name="data")
data = encode_steane_zero(data)
for i in qmc.range(7):
data[i] = qmc.h(data[i])
for i in qmc.range(7):
data[i] = qmc.h(data[i])
return qmc.measure(data)
def _logical_x_parity(outcome) -> int:
"""Return the measured parity for logical X = X0 X1 X2."""
bits = _bits7(outcome)
return (bits[0] + bits[1] + bits[2]) % 2First check that . The state is a +1 eigenstate of logical , so measuring in the basis should give .
print("Transversal H: |0_L> -> |+_L>")
exe_plus = transpiler.transpile(transversal_hadamard_to_plus_l)
result_plus = exe_plus.sample(_seeded_executor, shots=1024).result()
parity_zero = sum(
count for outcome, count in result_plus.results if _logical_x_parity(outcome) == 0
)
total_plus = sum(count for _, count in result_plus.results)
print(f" q[0] xor q[1] xor q[2] = 0 ratio: {parity_zero / total_plus:.3f}")Transversal H: |0_L> -> |+_L>
q[0] xor q[1] xor q[2] = 0 ratio: 1.000
Next check the round trip. Applying transversal twice should return to because .
print("Transversal H round trip: |0_L> -> H -> H -> |0_L>")
exe_round_trip = transpiler.transpile(transversal_hadamard_round_trip)
result_round_trip = exe_round_trip.sample(_seeded_executor, shots=1024).result()
valid = sum(
count
for outcome, count in result_round_trip.results
if _is_steane_zero_word(outcome)
)
total = sum(count for _, count in result_round_trip.results)
print(f" |0_L> codeword ratio: {valid / total:.3f}")Transversal H round trip: |0_L> -> H -> H -> |0_L>
|0_L> codeword ratio: 1.000
5. Summary¶
In this article, we implemented the Steane [[7,1,3]] code.
Built three -type and three -type stabilizers from the Hamming [7,4,3] code.
Used -type stabilizers to detect the component and -type stabilizers to detect the component.
Verified correction for all 21 single Pauli errors: locations.
Verified that seven physical Hadamard gates act as the logical Hadamard .
Compared with Shor’s code, the Steane code uses fewer physical qubits for the same distance and gives a clean example of CSS structure and transversal Clifford gates.
Next¶
Quantum Error Correction (1) — 3-qubit bit-flip / phase-flip / Shor codes
Surface codes — local stabilizers on a 2D lattice and repeated syndrome measurement