Tags: algorithm error-correction
Qubits are easily disturbed by noise. Quantum error correction (QEC) protects information by spreading one logical qubit across multiple physical qubits.
This article is the first part of a QEC introduction. We implement the following three codes with Qamomile’s @qkernel.
The 3-qubit bit-flip code — corrects a single bit-flip () error.
The 3-qubit phase-flip code — corrects a single phase-flip () error.
Shor’s 9-qubit code — corrects any single-qubit error (, , ).
The second part, Stabilizer Formalism and the Steane Code, presents the framework that unifies these codes.
Prerequisites: @qkernel, the CNOT gate, and measurement. If you are new to these, start with Your First Quantum Kernel.
# Install the latest Qamomile from pip.
# !pip install qamomile
# # or
# !uv add qamomile1. Why Quantum Error Correction Is Hard¶
Before getting into actual quantum error correction, we explain what makes the quantum case harder than the classical one.
1.1 The Classical Repetition Code¶
On a classical computer, the simplest way to protect a bit from noise is the repetition code.
encode: 0 → 000 1 → 111
compute: process while still encoded
recover: read all 3 bits at the end, take a majority voteEven if one bit flips along the way (000 → 010, say), the majority vote recovers the original value.
1.2 Encoding Works Fine on Quantum States Too¶
We carry the same idea over to quantum. Suppose we want to spread the logical state
across three qubits. Two CNOT gates produce the following state:
This is not three copies of the state (); it is an entanglement spread across the three qubits. Encoding is built without trouble this way.
1.3 The Real Obstacle Is Correction¶
What breaks down is not encoding but correction.
The classical repetition code reads the three bits during correction and takes a majority vote. Classical bits are not destroyed by reading.
In the quantum case, “reading” is measurement, and measurement destroys superposition. If you measure directly, it collapses to either or , and and are lost.
For the final readout at the end of a computation, “measure everything and take a majority vote” is fine, just as in the classical case. The problem is correcting during a computation — keeping the state intact for the rest of the computation while fixing only the error.
So quantum error correction does not measure the data itself. It measures only where and what kind of error occurred. This information about the location and type of the error is called the syndrome. “Syndrome” is originally a medical term for a set of symptoms; just as a doctor diagnoses a disease from its symptoms without observing it directly, error correction pins down the error from its “symptoms” alone, without measuring the error itself.
The syndrome is extracted through ancilla qubits, so the logical state () is left untouched. This operation is syndrome measurement.
1.4 Another Difference: Phase Errors¶
There is one more difference between the classical and quantum cases.
Classical errors are only bit flips (). Qubits also have phase errors. The error that flips only the sign of ,
does not change any probability when measured in the computational basis, so a majority vote cannot detect it at all.
Quantum errors are also continuous — there are infinitely many intermediate errors, such as a qubit rotating slightly. Correcting each of these individually looks impossible, but in fact it is enough to consider only discrete errors.
The reasoning has two steps. First, any error on a single qubit can be written, as a matrix, as a linear combination of (do nothing), , , . A small rotation, for instance, is a slight mixture of and . Applying such an error to an encoded state turns it into a superposition of “no error”, “ error”, “ error”, and so on.
Second, measuring the syndrome of this state collapses it onto one of the superposed cases. What is left after the measurement is a single discrete error, such as “an error on qubit ”. A continuous error is turned into a discrete Pauli error by syndrome measurement (we will see this collapse in action in the following sections).
So the only errors we need to correct are the full , , . And since , correcting and automatically covers .
1.5 The Quantum Error Correction Flow¶
Quantum error correction proceeds as follows.
encode → error → syndrome measurement → correctionEncode: spread one logical qubit across multiple physical qubits using entanglement.
Syndrome measurement: extract only the location and type of the error into ancilla qubits, without measuring the logical state.
Correction: apply a Pauli gate according to the syndrome to cancel the error.
From the next section on, we implement this flow with the simplest code — the 3-qubit bit-flip code.
Before getting into the implementation, we load Qamomile and the Qiskit integration and define two helper functions. _first_bit_distribution and _sample_first_bit are just utilities that compile and run a kernel and return the 0/1 counts of the first bit. They are not central to QEC, so feel free to skip them.
import math
import os
import qamomile.circuit as qmc
from qamomile.circuit import SampleResult
from qamomile.qiskit import QiskitTranspiler
docs_test_mode = os.environ.get("QAMOMILE_DOCS_TEST") == "1"
default_shots = 64 if docs_test_mode else 256
superposition_shots = 512 if docs_test_mode else 2000
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 _first_bit_distribution(result: SampleResult) -> dict[int, int]:
"""Return the 0/1 counts for the first measured bit."""
counts = {0: 0, 1: 0}
for outcome, count in result.results:
bit = outcome[0] if isinstance(outcome, (list, tuple)) else outcome & 1
counts[bit] += count
return counts
def _sample_first_bit(
kernel,
*,
bindings: dict[str, object] | None = None,
parameters: list[str] | None = None,
runtime_bindings: dict[str, object] | None = None,
shots: int = default_shots,
) -> dict[int, int]:
"""Compile and run a kernel, returning the 0/1 counts of the first bit."""
executable = transpiler.transpile(
kernel,
bindings=bindings or {},
parameters=parameters or [],
)
job = executable.sample(
_seeded_executor,
shots=shots,
bindings=runtime_bindings or {},
)
return _first_bit_distribution(job.result())2. The 3-Qubit Bit-Flip Code¶
The first code we build is the bit-flip code. It is the simplest quantum error correction code, correcting just a single bit-flip () error. We implement the whole flow from 1.5 — encode → error → syndrome measurement → correction — with this code.
2.1 The Target Error: Bit Flip¶
An error flips a qubit, . It is the error corresponding to a classical bit flip. The bit-flip code aims to correct this single error.
2.2 The Code Space¶
We encode the logical state into the 3-qubit state
The logical corresponds to and the logical to . The space spanned by these two is called the code space, and the only valid codewords are and .
2.3 The Encoding Circuit¶
Encoding is just one CNOT each from the data qubit to and .
@qmc.qkernel
def encode_3qubit_bitflip(
q0: qmc.Qubit, q1: qmc.Qubit, q2: qmc.Qubit
) -> tuple[qmc.Qubit, qmc.Qubit, qmc.Qubit]:
q0, q1 = qmc.cx(q0, q1)
q0, q2 = qmc.cx(q0, q2)
return q0, q1, q2If is , the two CNOTs flip and as well, giving . If is , nothing happens and it stays . If is a superposition , the result is .
2.4 Syndrome Measurement: Parity¶
To find the location of the error, we measure two parities (whether two qubits hold the same value).
— whether and are equal
— whether and are equal
In the code space ( and ) all three qubits hold the same value, so both parities report “equal”. When a single error occurs, the parity pattern changes according to its location.
| Error | Syndrome | Correction |
|---|---|---|
| none | none | |
The three errors each show a distinct syndrome, so the syndrome uniquely determines the error location.
To measure , prepare an ancilla qubit, apply CX(data[i], anc) and CX(data[j], anc), then measure anc. Only the ancilla is measured; the data qubits are left untouched.
Let us follow a concrete example. Suppose an error hits the second qubit of the encoded state . Since flips , the state becomes
We add two ancilla qubits initialized to and perform syndrome measurement. CX adds the control qubit’s value into the ancilla by XOR, so
: ancilla 1 receives . For it is ; for it is .
: ancilla 2 receives . For it is ; for it is .
Both terms give the same ancilla values , and the state evolves as follows.
The key point is that the two terms of the superposition give the same ancilla values. Because of this, measuring the ancilla does not destroy the superposition of and . All the measurement yields is the syndrome , which the table identifies as an error on .
2.5 Correction¶
Once the syndrome is known, correction is just applying the same as the detected error, at the same location, once more. Since returns to the identity when applied twice (), the error’s and the correction’s cancel out.
Continuing the previous example: the syndrome indicates an error on , so applying to gives
restoring the encoded state exactly. The “Correction” column of the table above gives the correction for each syndrome.
2.6 Implementation and Run: Logical ¶
We collect everything into a single @qkernel. error_pos specifies where to inject the error, and the kernel performs encoding, error injection, syndrome measurement, and correction.
@qmc.qkernel
def bitflip_syndrome_run(
error_pos: qmc.UInt,
theta: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
# Allocate 3 data qubits and 2 ancilla qubits for syndrome measurement.
data = qmc.qubit_array(3, name="data")
anc = qmc.qubit_array(2, name="anc")
# Prepare the logical state with ry(theta), then encode it into 3 qubits.
data[0] = qmc.ry(data[0], theta)
data[0], data[1], data[2] = encode_3qubit_bitflip(data[0], data[1], data[2])
# Inject an X error at error_pos (error_pos=3 means no error).
for i in qmc.range(3):
if error_pos == i:
data[i] = qmc.x(data[i])
# Syndrome measurement 1: extract the Z0 Z1 parity into anc[0].
data[0], anc[0] = qmc.cx(data[0], anc[0])
data[1], anc[0] = qmc.cx(data[1], anc[0])
s0 = qmc.measure(anc[0])
# Syndrome measurement 2: extract the Z0 Z2 parity into anc[1].
data[0], anc[1] = qmc.cx(data[0], anc[1])
data[2], anc[1] = qmc.cx(data[2], anc[1])
s1 = qmc.measure(anc[1])
# Identify the error location from the syndrome (s0, s1) and correct with X.
if s0 & s1: # (1, 1) -> data[0]
data[0] = qmc.x(data[0])
if s0 & ~s1: # (1, 0) -> data[1]
data[1] = qmc.x(data[1])
if ~s0 & s1: # (0, 1) -> data[2]
data[2] = qmc.x(data[2])
return qmc.measure(data)error_pos is a compile-time parameter. The values 0, 1, 2 inject an error at that location. The value 3 matches no branch, so it means “no error”.
First we prepare the logical (an ry gate with theta ). If correction works, data[0] should always be 1.
bitflip_cases = [
("no error", 3),
("X on data[0]", 0),
("X on data[1]", 1),
("X on data[2]", 2),
]
if docs_test_mode:
bitflip_cases = [
("no error", 3),
("X on data[1]", 1),
]
print("3-qubit bit-flip code: logical |1>")
for label, error_pos in bitflip_cases:
counts = _sample_first_bit(
bitflip_syndrome_run,
bindings={"error_pos": error_pos},
parameters=["theta"],
runtime_bindings={"theta": math.pi},
)
print(f" {label:14s}: data[0]=0 -> {counts[0]:3d}, data[0]=1 -> {counts[1]:3d}")
# Perfect single-X correction on a pure-|1> input is fully deterministic:
# every shot reads data[0] = 1.
assert counts[0] == 0
assert counts[1] == counts[0] + counts[1]3-qubit bit-flip code: logical |1>
no error : data[0]=0 -> 0, data[0]=1 -> 256
X on data[0] : data[0]=0 -> 0, data[0]=1 -> 256
X on data[1] : data[0]=0 -> 0, data[0]=1 -> 256
X on data[2] : data[0]=0 -> 0, data[0]=1 -> 256
2.7 Superposition Input¶
The code also preserves amplitudes. The state prepared with theta has probability
This probability should be preserved regardless of the injected error.
print("3-qubit bit-flip code: superposition input")
for label, error_pos in bitflip_cases:
counts = _sample_first_bit(
bitflip_syndrome_run,
bindings={"error_pos": error_pos},
parameters=["theta"],
runtime_bindings={"theta": math.pi / 3},
shots=superposition_shots,
)
total = counts[0] + counts[1]
print(f" {label:14s}: P(data[0]=1) = {counts[1] / total:.3f}")
assert abs(counts[1] / total - 0.25) < (0.08 if docs_test_mode else 0.05)
assert total == superposition_shots3-qubit bit-flip code: superposition input
no error : P(data[0]=1) = 0.269
X on data[0] : P(data[0]=1) = 0.269
X on data[1] : P(data[0]=1) = 0.269
X on data[2] : P(data[0]=1) = 0.269
2.8 Limitation: Powerless Against Phase Errors¶
The bit-flip code can correct only errors. It is powerless against the phase error .
The reason lies in how the parity is measured. A error only changes the sign of or ; it does not change the bit values. The parity only looks at whether bits are equal, so even when a error occurs the syndrome stays — the error cannot be detected.
Correcting errors requires a different code. In the next section we build the phase-flip code, which uses Hadamard gates to move the bit-flip code into “the world of phases”.
3. The 3-Qubit Phase-Flip Code¶
The bit-flip code could correct only errors. Next we build a code that corrects the phase error . Rather than designing one from scratch, we reuse the bit-flip code “in a different basis”.
3.1 The Target Error: Phase Flip¶
A error flips only the sign of (, ). As seen in 1.4, it is an error invisible when measured in the computational basis. The phase-flip code aims to correct this single error.
3.2 The Key Identity: Swaps and ¶
The Hadamard gate satisfies the following relations.
That is, conjugating by swaps errors and errors. The bit-flip code could correct errors. If we change the basis of each qubit with , that code becomes a code that corrects errors. This is the phase-flip code.
3.3 The Code Space¶
Since and , transforming the bit-flip codewords and with on all three qubits gives the logical states of the phase-flip code.
Just as an error swaps in the bit-flip code, a error swaps in the phase-flip code ().
3.4 The Encoding Circuit¶
Encoding is just the bit-flip encoding followed by an on all three qubits.
@qmc.qkernel
def encode_3qubit_phaseflip(
q0: qmc.Qubit, q1: qmc.Qubit, q2: qmc.Qubit
) -> tuple[qmc.Qubit, qmc.Qubit, qmc.Qubit]:
# After bit-flip encoding, move all 3 qubits to the X basis with H.
q0, q1, q2 = encode_3qubit_bitflip(q0, q1, q2)
q0 = qmc.h(q0)
q1 = qmc.h(q1)
q2 = qmc.h(q2)
return q0, q1, q23.5 Syndrome Measurement: Parity¶
For the bit-flip code we measured the parity . For the phase-flip code, with and swapped, we measure the parity .
To measure , prepare an ancilla in (apply ), use it as the control for CNOTs into the two data qubits, then apply again before measuring. The syndrome-to-error correspondence has the same shape as for the bit-flip code; only the correction changes from to .
| Error | Syndrome | Correction |
|---|---|---|
| none | none | |
3.6 Implementation and Run¶
We collect everything into a single @qkernel. This time we prepare the logical .
@qmc.qkernel
def phaseflip_syndrome_run(error_pos: qmc.UInt) -> qmc.Vector[qmc.Bit]:
# Allocate 3 data qubits and 2 ancilla qubits for syndrome measurement.
data = qmc.qubit_array(3, name="data")
anc = qmc.qubit_array(2, name="anc")
# Encode into the logical |0_L> = |+++>.
data[0], data[1], data[2] = encode_3qubit_phaseflip(data[0], data[1], data[2])
# Inject a Z error at error_pos (error_pos=3 means no error).
for i in qmc.range(3):
if error_pos == i:
data[i] = qmc.z(data[i])
# Syndrome measurement 1: extract the X0 X1 parity into anc[0] (H gives the X basis).
anc[0] = qmc.h(anc[0])
anc[0], data[0] = qmc.cx(anc[0], data[0])
anc[0], data[1] = qmc.cx(anc[0], data[1])
anc[0] = qmc.h(anc[0])
s0 = qmc.measure(anc[0])
# Syndrome measurement 2: extract the X0 X2 parity into anc[1].
anc[1] = qmc.h(anc[1])
anc[1], data[0] = qmc.cx(anc[1], data[0])
anc[1], data[2] = qmc.cx(anc[1], data[2])
anc[1] = qmc.h(anc[1])
s1 = qmc.measure(anc[1])
# Identify the error location from the syndrome (s0, s1) and correct with Z.
if s0 & s1: # (1, 1) -> data[0]
data[0] = qmc.z(data[0])
if s0 & ~s1: # (1, 0) -> data[1]
data[1] = qmc.z(data[1])
if ~s0 & s1: # (0, 1) -> data[2]
data[2] = qmc.z(data[2])
# data[0] is back in |+>, so apply H to turn it into |0> before measuring.
data[0] = qmc.h(data[0])
return qmc.measure(data)After correction, data[0] is back in . Since measured directly gives 0 and 1 half the time, we apply one final to data[0] to turn it into before measuring. If correction works, data[0] should always be 0.
phaseflip_cases = [
("no error", 3),
("Z on data[0]", 0),
("Z on data[1]", 1),
("Z on data[2]", 2),
]
if docs_test_mode:
phaseflip_cases = [
("no error", 3),
("Z on data[1]", 1),
]
print("3-qubit phase-flip code: logical |0_L> = |+++>")
for label, error_pos in phaseflip_cases:
counts = _sample_first_bit(
phaseflip_syndrome_run,
bindings={"error_pos": error_pos},
)
print(f" {label:14s}: data[0]=0 -> {counts[0]:3d}, data[0]=1 -> {counts[1]:3d}")
# After perfect single-Z correction the final H sends data[0] back to
# |0>, so every shot reads 0.
assert counts[1] == 0
assert counts[0] == counts[0] + counts[1]3-qubit phase-flip code: logical |0_L> = |+++>
no error : data[0]=0 -> 256, data[0]=1 -> 0
Z on data[0] : data[0]=0 -> 256, data[0]=1 -> 0
Z on data[1] : data[0]=0 -> 256, data[0]=1 -> 0
Z on data[2] : data[0]=0 -> 256, data[0]=1 -> 0
3.7 Limitation: Only One Error Type¶
The phase-flip code can correct errors, but now it cannot correct errors. Since it is a basis transformation of the bit-flip code, the errors it can correct were simply swapped.
The bit-flip code handles only , the phase-flip code only — each corrects just one error type. But real noise causes both and , and also , where both occur at once. In the next section we combine the two codes into Shor’s 9-qubit code, which corrects any single-qubit error.
4. Shor’s 9-Qubit Code¶
Combining the bit-flip code and the phase-flip code should let us correct both and . Shor’s 9-qubit code is what realizes this.
4.1 The Idea: Nest Two Codes¶
Shor’s code encodes in two stages.
Encode one qubit into three with the phase-flip code.
Encode each of those three qubits into three more with the bit-flip code.
This spreads 1 → 3 → 9 qubits. This construction of “putting one more code inside a code” is called a concatenated code. The outer phase-flip layer handles errors, and the inner bit-flip layer handles errors.
4.2 Viewing 9 Qubits as 3 Blocks¶
We view the 9 qubits as three blocks.
(q0, q1, q2) (q3, q4, q5) (q6, q7, q8)Each block is the inner bit-flip code. The representatives of the three blocks, , form the outer phase-flip code.
4.3 The Encoding Circuit¶
Encoding is just applying the outer phase-flip encoding to , then bit-flip encoding each block.
@qmc.qkernel
def encode_shor(q: qmc.Vector[qmc.Qubit]) -> qmc.Vector[qmc.Qubit]:
# Outer layer: encode q[0], q[3], q[6] with the phase-flip code.
q[0], q[3], q[6] = encode_3qubit_phaseflip(q[0], q[3], q[6])
# Inner layer: encode each of the 3 blocks with the bit-flip code.
q[0], q[1], q[2] = encode_3qubit_bitflip(q[0], q[1], q[2])
q[3], q[4], q[5] = encode_3qubit_bitflip(q[3], q[4], q[5])
q[6], q[7], q[8] = encode_3qubit_bitflip(q[6], q[7], q[8])
return q4.4 Syndrome Measurement¶
Shor’s syndrome has 8 bits, split into two kinds.
Intra-block parities (
anc[0]–anc[5], two per block): these are exactly the bit-flip code’s syndrome measurement, locating an error within each block.Inter-block parities (
anc[6],anc[7]): the two parities and . These correspond to the phase-flip code’s syndrome measurement, identifying the block containing a error.
4.5 Why Errors Are Corrected¶
Shor’s code corrects not only and but also errors. Since , a error contains both an component and a component.
The intra-block parities detect the component and the inter-block parities detect the component, independently. With both the correction and the correction applied, the error is cancelled.
4.6 Implementation and Run¶
We collect everything into a single @qkernel. error_type is 1=X, 2=Y, 3=Z, and error_pos is the index of the qubit to inject the error into.
Even after correction, the logical state is still encoded across nine qubits. For this demonstration we apply the inverse encoding circuit at the end so that the logical bit can be read directly from q[0]. This inverse encoding is a step for checking the result; the correction itself is already complete with syndrome measurement and feedback.
@qmc.qkernel
def shor_syndrome_run(
error_type: qmc.UInt,
error_pos: qmc.UInt,
theta: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
# Allocate 9 data qubits and 8 ancilla qubits for syndrome measurement.
q = qmc.qubit_array(9, name="q")
anc = qmc.qubit_array(8, name="anc")
# Prepare the logical state with ry(theta), then encode it into 9 qubits.
q[0] = qmc.ry(q[0], theta)
q = encode_shor(q)
# Inject the X / Y / Z error specified by error_type / error_pos.
for i in qmc.range(9):
if (error_type == 1) & (error_pos == i): # 1: X error
q[i] = qmc.x(q[i])
if (error_type == 2) & (error_pos == i): # 2: Y error
q[i] = qmc.y(q[i])
if (error_type == 3) & (error_pos == i): # 3: Z error
q[i] = qmc.z(q[i])
# Intra-block Z parity: for block b, anc[2b]=Z(3b,3b+1), anc[2b+1]=Z(3b,3b+2).
for b in qmc.range(3):
q[3 * b], anc[2 * b] = qmc.cx(q[3 * b], anc[2 * b])
q[3 * b + 1], anc[2 * b] = qmc.cx(q[3 * b + 1], anc[2 * b])
q[3 * b], anc[2 * b + 1] = qmc.cx(q[3 * b], anc[2 * b + 1])
q[3 * b + 2], anc[2 * b + 1] = qmc.cx(q[3 * b + 2], anc[2 * b + 1])
# Inter-block X parity: anc[6] spans q[0..5], anc[7] spans q[3..8].
for p in qmc.range(2):
anc[6 + p] = qmc.h(anc[6 + p])
for i in qmc.range(6):
anc[6 + p], q[3 * p + i] = qmc.cx(anc[6 + p], q[3 * p + i])
anc[6 + p] = qmc.h(anc[6 + p])
# X-component correction: per block, measure the syndrome, locate the X error, and correct.
for b in qmc.range(3):
s0 = qmc.measure(anc[2 * b])
s1 = qmc.measure(anc[2 * b + 1])
if s0 & s1: # (1, 1) -> qubit 0 of the block
q[3 * b] = qmc.x(q[3 * b])
if s0 & ~s1: # (1, 0) -> qubit 1 of the block
q[3 * b + 1] = qmc.x(q[3 * b + 1])
if ~s0 & s1: # (0, 1) -> qubit 2 of the block
q[3 * b + 2] = qmc.x(q[3 * b + 2])
# Z-component correction: find the block holding the Z error, apply Z to its representative.
phase_s0 = qmc.measure(anc[6])
phase_s1 = qmc.measure(anc[7])
if phase_s0 & ~phase_s1:
q[0] = qmc.z(q[0])
if phase_s0 & phase_s1:
q[3] = qmc.z(q[3])
if ~phase_s0 & phase_s1:
q[6] = qmc.z(q[6])
# Verification: apply the inverse encoder to collect the logical bit into q[0].
for b in qmc.range(3):
q[3 * b], q[3 * b + 1] = qmc.cx(q[3 * b], q[3 * b + 1])
q[3 * b], q[3 * b + 2] = qmc.cx(q[3 * b], q[3 * b + 2])
q[3 * b] = qmc.h(q[3 * b])
q[0], q[3] = qmc.cx(q[0], q[3])
q[0], q[6] = qmc.cx(q[0], q[6])
return qmc.measure(q)We try one representative error per block ( on block 0, on block 1, on block 2). If the logical is preserved, q[0] should always be 1.
shor_cases = [
("X", 1, 0),
("Y", 2, 4),
("Z", 3, 8),
]
if docs_test_mode:
shor_cases = [
("Y", 2, 4),
]
print("Shor 9-qubit code: logical |1>")
print(f" {'error':6s} | {'pos':5s} | P(q[0]=1)")
print(f" {'-' * 6}-+-{'-' * 5}-+-{'-' * 9}")
for name, error_type, error_pos in shor_cases:
counts = _sample_first_bit(
shor_syndrome_run,
bindings={"error_type": error_type, "error_pos": error_pos},
parameters=["theta"],
runtime_bindings={"theta": math.pi},
)
total = counts[0] + counts[1]
print(f" {name:6s} | q[{error_pos}] | {counts[1] / total:.3f}")
# The pure-|1> input survives any single Pauli error perfectly under
# Shor's code, so q[0] reads 1 on every shot.
assert counts[0] == 0
assert counts[1] == totalShor 9-qubit code: logical |1>
error | pos | P(q[0]=1)
-------+-------+----------
X | q[0] | 1.000
Y | q[4] | 1.000
Z | q[8] | 1.000
4.7 Why It Corrects “Any Single Error”¶
Shor’s code corrects all three of , , . This directly means it “corrects any single-qubit error”.
As seen in 1.4, any error can be written, as a matrix, as a linear combination of , and syndrome measurement collapses that superposition into a single discrete Pauli error. What remains after the collapse is just one of , , (or no error), all of which Shor’s code can correct. This is why it handles any single-qubit error, including continuous ones.
The formal treatment is organized in the second part, Stabilizer Formalism and the Steane Code.
5. The Common Pattern¶
We have built three codes — bit-flip, phase-flip, and Shor. In fact all of them follow the same four-step template.
Encode into a code space: spread one logical qubit across multiple physical qubits using entanglement.
Parity measurement: instead of measuring the data directly, extract a parity of several qubits ( or ) into ancilla qubits.
Syndrome: read the location and type of the error from the parity measurements.
Feedback correction: apply a Pauli gate according to the syndrome to cancel the error.
This skeleton does not change as the code changes. What differs is only “which parity to measure”.
5.1 Naming the Parity Operators: Stabilizers¶
The parity operators we have been measuring — , , and so on — have a name: stabilizers.
A stabilizer is a Pauli operator that leaves every state of the code space unchanged. Applied to a valid codeword, the state is unchanged (eigenvalue +1). When an error occurs, the measured value of a stabilizer that anticommutes with that error flips to -1, and that becomes a bit of the syndrome.
The of the bit-flip code, the of the phase-flip code, and the eight parities of Shor’s code are all stabilizers. The three codes were different manifestations of one idea: “measure stabilizers to obtain a syndrome.” This view is called the stabilizer formalism, and the second part treats it in earnest.
5.2 Summary of the Three Codes¶
We summarize the three codes together with their stabilizers.
| Code | Stabilizer generators | Corrects | |
|---|---|---|---|
| 3-qubit bit-flip | a single | ||
| 3-qubit phase-flip | a single | ||
| Shor 9-qubit | a single |
is a notation for a code: is the number of physical qubits, is the number of logical qubits protected, and is the code distance. The distance is an indicator with the meaning “ is required to correct any single-qubit error”. The 3-qubit codes have — they can correct only one specific type of error ( or ). Shor’s code has and corrects any single error. The precise definition of distance is covered in the second part.
6. Try It Yourself¶
To test your understanding, modify the code a little and run it.
Change the error location: set
error_posof each*_syndrome_runto various values and confirm that correction works.Make it fail on purpose: below, we experience the limitation of the bit-flip code.
Make It Fail on Purpose¶
The bit-flip code can correct only up to a single error. What happens when errors occur at two locations?
The following kernel injects errors at two locations, data[0] and data[1], into the logical (), and performs syndrome measurement and correction as usual.
@qmc.qkernel
def bitflip_two_errors(theta: qmc.Float) -> qmc.Vector[qmc.Bit]:
data = qmc.qubit_array(3, name="data")
anc = qmc.qubit_array(2, name="anc")
data[0] = qmc.ry(data[0], theta)
data[0], data[1], data[2] = encode_3qubit_bitflip(data[0], data[1], data[2])
# Inject X errors at two locations (beyond the single-error correction capability).
data[0] = qmc.x(data[0])
data[1] = qmc.x(data[1])
# Syndrome measurement and correction are the same as in bitflip_syndrome_run.
data[0], anc[0] = qmc.cx(data[0], anc[0])
data[1], anc[0] = qmc.cx(data[1], anc[0])
s0 = qmc.measure(anc[0])
data[0], anc[1] = qmc.cx(data[0], anc[1])
data[2], anc[1] = qmc.cx(data[2], anc[1])
s1 = qmc.measure(anc[1])
if s0 & s1:
data[0] = qmc.x(data[0])
if s0 & ~s1:
data[1] = qmc.x(data[1])
if ~s0 & s1:
data[2] = qmc.x(data[2])
return qmc.measure(data)print("bit-flip code with TWO X errors (logical |1>)")
counts = _sample_first_bit(
bitflip_two_errors,
parameters=["theta"],
runtime_bindings={"theta": math.pi},
)
print(f" data[0]=0 -> {counts[0]:3d}, data[0]=1 -> {counts[1]:3d}")
# Failure mode: with two X errors the syndrome misidentifies a single-X
# error and the "correction" turns logical |1> into logical |0>, so every
# shot deterministically reads data[0] = 0 — that is what d=1 means.
assert counts[1] == 0
assert counts[0] == counts[0] + counts[1]bit-flip code with TWO X errors (logical |1>)
data[0]=0 -> 256, data[0]=1 -> 0
data[0] becomes 0, not 1. Far from fixing the state, the correction turned the logical into the logical .
With at two locations, looks “different in only one place” from , so the correction applies an to the remaining location as well, ending at . This is the limitation “up to a single error” — the actual meaning of the bit-flip code having in the table of 5.2.
7. Summary¶
In this article we implemented three quantum error correction codes.
The 3-qubit bit-flip code — corrects a single error with parity checks.
The 3-qubit phase-flip code — corrects a single error with parity checks.
Shor’s 9-qubit code — concatenates the two and corrects any single-qubit error ().
The common skeleton was “encode into a code space → parity (stabilizer) measurement → syndrome → feedback correction”.
Next¶
The second part, Stabilizer Formalism and the Steane Code, treats the stabilizers — only named here — formally. Its main subjects are the CSS construction, which systematically builds quantum codes from classical codes, and the Steane code, which achieves with seven qubits, fewer than Shor’s nine.