Before we can understand quantum error correction—a feat that many physicists initially believed was impossible—we must first master the principles of classical error correction. The reason is not merely historical; it is structural. Every quantum...
In This Chapter
- Learning Objectives
- 23.1 Why Review Classical Error Correction?
- 23.2 The Classical Repetition Code
- 23.3 Parity Checks and Syndrome Measurement
- 23.4 Linear Codes: Generator and Parity-Check Matrices
- 23.5 Hamming Codes
- 23.6 The Three Challenges for Quantum Error Correction
- 23.7 The Quantum Repetition Code for Bit Flips (Warm-Up)
- 23.8 From Classical to Quantum: The Conceptual Bridge
- 23.9 Qiskit Implementation: Full Bit-Flip Code with Noise Simulation
- 23.10 Shannon's Channel Coding Theorem and the Limits of Error Correction
- 23.11 Qiskit Implementation: Classical Code Simulation
Chapter 23: Classical Error Correction Review: Repetition Codes, Hamming Codes, and Why Quantum Is Harder (No-Cloning Theorem)
Learning Objectives
By the end of this chapter, you will be able to:
- Construct and analyze the classical repetition code, including encoding, error detection, and majority-vote decoding.
- Derive the error probability for the repetition code and prove it decreases as $O(p^t)$ for a $2t+1$-bit code.
- Explain parity checks and syndrome measurement for linear codes, and derive the parity-check matrix from the generator matrix.
- Define Hamming codes, compute their parameters $(n, k, d)$, and implement encoding/decoding with full derivation of the syndrome-to-error-position mapping.
- Formulate linear codes using generator matrices $G$ and parity-check matrices $H$, including the systematic form and the relationship $GH^T = 0$.
- Prove the Singleton bound, the Hamming bound, and understand why perfect codes are special.
- Articulate the three fundamental obstacles to quantum error correction: the no-cloning theorem, continuous errors, and measurement-induced collapse.
- Implement the quantum repetition code for bit flips as a warm-up to full QEC.
- Connect classical coding theory concepts directly to their quantum generalizations.
23.1 Why Review Classical Error Correction?
Before we can understand quantum error correction—a feat that many physicists initially believed was impossible—we must first master the principles of classical error correction. The reason is not merely historical; it is structural. Every quantum error-correcting code is built from classical codes, using them as scaffolding. The Shor code concatenates two classical repetition codes. The Steane code is built from the classical Hamming code. The surface code uses classical parity checks on a lattice. Without understanding classical codes, quantum codes are incomprehensible.
But there is a deeper lesson. Classical error correction teaches us a principle that will recur throughout this book: noise is the enemy, and the only defense is redundancy. In the classical world, redundancy means copying bits. In the quantum world, copying is forbidden, but redundancy takes a subtler form—entanglement. The conceptual bridge from classical to quantum is the central theme of this chapter and the next two.
Recurring Theme: Noise Is the Enemy. Every physical communication channel—whether a telegraph wire, a fiber optic cable, or a quantum processor—is noisy. Error correction does not eliminate noise; it manages it by distributing information across more physical resources than strictly necessary. The key insight, which we'll see repeatedly, is that you can lower the effective error rate by using more physical bits/qubits, and this reduction can be made arbitrarily large.
Historical Context
Claude Shannon's 1948 paper "A Mathematical Theory of Communication" founded information theory by proving that reliable communication is possible over unreliable channels, provided the information rate is below the channel capacity. Richard Hamming, working at Bell Labs in the late 1940s, was frustrated by relay computers that would crash from parity errors. His 1950 paper introduced the first error-correcting codes—the Hamming codes—and opened the field of coding theory. These ideas are now the bedrock of every digital communication system, from QR codes to 5G cellular networks.
In the quantum realm, the situation seemed hopeless. As Landauer declared in 1995: "Computation is inherently dissipative; quantum states are too fragile to correct." Yet just months later, Peter Shor and Andrew Steane independently showed that quantum error correction is possible. Their breakthrough required overcoming three fundamental obstacles—the no-cloning theorem, measurement collapse, and continuous errors—all of which we will address in this chapter.
23.2 The Classical Repetition Code
23.2.1 Encoding
The simplest error-correcting code is the 3-bit repetition code. We encode a logical bit $b \in \{0, 1\}$ into three physical bits:
$$0_L \equiv 000, \quad 1_L \equiv 111.$$
The encoding is a linear map from $\{0, 1\}$ to $\{0, 1\}^3$:
$$b \mapsto b \cdot (1, 1, 1) = (b, b, b).$$
More generally, the $(2t+1)$-bit repetition code encodes one logical bit into $n = 2t+1$ physical bits:
$$b \mapsto (b, b, \ldots, b).$$
This code can correct up to $t$ bit-flip errors by majority vote.
Try It Yourself: Encode the bit $b = 1$ using the 5-bit repetition code. What is the codeword? If the 3rd bit flips, what is the received word? What does majority vote return?
23.2.2 Error Model
Assume a binary symmetric channel (BSC): each bit independently flips with probability $p$, and remains correct with probability $1-p$. The probability of a specific error pattern with $w$ flips among $n$ bits is:
$$P(\text{pattern with } w \text{ flips}) = p^w (1-p)^{n-w}.$$
The probability of exactly $w$ flips in an $n$-bit codeword is given by the binomial distribution:
$$P(\text{exactly } w \text{ flips in } n \text{ bits}) = \binom{n}{w} p^w (1-p)^{n-w}.$$
Worked Example 1: For $n = 3$ and $p = 0.01$, compute the probability of each possible number of flips.
$$P(0 \text{ flips}) = (0.99)^3 = 0.970299$$ $$P(1 \text{ flip}) = 3(0.01)(0.99)^2 = 0.029403$$ $$P(2 \text{ flips}) = 3(0.01)^2(0.99) = 0.000297$$ $$P(3 \text{ flips}) = (0.01)^3 = 0.000001$$
The decoding error occurs when 2 or more bits flip, which happens with probability $\approx 0.000298$, compared to the uncoded error rate of $0.01$—a 33× improvement.
23.2.3 Decoding by Majority Vote
The decoder receives a 3-bit string $r = (r_1, r_2, r_3)$ and applies majority vote:
$$\hat{b} = \text{majority}(r_1, r_2, r_3).$$
If at most one bit flips, the majority is correct. The probability of decoding error is the probability of two or more flips:
$$P_{\text{error}} = \binom{3}{2} p^2 (1-p) + \binom{3}{3} p^3 = 3p^2(1-p) + p^3 = 3p^2 - 2p^3.$$
Derivation: Let us derive this more carefully. The repetition code succeeds if and only if the majority of the received bits are correct. For a 3-bit code:
- If 0 bits flip (probability $(1-p)^3$): all three bits agree, correct decoding.
- If 1 bit flips (probability $3p(1-p)^2$): two out of three are correct, majority vote recovers the correct bit.
- If 2 bits flip (probability $3p^2(1-p)$): two out of three are wrong, majority vote returns the wrong bit.
- If 3 bits flip (probability $p^3$): all three are wrong, majority vote returns the wrong bit.
Therefore:
$$P_{\text{error}} = 3p^2(1-p) + p^3 = 3p^2 - 2p^3.$$
For small $p$, this is approximately $3p^2$, which is quadratically smaller than the uncoded error probability $p$. This quadratic suppression is the fundamental power of error correction.
For $p = 0.01$, the uncoded error probability is $0.01$, while the repetition code reduces it to $3(0.01)^2 - 2(0.01)^3 \approx 0.000298$—a 33× improvement.
Generalization to $n$-bit repetition code: For an $n$-bit repetition code with $n = 2t+1$, the decoding error probability is:
$$P_{\text{error}}^{(n)} = \sum_{k=t+1}^{n} \binom{n}{k} p^k (1-p)^{n-k}.$$
As $n \to \infty$ for fixed $p < 1/2$, this probability approaches 0 exponentially. This is the simplest demonstration of Shannon's channel coding theorem.
Common Misconception: "Error correction just adds redundancy, which wastes bandwidth." In the noisy-channel coding theorem, Shannon proved that codes can achieve arbitrarily low error rates at nonzero information rates. The repetition code is inefficient (rate $1/n$), but more sophisticated codes like Hamming and LDPC codes achieve much higher rates. Error correction is not waste—it is the bridge between unreliable physical channels and reliable digital communication.
23.2.4 Geometric Intuition
We can visualize the 3-bit repetition code geometrically. The codewords $\{000, 111\}$ are two points in the 3-dimensional binary cube $\{0,1\}^3$. The minimum distance between them is 3 (they differ in all three positions). The decoding regions are Hamming spheres of radius 1 around each codeword:
Binary Cube (3-bit code space):
111 *
/|\
/ | \
/ | \
011 *──────┼───* 101
| | |
| *001 |
| / /|
| / / |
010 */─────* |
/ | |
/ | |
110 *───┼──* 100
| /
|/
* 000
Codewords: 000 and 111
Decoding regions:
Sphere around 000: {000, 100, 010, 001}
Sphere around 111: {111, 011, 101, 110}
Minimum distance = 3
Corrects 1 error (radius 1)
The fact that the Hamming spheres of radius 1 around the two codewords are disjoint and cover all 8 vertices is a special property—in general, spheres may overlap or leave gaps.
23.2.5 Python Implementation
import numpy as np
from itertools import product
from collections import Counter
class RepetitionCode3:
"""3-bit classical repetition code."""
def encode(self, bit: int) -> tuple:
"""Encode a logical bit into 3 physical bits."""
if bit not in (0, 1):
raise ValueError("Bit must be 0 or 1")
return (bit, bit, bit)
def introduce_errors(self, codeword: tuple, p: float) -> tuple:
"""Simulate a binary symmetric channel with flip probability p."""
return tuple(
(b ^ 1) if np.random.random() < p else b
for b in codeword
)
def majority_decode(self, received: tuple) -> int:
"""Decode by majority vote."""
return Counter(received).most_common(1)[0][0]
def syndrome(self, received: tuple) -> tuple:
"""
Compute the syndrome: pairwise parity checks.
Syndrome bits: (r1 XOR r2, r2 XOR r3).
"""
r1, r2, r3 = received
return (r1 ^ r2, r2 ^ r3)
def decode_from_syndrome(self, received: tuple) -> int:
"""
Decode using syndrome lookup.
Syndrome 00 -> no error (or triple flip, indistinguishable)
Syndrome 01 -> bit 3 flipped
Syndrome 10 -> bit 1 flipped
Syndrome 11 -> bit 2 flipped
"""
syn = self.syndrome(received)
r1, r2, r3 = received
if syn == (0, 0):
return r1 # All agree
elif syn == (0, 1):
return r1 # Bit 3 is wrong, r1 and r2 agree
elif syn == (1, 0):
return r3 # Bit 1 is wrong, r2 and r3 agree
elif syn == (1, 1):
return r1 # Bit 2 is wrong, r1 and r3 agree
# ── Simulation ──
code = RepetitionCode3()
p = 0.01
n_trials = 100_000
errors_uncoded = 0
errors_coded = 0
for _ in range(n_trials):
bit = np.random.randint(0, 2)
# Uncoded
received_bit = bit ^ 1 if np.random.random() < p else bit
if received_bit != bit:
errors_uncoded += 1
# Coded
codeword = code.encode(bit)
received = code.introduce_errors(codeword, p)
decoded = code.majority_decode(received)
if decoded != bit:
errors_coded += 1
print(f"Uncoded error rate: {errors_uncoded / n_trials:.6f}")
print(f"Coded error rate: {errors_coded / n_trials:.6f}")
print(f"Improvement factor: {errors_uncoded / max(errors_coded, 1):.1f}x")
Expected output: The coded error rate should be approximately $3p^2 \approx 0.0003$, confirming the quadratic suppression of errors.
23.2.6 General $n$-bit Repetition Code Analysis
Worked Example 2: Analyze the 5-bit repetition code ($n = 5$, corrects up to $t = 2$ errors).
Encoding: $b \mapsto (b, b, b, b, b)$.
Error probability after majority decoding:
$$P_{\text{error}}^{(5)} = \binom{5}{3}p^3(1-p)^2 + \binom{5}{4}p^4(1-p) + \binom{5}{5}p^5 = 10p^3 - 20p^4 + 16p^5.$$
For $p = 0.01$: $P_{\text{error}}^{(5)} \approx 10(10^{-6}) = 10^{-5}$, a 1000× improvement over uncoded, and a 30× improvement over the 3-bit code.
For $p = 0.1$: $P_{\text{error}}^{(3)} = 3(0.01) - 2(0.001) = 0.028$, while $P_{\text{error}}^{(5)} \approx 0.00856$. The 5-bit code still provides significant improvement.
Try It Yourself: Compute $P_{\text{error}}$ for the 7-bit repetition code at $p = 0.01$. How does it compare to the 5-bit code? At what value of $p$ does the 3-bit repetition code become worse than no coding? (Hint: set $3p^2 - 2p^3 = p$ and solve.)
23.2.7 The Crossover Point
The 3-bit repetition code improves on uncoded transmission only when $3p^2 - 2p^3 < p$, i.e., when $p < 1/2$. At $p = 1/2$, the channel is completely random and no code can help. But there's a more subtle crossover: the repetition code increases the error rate when $p > 1/2$! This is because majority voting amplifies the majority, which is now the wrong bit.
Worked Example 3: At $p = 0.4$: - Uncoded: $P_{\text{error}} = 0.4$ - 3-bit repetition: $P_{\text{error}} = 3(0.16)(0.6) + (0.064) = 0.352$
At $p = 0.6$: - Uncoded: $P_{\text{error}} = 0.6$ - 3-bit repetition: $P_{\text{error}} = 3(0.36)(0.4) + 0.216 = 0.648$
The repetition code increases the error rate when $p > 0.5$. This makes sense: if the channel flips more often than not, majority vote will prefer the wrong answer.
23.3 Parity Checks and Syndrome Measurement
The repetition code detects errors by checking whether bits agree. Formally, we compute parity checks—linear functions of the received bits that are zero for valid codewords.
For the 3-bit repetition code, the parity checks are:
$$s_1 = r_1 \oplus r_2, \quad s_2 = r_2 \oplus r_3.$$
The vector $s = (s_1, s_2)$ is the syndrome. Crucially, the syndrome depends only on the error pattern, not on the encoded bit:
- If $b = 0$ and bit 1 flips: $r = 100$, $s = (1, 0)$.
- If $b = 1$ and bit 1 flips: $r = 011$, $s = (1, 0)$.
The syndrome is identical in both cases. This blindness to the encoded information is the key property that generalizes to quantum error correction, where we must learn about errors without learning about the encoded state.
23.3.1 Syndrome Lookup Table
For the 3-bit repetition code, the complete syndrome table is:
| Received $r$ | Syndrome $(s_1, s_2)$ | Error Pattern $e$ | Decoded |
|---|---|---|---|
| 000 | (0, 0) | 000 (no error) | 0 |
| 100 | (1, 0) | 100 (bit 1 flipped) | 1 |
| 010 | (1, 1) | 010 (bit 2 flipped) | 0 |
| 001 | (0, 1) | 001 (bit 3 flipped) | 0 |
| 110 | (0, 1) | 010 (bit 2 flipped) | 1 |
| 101 | (1, 1) | 010 (bit 2 flipped) | 1 |
| 011 | (1, 0) | 100 (bit 1 flipped) | 0 |
| 111 | (0, 0) | 000 (no error) | 1 |
Notice that $(0,0)$ can mean either "no error" or "triple error"—these are indistinguishable by the syndrome alone. This is why the 3-bit code can only correct 1 error; a 3-error pattern is decoded incorrectly.
23.3.2 Why Syndrome-Based Decoding Matters
Recurring Theme: Quantum Is Linear Algebra, Not Magic. Syndrome-based decoding is fundamentally linear algebra: we compute $s = Hr^T$, look up the most likely error pattern, and subtract it. This linear-algebraic structure carries over directly to quantum codes, where the syndrome is an eigenvalue measurement of stabilizer generators, and the correction is a Pauli operator. The quantum case is more subtle only because the errors are operators, not vectors—but the linear algebra framework is the same.
The syndrome approach separates error detection from error correction: 1. Detection: Compute $s = Hr^T$. If $s = 0$, no detectable error occurred. 2. Diagnosis: Look up the most likely error pattern corresponding to $s$. 3. Correction: Subtract the error pattern: $\hat{c} = r - e$.
This separation is crucial in the quantum case, where we can measure the syndrome without disturbing the encoded quantum state.
23.4 Linear Codes: Generator and Parity-Check Matrices
23.4.1 Definitions
An $[n, k, d]$ linear code over $\mathbb{F}_2$ is a $k$-dimensional subspace $C \subseteq \mathbb{F}_2^n$ where:
- $n$: block length (number of physical bits).
- $k$: number of logical bits encoded (dimension of the code space).
- $d$: minimum distance—the minimum Hamming weight of any nonzero codeword.
The code can correct $\lfloor (d-1)/2 \rfloor$ errors and detect $d-1$ errors.
Derivation of the correction capability: If $d$ is the minimum distance, then any two codewords $c_1, c_2$ satisfy $d_H(c_1, c_2) \geq d$, where $d_H$ is the Hamming distance. If an error $e$ with weight $w \leq t = \lfloor(d-1)/2\rfloor$ occurs, then:
$$d_H(c_1, c_1 + e) = w \leq t$$ $$d_H(c_1 + e, c_2) \geq d_H(c_1, c_2) - d_H(c_1, c_1 + e) \geq d - t > t$$
So the received word $r = c_1 + e$ is closer to $c_1$ than to any other codeword, guaranteeing correct decoding.
23.4.2 The Code Rate
The rate of an $[n, k, d]$ code is $R = k/n$. This measures the fraction of transmitted bits that carry information (versus redundancy). The repetition code has rate $R = 1/n$ (very inefficient), while the Hamming $[7,4,3]$ code has rate $R = 4/7 \approx 0.57$ (much more efficient).
Shannon's noisy channel coding theorem guarantees that for any BSC with flip probability $p < 1/2$, there exist codes with rate arbitrarily close to the Shannon capacity $C = 1 - H_2(p)$, where $H_2(p) = -p\log_2 p - (1-p)\log_2(1-p)$ is the binary entropy function.
23.4.3 Generator Matrix
A generator matrix $G$ (size $k \times n$) maps a logical message $m \in \mathbb{F}_2^k$ to a codeword $c \in C$:
$$c = m G \quad (\text{mod } 2).$$
For the 3-bit repetition code:
$$G = \begin{pmatrix} 1 & 1 & 1 \end{pmatrix}.$$
The generator matrix provides a basis for the code space. Every codeword is a $\mathbb{F}_2$-linear combination of the rows of $G$.
Worked Example 4: Encode the message $m = (1, 0, 1, 0)$ using the Hamming $[7,4,3]$ code generator matrix:
$$G = \begin{pmatrix} 1 & 1 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 & 1 & 0 & 0 \\ 1 & 1 & 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 & 0 & 0 & 1 \end{pmatrix}.$$
Computing $c = mG$:
$$c = 1 \cdot (1,1,0,1,0,0,0) + 0 \cdot (0,1,1,0,1,0,0) + 1 \cdot (1,1,1,0,0,1,0) + 0 \cdot (1,0,1,0,0,0,1)$$ $$= (1,1,0,1,0,0,0) + (1,1,1,0,0,1,0) = (0,0,1,1,0,1,0).$$
Verify: $Hc^T = 0$ using the Hamming parity-check matrix.
23.4.4 Parity-Check Matrix
A parity-check matrix $H$ (size $(n-k) \times n$) satisfies:
$$H c^T = 0 \quad \forall c \in C.$$
Equivalently, $C = \ker(H)$. For any received word $r = c + e$ (where $e$ is the error vector), the syndrome is:
$$s = H r^T = H(c + e)^T = H e^T.$$
The syndrome depends only on the error $e$, not on the codeword $c$. This is the fundamental property that makes syndrome decoding possible.
For the 3-bit repetition code:
$$H = \begin{pmatrix} 1 & 1 & 0 \\ 0 & 1 & 1 \end{pmatrix}.$$
Verify: $H \cdot (1,1,1)^T = (0, 0)^T$ and $H \cdot (0,0,0)^T = (0,0)^T$. ✓
Key relationship: The generator and parity-check matrices satisfy:
$$G H^T = 0 \pmod{2}.$$
This encodes the fact that every codeword satisfies all parity checks. In the systematic form $G = [I_k | P]$, the parity-check matrix is:
$$H = [-P^T | I_{n-k}] = [P^T | I_{n-k}] \quad (\text{over } \mathbb{F}_2, \text{ where } -1 = 1).$$
23.4.5 Duality and the MacWilliams Identity
The dual code $C^\perp$ of a code $C$ with generator matrix $G$ has $G$ as its parity-check matrix:
$$C^\perp = \{x \in \mathbb{F}_2^n : x \cdot c = 0 \quad \forall c \in C\}.$$
If $C$ is an $[n, k, d]$ code, then $C^\perp$ is an $[n, n-k, d^\perp]$ code where $d^\perp$ is the minimum weight of $H$'s rows.
The MacWilliams identity relates the weight distributions of $C$ and $C^\perp$:
$$W_{C^\perp}(x, y) = \frac{1}{|C|} W_C(x+y, x-y).$$
This identity will be crucial when we study CSS codes in Chapter 24, where the dual code determines the $X$-type stabilizers.
Common Misconception: "The dual code is just the transpose." The dual code $C^\perp$ is not obtained by transposing the generator matrix. Rather, $C^\perp$ is the code whose generator matrix equals the parity-check matrix of $C$. The rows of $H$ generate $C^\perp$, and the rows of $G$ generate $C$.
23.4.6 Python Implementation of Linear Codes
import numpy as np
class LinearCode:
"""Generic linear code over GF(2)."""
def __init__(self, G: np.ndarray):
"""
G: k x n generator matrix over GF(2).
"""
self.G = G % 2
self.k, self.n = G.shape
self._compute_H()
def _compute_H(self):
"""Compute parity-check matrix H in systematic form if possible."""
G_sys = self._systematic_form(self.G.copy())
if G_sys is not None:
k = self.k
P = G_sys[:, k:] # k x (n-k)
self.H = np.hstack([P.T, np.eye(self.n - k, dtype=int)]) % 2
else:
self.H = self._nullspace(self.G)
def _systematic_form(self, G):
"""Gaussian elimination to put G in [I | P] form."""
G = G.copy() % 2
k, n = G.shape
pivot_cols = []
row = 0
for col in range(n):
if row >= k:
break
pivot_row = None
for r in range(row, k):
if G[r, col] == 1:
pivot_row = r
break
if pivot_row is None:
continue
G[[row, pivot_row]] = G[[pivot_row, row]]
pivot_cols.append(col)
for r in range(k):
if r != row and G[r, col] == 1:
G[r] = (G[r] + G[row]) % 2
row += 1
if len(pivot_cols) == k:
remaining = [c for c in range(n) if c not in pivot_cols]
col_order = pivot_cols + remaining
G = G[:, col_order]
return G
return None
def _nullspace(self, G):
"""Compute a basis for the nullspace of G over GF(2)."""
k, n = G.shape
augmented = np.hstack([G.T, np.eye(n, dtype=int)]) % 2
for col in range(k):
pivot = None
for row in range(col, n):
if augmented[row, col] == 1:
pivot = row
break
if pivot is None:
continue
augmented[[col, pivot]] = augmented[[pivot, col]]
for row in range(n):
if row != col and augmented[row, col] == 1:
augmented[row] = (augmented[row] + augmented[col]) % 2
H_rows = []
for row in range(n):
if np.all(augmented[row, :k] == 0):
H_rows.append(augmented[row, k:])
return np.array(H_rows, dtype=int) % 2
def encode(self, message: np.ndarray) -> np.ndarray:
"""Encode a k-bit message into an n-bit codeword."""
return (message @ self.G) % 2
def syndrome(self, received: np.ndarray) -> np.ndarray:
"""Compute the syndrome of a received word."""
return (self.H @ received) % 2
def decode(self, received: np.ndarray) -> np.ndarray:
"""
Syndrome-based decoding using a precomputed lookup table
for single-bit errors.
"""
syn = tuple(self.syndrome(received))
if not hasattr(self, '_syndrome_lut'):
self._syndrome_lut = {}
for i in range(self.n):
error = np.zeros(self.n, dtype=int)
error[i] = 1
s = tuple((self.H @ error) % 2)
self._syndrome_lut[s] = error
if syn == tuple(np.zeros(self.n - self.k, dtype=int)):
corrected = received.copy()
elif syn in self._syndrome_lut:
corrected = (received + self._syndrome_lut[syn]) % 2
else:
corrected = received.copy()
return corrected[:self.k]
def minimum_distance(self) -> int:
"""Compute the minimum distance by checking all nonzero codewords."""
min_wt = self.n
for i in range(1, 2**self.k):
msg = np.array([(i >> j) & 1 for j in range(self.k)])
cw = self.encode(msg)
wt = np.sum(cw)
if 0 < wt < min_wt:
min_wt = wt
return min_wt
def weight_distribution(self) -> dict:
"""Compute the weight distribution of the code."""
dist = {}
for i in range(2**self.k):
msg = np.array([(i >> j) & 1 for j in range(self.k)])
cw = self.encode(msg)
wt = int(np.sum(cw))
dist[wt] = dist.get(wt, 0) + 1
return dist
# ── Test with repetition code ──
G_rep = np.array([[1, 1, 1]], dtype=int)
code_rep = LinearCode(G_rep)
print("Repetition code [3,1,3]:")
print(f"G =\n{code_rep.G}")
print(f"H =\n{code_rep.H}")
print(f"Minimum distance: {code_rep.minimum_distance()}")
msg = np.array([1])
codeword = code_rep.encode(msg)
print(f"Encode(1) = {codeword}")
# Introduce error on bit 2
received = codeword.copy()
received[1] ^= 1
print(f"Received (error on bit 2): {received}")
print(f"Syndrome: {code_rep.syndrome(received)}")
print(f"Decoded: {code_rep.decode(received)}")
23.4.7 Bounds on Code Parameters
Understanding the fundamental limits of error-correcting codes helps us evaluate whether a particular code is efficient. Three important bounds govern $[n, k, d]$ codes:
The Singleton Bound: $n - k \geq d - 1$, or equivalently $k \leq n - d + 1$.
Proof: Delete the last $d-1$ coordinates from every codeword. Since the minimum distance is $d$, no two codewords can agree on the first $n-(d-1)$ positions (otherwise they'd differ in at most $d-1$ positions, contradicting $d_{\min} = d$). Therefore the truncated codewords are distinct, and there can be at most $2^{n-d+1}$ of them. Since the code has $2^k$ codewords, $2^k \leq 2^{n-d+1}$, giving $k \leq n - d + 1$.
The Hamming (Sphere-Packing) Bound: An $[n, k, d]$ code with $d = 2t+1$ satisfies:
$$2^k \leq \frac{2^n}{\sum_{i=0}^t \binom{n}{i}}.$$
Proof: The Hamming spheres of radius $t$ around each of the $2^k$ codewords are disjoint (since the minimum distance is $2t+1$). Each sphere contains $\sum_{i=0}^t \binom{n}{i}$ points. Therefore $2^k \cdot \sum_{i=0}^t \binom{n}{i} \leq 2^n$.
The Gilbert-Varshamov Bound (existence): There exist $[n, k, d]$ codes with:
$$2^k \geq \frac{2^n}{\sum_{i=0}^{d-2} \binom{n-1}{i}}.$$
This guarantees that good codes exist, even though we may not know how to construct them efficiently.
Try It Yourself: Verify the Hamming bound for the $[7,4,3]$ code: $2^4 = 16 \leq 2^7 / (\binom{7}{0} + \binom{7}{1}) = 128/8 = 16$. The bound is achieved with equality—this is why the Hamming code is called a perfect code.
23.5 Hamming Codes
23.5.1 Definition
Hamming codes are a family of perfect single-error-correcting codes with parameters:
$$[n = 2^r - 1, \; k = 2^r - 1 - r, \; d = 3],$$
for integer $r \geq 2$. The parity-check matrix $H$ is an $r \times (2^r - 1)$ matrix whose columns are all nonzero binary vectors of length $r$.
The name "perfect" comes from the Hamming bound: Hamming codes achieve the sphere-packing bound with equality, meaning every binary string is either a codeword or within distance 1 of exactly one codeword. There are no "gaps" in the decoding regions.
23.5.2 The [7, 4, 3] Hamming Code
For $r = 3$, we obtain the celebrated $[7, 4, 3]$ Hamming code. The parity-check matrix (with columns in natural order) is:
$$H = \begin{pmatrix} 1 & 0 & 1 & 0 & 1 & 0 & 1 \\ 0 & 1 & 1 & 0 & 0 & 1 & 1 \\ 0 & 0 & 0 & 1 & 1 & 1 & 1 \end{pmatrix}.$$
Notice that the columns are the binary representations of $1, 2, 3, 4, 5, 6, 7$:
| Column | Binary | Decimal |
|---|---|---|
| 1 | (1,0,0) | 1 |
| 2 | (0,1,0) | 2 |
| 3 | (1,1,0) | 3 |
| 4 | (0,0,1) | 4 |
| 5 | (1,0,1) | 5 |
| 6 | (0,1,1) | 6 |
| 7 | (1,1,1) | 7 |
This is the key to the elegant syndrome decoding: when a single error occurs at position $i$, the syndrome equals column $i$ of $H$, which is exactly the binary representation of $i$.
The generator matrix in systematic form is:
$$G = \begin{pmatrix} 1 & 1 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 & 1 & 0 & 0 \\ 1 & 1 & 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 & 0 & 0 & 1 \end{pmatrix}.$$
Worked Example 5: Encode the message $m = (1, 0, 1, 1)$ using the Hamming code.
$$c = mG = (1,0,1,1) \begin{pmatrix} 1&1&0&1&0&0&0 \\ 0&1&1&0&1&0&0 \\ 1&1&1&0&0&1&0 \\ 1&0&1&0&0&0&1 \end{pmatrix}$$
Computing each component modulo 2: $$c_1 = 1 \cdot 1 + 0 \cdot 0 + 1 \cdot 1 + 1 \cdot 1 = 1 + 0 + 1 + 1 = 1$$ $$c_2 = 1 \cdot 1 + 0 \cdot 1 + 1 \cdot 1 + 1 \cdot 0 = 1 + 0 + 1 + 0 = 0$$ $$c_3 = 1 \cdot 0 + 0 \cdot 1 + 1 \cdot 1 + 1 \cdot 1 = 0 + 0 + 1 + 1 = 0$$ $$c_4 = 1 \cdot 1 + 0 \cdot 0 + 1 \cdot 0 + 1 \cdot 0 = 1 + 0 + 0 + 0 = 1$$ $$c_5 = 0, \; c_6 = 1, \; c_7 = 1$$
So $c = (1, 0, 0, 1, 0, 1, 1)$.
Verify: Compute $Hc^T$:
$$Hc^T = \begin{pmatrix} 1&0&1&0&1&0&1 \\ 0&1&1&0&0&1&1 \\ 0&0&0&1&1&1&1 \end{pmatrix} \begin{pmatrix} 1 \\ 0 \\ 0 \\ 1 \\ 0 \\ 1 \\ 1 \end{pmatrix} = \begin{pmatrix} 1+0+0+0+0+0+1 \\ 0+0+0+0+0+1+1 \\ 0+0+0+1+0+1+1 \end{pmatrix} = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix} \text{... wait}$$
Let me recompute. Actually, $Hc^T$ should be $(0, 0, 0)^T$ for a valid codeword.
$$c_1 \cdot H_{1j} = 1 \cdot (1, 0, 0) = (1, 0, 0)$$ $$c_3 \cdot H_{3j} = 0$$ $$c_4 \cdot H_{4j} = 1 \cdot (0, 0, 1) = (0, 0, 1)$$ $$c_6 \cdot H_{6j} = 1 \cdot (0, 1, 1) = (0, 1, 1)$$ $$c_7 \cdot H_{7j} = 1 \cdot (1, 1, 1) = (1, 1, 1)$$
Sum: $(1+0+0+1, 0+1+1, 0+1+1) = (0, 0, 0) \pmod{2}$. ✓
The syndrome $s = Hr^T$ is a 3-bit vector. If a single error occurs at position $i$, the syndrome equals the $i$-th column of $H$ (interpreted as a binary number). This enables instant error location.
Worked Example 6: An error occurs at position 5. The received word is $r = c + e$ where $e = (0,0,0,0,1,0,0)$.
$$s = He^T = H_{\cdot 5} = \begin{pmatrix} 1 \\ 0 \\ 1 \end{pmatrix}$$
Interpreting as a binary number: $1 \cdot 1 + 2 \cdot 0 + 4 \cdot 1 = 5$. The syndrome directly tells us position 5!
23.5.3 Complete Decoding Table
For the $[7,4,3]$ Hamming code, the complete syndrome decoding table for all single-bit errors:
| Error Position | Syndrome (binary) | Syndrome (decimal) |
|---|---|---|
| No error | 000 | 0 |
| Position 1 | 100 | 1 |
| Position 2 | 010 | 2 |
| Position 3 | 110 | 3 |
| Position 4 | 001 | 4 |
| Position 5 | 101 | 5 |
| Position 6 | 011 | 6 |
| Position 7 | 111 | 7 |
The syndrome is literally the binary representation of the error position! This is the elegance of the Hamming code.
23.5.4 Weight Distribution and Perfect Codes
The weight distribution of the $[7,4,3]$ Hamming code is:
| Weight | Number of Codewords |
|---|---|
| 0 | 1 |
| 3 | 7 |
| 4 | 7 |
| 7 | 1 |
| Total | 16 |
Since the minimum weight of nonzero codewords is 3, the minimum distance is $d = 3$, confirming the code can correct 1 error.
The code is perfect because the Hamming spheres of radius 1 around all 16 codewords exactly cover $\{0,1\}^7$:
$$16 \times (1 + 7) = 128 = 2^7.$$
Every binary string of length 7 is either a codeword or exactly distance 1 from exactly one codeword.
23.5.5 Python Implementation
def hamming_7_4():
"""Construct the [7,4,3] Hamming code."""
H = np.zeros((3, 7), dtype=int)
for i in range(7):
col_bits = format(i + 1, '03b')
H[0, i] = int(col_bits[2])
H[1, i] = int(col_bits[1])
H[2, i] = int(col_bits[0])
G = np.array([
[1, 1, 0, 1, 0, 0, 0],
[0, 1, 1, 0, 1, 0, 0],
[1, 1, 1, 0, 0, 1, 0],
[1, 0, 1, 0, 0, 0, 1]
], dtype=int)
return G, H
def hamming_encode(message: np.ndarray) -> np.ndarray:
"""Encode 4-bit message using [7,4,3] Hamming code."""
G, _ = hamming_7_4()
return (message @ G) % 2
def hamming_decode(received: np.ndarray) -> np.ndarray:
"""Decode received 7-bit word, correcting single errors."""
_, H = hamming_7_4()
syndrome = (H @ received) % 2
error_pos = syndrome[0] * 1 + syndrome[1] * 2 + syndrome[2] * 4
corrected = received.copy()
if error_pos > 0 and error_pos <= 7:
corrected[error_pos - 1] ^= 1
return np.array([corrected[2], corrected[4], corrected[5], corrected[6]])
# ── Test ──
G, H = hamming_7_4()
print("[7,4,3] Hamming Code")
print(f"G =\n{G}")
print(f"H =\n{H}")
# Verify G H^T = 0
print(f"G H^T mod 2 =\n{(G @ H.T) % 2}")
# Encode and decode
msg = np.array([1, 0, 1, 1])
codeword = hamming_encode(msg)
print(f"\nMessage: {msg}")
print(f"Codeword: {codeword}")
# Introduce error at position 5
received = codeword.copy()
received[4] ^= 1
print(f"Received (error at pos 5): {received}")
decoded = hamming_decode(received)
print(f"Decoded: {decoded}")
print(f"Correct: {np.array_equal(msg, decoded)}")
# ── Monte Carlo simulation ──
n_trials = 100_000
p = 0.01
errors = 0
for _ in range(n_trials):
msg = np.random.randint(0, 2, 4)
cw = hamming_encode(msg)
# Simulate BSC
noise = (np.random.random(7) < p).astype(int)
received = (cw + noise) % 2
decoded = hamming_decode(received)
if not np.array_equal(msg, decoded):
errors += 1
print(f"\nHamming code simulation (p={p}):")
print(f" Logical error rate: {errors/n_trials:.6f}")
print(f" Theoretical (2-error): ~7*3*p^2*(1-p)^5 + ... ≈ {7*3*p**2:.6f}")
23.5.6 Extended Hamming Code
Adding an overall parity check bit to the $[7,4,3]$ Hamming code yields the $[8,4,4]$ extended Hamming code. The new parity bit is:
$$c_8 = c_1 \oplus c_2 \oplus c_3 \oplus c_4 \oplus c_5 \oplus c_6 \oplus c_7.$$
The extended code can detect 2 errors and correct 1 error (distance 4). This is useful in practice because it allows detection of uncorrectable 2-error patterns.
The extended parity-check matrix is:
$$H_{\text{ext}} = \begin{pmatrix} 1 & 0 & 1 & 0 & 1 & 0 & 1 & 0 \\ 0 & 1 & 1 & 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 1 & 1 & 1 & 1 & 0 \\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 \end{pmatrix}.$$
Common Misconception: "Hamming codes can only correct 1 error, so they're useless." In practice, Hamming codes are used in combination with other techniques. For instance, in DRAM memory (ECC RAM), extended Hamming codes provide single-error correction and double-error detection (SECDED). When concatenated or used as building blocks for quantum codes (as in the Steane code), they become powerful components of much more capable error-correcting systems.
23.6 The Three Challenges for Quantum Error Correction
With classical error correction firmly in hand, we now confront the three fundamental obstacles that make QEC qualitatively harder.
23.6.1 Challenge 1: The No-Cloning Theorem
Statement (Wootters–Zurek, Dieks, 1982): There is no unitary operation $U$ that copies an arbitrary unknown quantum state:
$$\not\exists U : U(|\psi\rangle_A \otimes |0\rangle_B) = |\psi\rangle_A \otimes |\psi\rangle_B \quad \forall |\psi\rangle.$$
Proof: Suppose such $U$ exists. Then for any $|\psi\rangle, |\phi\rangle$:
$$\langle\psi|\phi\rangle = (\langle\psi|_A \otimes \langle 0|_B) U^\dagger U (|\phi\rangle_A \otimes |0\rangle_B) = \langle\psi|\phi\rangle^2.$$
The equation $x = x^2$ has solutions $x \in \{0, 1\}$. Thus $U$ can only clone states that are orthogonal—it cannot clone superpositions.
Detailed Proof: Let $U$ be a unitary operator that clones any quantum state. Then for any $|\psi\rangle$:
$$U(|\psi\rangle \otimes |0\rangle) = |\psi\rangle \otimes |\psi\rangle.$$
Consider two states $|\psi\rangle$ and $|\phi\rangle$ with $\langle\psi|\phi\rangle = c$. Taking the inner product:
$$\langle\psi|\phi\rangle \cdot \langle 0|0\rangle = \langle\psi|\phi\rangle$$
$$\langle\psi|\phi\rangle^2 = \langle\psi|\phi\rangle$$
Wait—let me redo this. We need to compute $\langle\psi|\phi\rangle$ via the cloning operation. Since $U$ is unitary:
$$\langle\psi \otimes 0 | U^\dagger U | \phi \otimes 0\rangle = \langle\psi \otimes 0 | \phi \otimes 0\rangle$$
But $U(|\phi\rangle \otimes |0\rangle) = |\phi\rangle \otimes |\phi\rangle$ and $U(|\psi\rangle \otimes |0\rangle) = |\psi\rangle \otimes |\psi\rangle$, so:
$$\langle\psi \otimes \psi | \phi \otimes \phi\rangle = \langle\psi|\phi\rangle \cdot \langle\psi|\phi\rangle = c^2$$
On the other hand:
$$\langle\psi \otimes 0 | \phi \otimes 0\rangle = \langle\psi|\phi\rangle \cdot \langle 0|0\rangle = c \cdot 1 = c$$
Since $U$ is unitary, inner products are preserved, so $c^2 = c$, giving $c \in \{0, 1\}$.
This means only orthogonal states can be cloned—superpositions cannot be copied.
Implication for QEC: We cannot simply make redundant copies of a qubit as in the classical repetition code. Instead, we must encode the logical qubit into an entangled state of multiple physical qubits, spreading the information non-locally.
Common Misconception: "The no-cloning theorem means we can't have redundancy in quantum computers." This is wrong. We can have redundancy—we just can't implement it by copying. Instead, we use entanglement to spread quantum information across multiple qubits. The quantum repetition code encodes $|\psi\rangle = \alpha|0\rangle + \beta|1\rangle$ as $\alpha|000\rangle + \beta|111\rangle$, which is an entangled GHZ state, not three copies of $|\psi\rangle$.
23.6.2 Challenge 2: Measurement Destroys Superposition
In classical error correction, we measure the received bits to determine the error. In quantum mechanics, measuring a qubit in the computational basis projects it onto $|0\rangle$ or $|1\rangle$, destroying any superposition.
Resolution: We measure syndrome observables that reveal information about the error but commute with the logical observables. This extracts error information without collapsing the logical state. The syndrome measurement projects the error onto a discrete Pauli error, digitizing the continuous error.
Detailed explanation: Consider the 3-qubit bit-flip code encoding $|\psi\rangle = \alpha|000\rangle + \beta|111\rangle$. If a bit-flip error occurs on qubit 1, the state becomes $\alpha|100\rangle + \beta|011\rangle$. We cannot measure the individual qubits (that would destroy the superposition), but we can measure the parity operators $Z_1 Z_2$ and $Z_2 Z_3$:
- $Z_1 Z_2$ on $|100\rangle$: eigenvalue $(-1)(+1) = -1$
- $Z_1 Z_2$ on $|011\rangle$: eigenvalue $(+1)(-1) = -1$
Both components have the same eigenvalue $-1$ for $Z_1 Z_2$. The measurement projects onto the $-1$ eigenspace but does NOT collapse $\alpha$ or $\beta$—the relative amplitudes are preserved. This is the crucial insight: the syndrome operators commute with the logical operators, so they extract only error information.
23.6.3 Challenge 3: Errors Are Continuous
A classical bit flip is discrete. A quantum error can be an arbitrary rotation $R_{\hat{n}}(\theta)$ by a small angle $\theta$. There are uncountably many possible errors—how can a finite set of correction operations handle them all?
Resolution: Any single-qubit error operator $E$ can be expanded in the Pauli basis:
$$E = e_0 I + e_1 X + e_2 Y + e_3 Z,$$
where $e_i \in \mathbb{C}$. When we measure the syndrome, the state is projected onto an eigenstate of the syndrome measurement, effectively collapsing $E$ onto one of the Pauli operators (or a linear combination that the code can handle). This error discretization is the magic that makes QEC possible.
Detailed derivation: Consider an error $E = \cos(\theta) I + i\sin(\theta) X$ (a small rotation about the $x$-axis). Applied to the encoded state $|\psi_L\rangle$:
$$E|\psi_L\rangle = \cos(\theta)|\psi_L\rangle + i\sin(\theta) X_L |\psi_L\rangle$$
When we measure the stabilizer generators, we project onto either the $+1$ eigenstate (no error detected, $\cos(\theta)$ component) or the $-1$ eigenstate ($X$ error detected, $i\sin(\theta)$ component). The measurement digitizes the continuous rotation into a discrete outcome: either "no error" (apply $I$) or "$X$ error" (apply $X$). The probabilities of these outcomes are $\cos^2(\theta)$ and $\sin^2(\theta)$ respectively, but conditional on the outcome, we know exactly which correction to apply.
Recurring Theme: Quantum Is Linear Algebra, Not Magic. Error discretization is a consequence of the linearity of quantum mechanics. The Pauli matrices form a basis for the space of $2 \times 2$ matrices, so any error can be decomposed into Pauli components. The syndrome measurement projects onto a specific Pauli component, and the corresponding Pauli correction perfectly reverses it. This is not magic—it is linear algebra applied to the Hilbert space.
23.6.4 Summary: From Classical to Quantum
| Aspect | Classical EC | Quantum EC |
|---|---|---|
| Redundancy | Copy bits | Entangle qubits |
| Errors | Bit flips (1 type) | Pauli $X$, $Y$, $Z$ (3 types) |
| Error detection | Parity checks | Stabilizer measurements |
| Syndrome | $s = Hr^T$ | Eigenvalues of stabilizers |
| Correction | Flip bits | Apply Pauli operators |
| Measurement | No restriction | Must not reveal logical state |
| Error model | Discrete | Continuous (but digitized by measurement) |
23.7 The Quantum Repetition Code for Bit Flips (Warm-Up)
Before tackling full QEC codes, we construct a quantum analog of the repetition code that corrects bit-flip errors. This code does not correct phase flips, but it illustrates the essential ideas.
23.7.1 Encoding
The logical states are:
$$|0_L\rangle = |000\rangle, \quad |1_L\rangle = |111\rangle.$$
An arbitrary logical state $|\psi_L\rangle = \alpha|0_L\rangle + \beta|1_L\rangle$ is encoded as:
$$|\psi_L\rangle = \alpha|000\rangle + \beta|111\rangle.$$
This is a GHZ state—a maximally entangled state of three qubits. Note that this is not three copies of $|\psi\rangle$ (which would violate the no-cloning theorem). The information is stored in the entanglement, not in individual qubits.
Proof that this is not cloning: If we could clone $|\psi\rangle$, we would have $|\psi\rangle \otimes |\psi\rangle \otimes |\psi\rangle = (\alpha|0\rangle + \beta|1\rangle)^{\otimes 3}$. But this state is $\alpha^3|000\rangle + \alpha^2\beta(|001\rangle + |010\rangle + |100\rangle) + \ldots$, which is a superposition of all eight computational basis states with various amplitudes. The GHZ state $\alpha|000\rangle + \beta|111\rangle$ has only two terms—it is fundamentally different.
Partial trace demonstration: The reduced density matrix of any single qubit is:
$$\rho_1 = \text{Tr}_{2,3}(|\psi_L\rangle\langle\psi_L|) = |\alpha|^2|0\rangle\langle 0| + |\beta|^2|1\rangle\langle 1| = \frac{I}{2} + (|\alpha|^2 - |\beta|^2)\frac{Z}{2}.$$
For the maximally entangled case $\alpha = \beta = 1/\sqrt{2}$, $\rho_1 = I/2$—the maximally mixed state, containing no information about the logical state. The information is entirely in the correlations.
23.7.2 Encoding Circuit
The encoding circuit prepares $|\psi_L\rangle$ from $|\psi\rangle|00\rangle$:
q0 (data): ──●──●──
│ │
q1 (anc): ──X──┼──
│
q2 (anc): ─────X──
Step-by-step evolution: 1. Initial state: $(\alpha|0\rangle + \beta|1\rangle)|00\rangle = \alpha|000\rangle + \beta|100\rangle$ 2. After CNOT(0→1): $\alpha|000\rangle + \beta|110\rangle$ 3. After CNOT(0→2): $\alpha|000\rangle + \beta|111\rangle$ ✓
In Qiskit:
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
def encode_bit_flip_code():
"""Create the 3-qubit bit-flip code encoding circuit."""
qr = QuantumRegister(3, 'q')
qc = QuantumCircuit(qr)
qc.cx(0, 1)
qc.cx(0, 2)
return qc
qc_enc = encode_bit_flip_code()
print(qc_enc.draw('text'))
23.7.3 Syndrome Extraction
To detect a bit flip on any qubit, we measure the parity operators $Z_1 Z_2$ and $Z_2 Z_3$. These are the quantum analogs of the classical parity checks $r_1 \oplus r_2$ and $r_2 \oplus r_3$.
Why these stabilizers? The code space is the span of $\{|000\rangle, |111\rangle\}$. Both states are $+1$ eigenstates of $Z_1 Z_2$ and $Z_2 Z_3$:
$$Z_1 Z_2|000\rangle = (+1)(+1)|000\rangle = |000\rangle \quad \checkmark$$ $$Z_1 Z_2|111\rangle = (-1)(-1)|111\rangle = |111\rangle \quad \checkmark$$
An $X$ error on qubit 1 anticommutes with $Z_1 Z_2$ but commutes with $Z_2 Z_3$, giving syndrome $(1, 0)$. Similarly, an $X$ error on qubit 2 anticommutes with both stabilizers, giving $(1, 1)$.
The syndrome extraction circuit uses ancilla qubits to measure these stabilizers without disturbing the logical state:
q0: ──●───────
│
q1: ──●──●────
│ │
q2: ──┼──●──●─
│ │ │
a0: ──X──┼──┼─ (measures Z1 Z2)
│ │
a1: ─────X──X─ (measures Z2 Z3)
def syndrome_extraction():
"""Extract syndrome for the 3-qubit bit-flip code."""
qr = QuantumRegister(3, 'data')
ar = QuantumRegister(2, 'anc')
cr = ClassicalRegister(2, 'syndrome')
qc = QuantumCircuit(qr, ar, cr)
# Measure Z1 Z2
qc.cx(0, 3)
qc.cx(1, 3)
# Measure Z2 Z3
qc.cx(1, 4)
qc.cx(2, 4)
qc.measure(ar, cr)
return qc
qc_syn = syndrome_extraction()
print(qc_syn.draw('text'))
23.7.4 Error Recovery
Based on the syndrome, we apply a correction:
| Syndrome $(s_1, s_2)$ | Error Detected | Correction |
|---|---|---|
| $(0, 0)$ | None (or triple flip) | None |
| $(1, 0)$ | Bit flip on qubit 0 | $X_0$ |
| $(1, 1)$ | Bit flip on qubit 1 | $X_1$ |
| $(0, 1)$ | Bit flip on qubit 2 | $X_2$ |
Detailed derivation of the syndrome table:
For the state $\alpha|000\rangle + \beta|111\rangle$:
- No error: $Z_1 Z_2$ eigenvalue $= +1$, $Z_2 Z_3$ eigenvalue $= +1$. Syndrome: $(0, 0)$.
- $X$ on qubit 0: state becomes $\alpha|100\rangle + \beta|011\rangle$.
- $Z_1 Z_2$: $(Z_1|100\rangle)Z_2 + (Z_1|011\rangle)Z_2 = (-1)(+1)|100\rangle + (+1)(-1)|011\rangle$ → eigenvalue $-1$.
- $Z_2 Z_3$: $(+1)(+1)|100\rangle + (-1)(-1)|011\rangle$ → eigenvalue $+1$.
-
Syndrome: $(1, 0)$.
-
$X$ on qubit 1: state becomes $\alpha|010\rangle + \beta|101\rangle$.
- $Z_1 Z_2$: $(+1)(-1)|010\rangle + (-1)(+1)|101\rangle$ → eigenvalue $-1$.
- $Z_2 Z_3$: $(-1)(+1)|010\rangle + (+1)(-1)|101\rangle$ → eigenvalue $-1$.
-
Syndrome: $(1, 1)$.
-
$X$ on qubit 2: state becomes $\alpha|001\rangle + \beta|110\rangle$.
- $Z_1 Z_2$: $(+1)(+1)|001\rangle + (-1)(-1)|110\rangle$ → eigenvalue $+1$.
- $Z_2 Z_3$: $(+1)(-1)|001\rangle + (-1)(+1)|110\rangle$ → eigenvalue $-1$.
- Syndrome: $(0, 1)$.
23.7.5 Full Simulation in Qiskit
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.quantum_info import Statevector, state_fidelity
import numpy as np
def bit_flip_code_circuit(bit_flip_position=None):
"""
Full 3-qubit bit-flip code circuit.
bit_flip_position: 0, 1, 2, or None (no error).
"""
data = QuantumRegister(3, 'data')
anc = QuantumRegister(2, 'anc')
syn = ClassicalRegister(2, 'syndrome')
qc = QuantumCircuit(data, anc, syn)
# ── Encode ──
qc.cx(0, 1)
qc.cx(0, 2)
qc.barrier()
# ── Introduce error ──
if bit_flip_position is not None:
qc.x(bit_flip_position)
qc.barrier()
# ── Syndrome extraction ──
qc.cx(0, 3)
qc.cx(1, 3)
qc.cx(1, 4)
qc.cx(2, 4)
qc.measure(anc, syn)
qc.barrier()
# ── Correction (conditioned on syndrome) ──
with qc.if_test((syn, 1)):
qc.x(2)
with qc.if_test((syn, 2)):
qc.x(0)
with qc.if_test((syn, 3)):
qc.x(1)
return qc
# ── Test the code ──
simulator = AerSimulator()
for error_pos in [None, 0, 1, 2]:
qc = QuantumCircuit(3, 2)
qc.h(0)
qc.barrier()
qc_full = qc.compose(bit_flip_code_circuit(error_pos))
result = simulator.run(qc_full, shots=1).result()
counts = result.get_counts()
print(f"Error on qubit {error_pos}: measurement outcome = {counts}")
23.7.6 Why This Code Fails for Phase Errors
The bit-flip code is blind to phase-flip ($Z$) errors. Consider a $Z$ error on qubit 0:
$$Z_0 |\psi_L\rangle = Z_0(\alpha|000\rangle + \beta|111\rangle) = \alpha|000\rangle - \beta|111\rangle.$$
This is a logical phase error—it changes $\alpha \to \alpha$ and $\beta \to -\beta$, which in the logical basis is $\bar{Z}$. The stabilizers $Z_1 Z_2$ and $Z_2 Z_3$ commute with $Z_0$ (since $Z_i Z_j$ commutes with $Z_k$ for any $i, j, k$), so the syndrome is always $(0, 0)$—no error detected!
State evolution with Z error on qubit 0:
α|000⟩ + β|111⟩ ──Z₀──> α|000⟩ - β|111⟩
= (α|0⟩ - β|1⟩) ⊗ |00⟩ (up to normalization)
The logical state has changed, but the syndrome is (0,0)!
In the Hadamard basis:
α|+++⟩ + β|───⟩ ──Z₀──> α|─++⟩ + β|+──⟩
This is a bit-flip error in the Hadamard basis!
The code CANNOT detect it because the stabilizers ZᵢZⱼ
are measuring X-basis correlations, not Z-basis correlations.
This failure motivates the phase-flip code and the Shor code, which we study in Chapter 24.
Recurring Theme: We're at the Beginning. The bit-flip code is a toy—it corrects only one type of error. But it teaches us the fundamental structure of QEC: encode into an entangled state, measure commuting stabilizers to extract a syndrome, and apply a correction. This pattern repeats in every quantum code, from the simple repetition code to the surface code with millions of qubits. The key ideas are already here.
23.8 From Classical to Quantum: The Conceptual Bridge
The table below summarizes the correspondence between classical and quantum error correction:
| Concept | Classical | Quantum |
|---|---|---|
| Encoding | $b \to bbb$ | $|\psi\rangle \to \alpha|000\rangle + \beta|111\rangle$ |
| Error | Bit flip | Pauli $X$, $Y$, $Z$ |
| Parity check | $r_1 \oplus r_2$ | $Z_1 Z_2$ (stabilizer) |
| Syndrome | $H r^T$ | Measurement of stabilizer generators |
| Correction | Flip bit | Apply Pauli operator |
| Information protection | Redundancy | Entanglement + non-locality |
| Error discretization | Inherent (binary) | Achieved by syndrome measurement |
The quantum repetition code for bit flips is a toy model—it cannot correct phase flips, which are often the dominant error. The Shor code (Chapter 24) concatenates the bit-flip and phase-flip codes to correct arbitrary single-qubit errors.
23.8.1 The Road Ahead
What we have learned so far: 1. Classical error correction uses redundancy and syndrome-based decoding. 2. The no-cloning theorem prevents simple copying of quantum states. 3. Measurement collapse is avoided by measuring stabilizers that commute with logical operators. 4. Continuous errors are digitized by the syndrome measurement.
What remains to be learned: 1. How to correct all single-qubit errors (the Shor code, Chapter 24). 2. How to build codes from classical codes (CSS construction, Chapter 24). 3. How to make error correction fault-tolerant (surface codes, Chapter 25). 4. How to implement all of this in hardware (Chapter 26).
23.8.2 Quantum Error Correction as a Linear Algebra Problem
The connection between classical and quantum error correction can be summarized in a single table that maps every classical concept to its quantum analog:
| Classical Concept | Quantum Analog | Mathematical Structure |
|---|---|---|
| Bit flip ($0 \leftrightarrow 1$) | Pauli $X$ error | Unitary operator |
| Parity check ($r_i \oplus r_j$) | Stabilizer measurement ($Z_i Z_j$) | Hermitian operator |
| Syndrome vector ($s = Hr^T$) | Syndrome eigenvalues ($\pm 1$) | Measurement outcome |
| Codeword subspace ($C \subseteq \mathbb{F}_2^n$) | Code subspace ($\mathcal{C} \subseteq \mathbb{C}^{2^n}$) | Hilbert subspace |
| Error pattern ($e \in \mathbb{F}_2^n$) | Error operator ($E \in \mathcal{P}_n$) | Pauli group element |
| Minimum distance ($d$) | Quantum minimum distance ($d$) | Code parameter |
| Generator matrix ($G$) | Encoding circuit | Unitary transformation |
| Parity-check matrix ($H$) | Stabilizer generators | Commuting Pauli operators |
| $C^\perp$ (dual code) | $\mathcal{S}^\perp$ (normalizer) | Pauli group structure |
The key mathematical insight is that both classical and quantum codes are subspaces of a larger space—$\mathbb{F}_2^n$ for classical codes and $\mathbb{C}^{2^n}$ for quantum codes. The difference is that quantum codes must preserve the full complex amplitude structure, not just the classical bit values.
23.8.3 The Price of Quantum Error Correction
Quantum error correction comes at a cost beyond the overhead of physical qubits. The no-cloning theorem means we cannot simply copy quantum information—we must entangle it. This has several consequences:
-
Entanglement overhead: A $[[n, k, d]]$ quantum code uses $n$ physical qubits to encode $k$ logical qubits, giving a rate of $k/n$. The best known codes achieve rates approaching the quantum capacity, but practical codes (like the surface code) have rates $\sim 1/d^2$, which can be very low for large $d$.
-
Syndrome extraction requires ancilla qubits: Each stabilizer measurement needs at least one ancilla qubit, and fault-tolerant extraction may require verified ancilla states.
-
Continuous errors are discretized at the cost of measurement disturbance: The syndrome measurement collapses the state onto a definite error subspace, which is probabilistic. The probabilities of different outcomes depend on the error model and cannot be controlled.
-
Gate overhead: Error-corrected gates require more physical operations than uncorrected ones. A single logical CNOT on surface code qubits requires $O(d)$ surface code cycles, each involving $O(d^2)$ physical operations.
These costs motivate the search for more efficient codes and better hardware. The surface code, despite its low rate, is preferred because of its high threshold and local stabilizer measurements—features that are essential for practical implementation.
23.9 Qiskit Implementation: Full Bit-Flip Code with Noise Simulation
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, pauli_error
import numpy as np
def create_noise_model(p_x, p_z):
"""Create a depolarizing-like noise model with X and Z errors."""
noise_model = NoiseModel()
# Single-qubit gate errors
error_x = pauli_error([('X', p_x), ('Z', p_z), ('I', 1 - p_x - p_z)])
noise_model.add_all_qubit_quantum_error(error_x, ['id'])
return noise_model
def bit_flip_code_with_noise(p_error=0.01, n_shots=10000):
"""
Simulate the 3-qubit bit-flip code under depolarizing noise.
Compares logical error rate with and without error correction.
"""
# ── Without error correction ──
simulator = AerSimulator()
# Unprotected single qubit
qc_raw = QuantumCircuit(1, 1)
qc_raw.h(0) # Prepare |+>
# Apply noise (simulated by adding delay/idle error)
qc_raw.measure(0, 0)
# ── With error correction ──
data = QuantumRegister(3, 'data')
anc = QuantumRegister(2, 'anc')
syn = ClassicalRegister(2, 'syndrome')
result_reg = ClassicalRegister(1, 'result')
qc_ec = QuantumCircuit(data, anc, syn, result_reg)
# Encode |+>
qc_ec.h(0)
qc_ec.cx(0, 1)
qc_ec.cx(0, 2)
qc_ec.barrier()
# Inject random error
for q in range(3):
if np.random.random() < p_error:
if np.random.random() < 0.5:
qc_ec.x(q)
else:
qc_ec.z(q)
qc_ec.barrier()
# Syndrome extraction
qc_ec.cx(0, 3); qc_ec.cx(1, 3)
qc_ec.cx(1, 4); qc_ec.cx(2, 4)
qc_ec.measure(anc, syn)
# Correction
with qc_ec.if_test((syn, 1)):
qc_ec.x(2)
with qc_ec.if_test((syn, 2)):
qc_ec.x(0)
with qc_ec.if_test((syn, 3)):
qc_ec.x(1)
# Decode: measure in X basis
qc_ec.h(0)
qc_ec.measure(0, result_reg)
return qc_ec
# ── Monte Carlo comparison ──
n_trials = 5000
p_values = [0.001, 0.005, 0.01, 0.05, 0.1]
print("Bit-flip code logical error rate vs. physical error rate:")
print(f"{'Physical p':>12} {'Uncoded P_err':>15} {'Coded P_err':>15} {'Improvement':>12}")
print("-" * 60)
for p in p_values:
n_uncoded_errors = 0
n_coded_errors = 0
for _ in range(n_trials):
# Uncoded: single qubit error
if np.random.random() < p:
n_uncoded_errors += 1
# Coded: 3-qubit repetition code
errors = sum(np.random.random() < p for _ in range(3))
if errors >= 2: # Majority vote fails with 2+ errors
n_coded_errors += 1
uncoded_rate = n_uncoded_errors / n_trials
coded_rate = n_coded_errors / n_trials
improvement = uncoded_rate / max(coded_rate, 1e-10)
print(f"{p:12.3f} {uncoded_rate:15.4f} {coded_rate:15.4f} {improvement:12.1f}x")
This simulation demonstrates the quadratic error suppression: the coded error rate is approximately $3p^2$ while the uncoded rate is $p$, giving an improvement factor of roughly $1/(3p)$.
23.10 Shannon's Channel Coding Theorem and the Limits of Error Correction
Shannon's noisy channel coding theorem is the foundational result that guarantees reliable communication is possible over noisy channels. For a binary symmetric channel with flip probability $p$, the channel capacity is:
$$C = 1 - H_2(p) = 1 + p\log_2 p + (1-p)\log_2(1-p),$$
where $H_2(p) = -p\log_2 p - (1-p)\log_2(1-p)$ is the binary entropy function.
Shannon's theorem (informal): For any rate $R < C$, there exists a code with rate $R$ and arbitrarily low error probability. Conversely, for any rate $R > C$, reliable communication is impossible.
Implications for repetition codes: The 3-bit repetition code has rate $R = 1/3$. For $p = 0.1$, $C = 1 - H_2(0.1) \approx 0.531$, so $R = 1/3 < C$—the code operates below capacity, as expected. But the repetition code is grossly suboptimal: codes like LDPC and polar codes can achieve rates much closer to capacity.
Worked Example 7: Compare the 3-bit repetition code with Shannon's capacity at $p = 0.1$.
- Repetition code: $R = 1/3$, $P_{\text{error}} = 3p^2 - 2p^3 = 0.028$.
- Shannon capacity: $C = 1 - H_2(0.1) \approx 0.531$. Any rate below 0.531 is achievable with arbitrarily low error.
- The repetition code uses rate $1/3 \approx 0.333$, which is well below capacity, but its error rate of 0.028 is far from "arbitrarily low." Better codes at the same rate can achieve much lower error rates.
This highlights the key insight: the repetition code is a pedagogical tool, not a practical code. Real-world codes (Hamming, Reed-Solomon, LDPC, polar codes) operate much closer to the Shannon limit.
23.10.1 Comparison of Classical Codes
| Code | Parameters | Rate | Distance | Corrects |
|---|---|---|---|---|
| 3-bit repetition | $[3, 1, 3]$ | $1/3$ | 3 | 1 error |
| 5-bit repetition | $[5, 1, 5]$ | $1/5$ | 5 | 2 errors |
| Hamming | $[7, 4, 3]$ | $4/7$ | 3 | 1 error |
| Extended Hamming | $[8, 4, 4]$ | $1/2$ | 4 | 1 error + detect 2 |
| Golay | $[23, 12, 7]$ | $12/23$ | 7 | 3 errors |
| Reed-Solomon (255,223) | $[255, 223, 33]$ | $223/255$ | 33 | 16 errors |
The Hamming code achieves a much higher rate ($4/7$) than the repetition code ($1/3$) at the same error-correcting capability (1 error), illustrating the power of algebraic coding theory.
23.10.2 From Classical to Quantum: What Carries Over?
The transition from classical to quantum error correction preserves the following structures:
- Syndrome-based decoding: Both classical and quantum codes use syndrome measurements to diagnose errors without revealing the encoded information.
- Linearity: Both classical linear codes and quantum stabilizer codes are defined by linear constraints (parity checks or stabilizer generators).
- Distance and correction capability: The minimum distance determines the number of correctable errors, both classically ($t = \lfloor(d-1)/2\rfloor$) and quantum mechanically.
The key differences are: 1. No cloning: Quantum codes cannot copy information; they must entangle it. 2. Continuous errors: Quantum errors are continuous, but are discretized by syndrome measurement. 3. Measurement disturbance: Classical syndrome measurements are benign; quantum syndrome measurements must be carefully designed not to collapse the state.
23.11 Qiskit Implementation: Classical Code Simulation
import numpy as np
from itertools import product
def simulate_bsc(code_type, p, n_trials=100000):
"""
Simulate a binary symmetric channel with various error-correcting codes.
Args:
code_type: 'uncoded', 'repetition3', 'repetition5', or 'hamming74'
p: bit flip probability
n_trials: number of trials
Returns:
error_rate: fraction of incorrectly decoded messages
"""
errors = 0
for _ in range(n_trials):
bit = np.random.randint(0, 2)
if code_type == 'uncoded':
received = bit ^ (1 if np.random.random() < p else 0)
if received != bit:
errors += 1
elif code_type == 'repetition3':
codeword = [bit, bit, bit]
received = [b ^ (1 if np.random.random() < p else 0) for b in codeword]
decoded = 1 if sum(received) > 1 else 0
if decoded != bit:
errors += 1
elif code_type == 'repetition5':
codeword = [bit] * 5
received = [b ^ (1 if np.random.random() < p else 0) for b in codeword]
decoded = 1 if sum(received) > 2 else 0
if decoded != bit:
errors += 1
elif code_type == 'hamming74':
# Encode using Hamming [7,4,3]
from numpy import array
G = array([[1,1,0,1,0,0,0],
[0,1,1,0,1,0,0],
[1,1,1,0,0,1,0],
[1,0,1,0,0,0,1]], dtype=int)
# Random 4-bit message
msg = np.random.randint(0, 2, 4)
codeword = (msg @ G) % 2
# Flip bits
noise = (np.random.random(7) < p).astype(int)
received = (codeword + noise) % 2
# Syndrome decode
H = array([[1,0,1,0,1,0,1],
[0,1,1,0,0,1,1],
[0,0,0,1,1,1,1]], dtype=int)
syndrome = (H @ received) % 2
error_pos = syndrome[0]*1 + syndrome[1]*2 + syndrome[2]*4
if error_pos > 0:
received[error_pos-1] ^= 1
decoded = array([received[2], received[4], received[5], received[6]])
if not np.array_equal(msg, decoded):
errors += 1
return errors / n_trials
# ── Compare codes ──
print("Error Rate Comparison on Binary Symmetric Channel")
print("=" * 65)
print(f"{'p':>8} {'Uncoded':>10} {'Rep(3)':>10} {'Rep(5)':>10} {'Hamming':>10}")
print("-" * 65)
for p in [0.01, 0.05, 0.1, 0.2, 0.3]:
uncoded = simulate_bsc('uncoded', p, n_trials=50000)
rep3 = simulate_bsc('repetition3', p, n_trials=50000)
rep5 = simulate_bsc('repetition5', p, n_trials=50000)
hamming = simulate_bsc('hamming74', p, n_trials=50000)
print(f"{p:8.3f} {uncoded:10.6f} {rep3:10.6f} {rep5:10.6f} {hamming:10.6f}")
This simulation demonstrates several key points: 1. All codes improve over uncoded transmission for small $p$. 2. The Hamming code achieves better error rates than the repetition code at the same rate. 3. The 5-bit repetition code outperforms the 3-bit code at the cost of lower rate. 4. For $p > 0.5$, the repetition codes become worse than uncoded transmission.