35 min read

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...

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:

  1. 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$.

  2. Syndrome extraction requires ancilla qubits: Each stabilizer measurement needs at least one ancilla qubit, and fault-tolerant extraction may require verified ancilla states.

  3. 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.

  4. 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:

  1. Syndrome-based decoding: Both classical and quantum codes use syndrome measurements to diagnose errors without revealing the encoded information.
  2. Linearity: Both classical linear codes and quantum stabilizer codes are defined by linear constraints (parity checks or stabilizer generators).
  3. 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.