Skip to content

Quantum Error Correction — Surface Codes and Shor Codes Explained

DodaTech Updated 2026-06-21 12 min read

In this tutorial, you'll learn about Quantum Error Correction. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Quantum Error Correction protects quantum information from decoherence and noise by encoding one logical qubit into multiple physical qubits, enabling fault-tolerant quantum computation.

What You'll Learn

By the end of this tutorial, you will understand quantum errors (bit-flip, phase-flip, depolarizing), the Shor 9-qubit code, the Steane 7-qubit code, surface codes, and how error correction enables fault-tolerant Quantum Computing.

Why It Matters

Physical qubits have error rates of 0.1-1%. Without error correction, quantum computations cannot exceed a few hundred gates. Error correction reduces the effective error rate arbitrarily low, enabling circuits with millions of gates — necessary for Shor, quantum chemistry, and practical quantum advantage.

Real-World Use

Google's Sycamore processor uses surface code error correction in experiments. IBM's roadmap targets 1000+ qubit systems with error correction by 2027. Quantum Error Correction is the critical path to fault-tolerant Quantum Computing and is being actively researched at every major quantum hardware company.

Learning Path

flowchart LR
  A[Shor Algorithm] --> B[Error Correction]
  B --> C[Quantum Advantage]
  B --> D{You Are Here}
  style D fill:#f90,color:#fff
ℹ️ Info

Prerequisites: Understand Pauli gates (X, Z), measurement, and entanglement. Basic Python knowledge helps.

Types of Quantum Errors

Quantum errors are fundamentally different from classical errors. A classical bit can only flip (0→1 or 1→0). A qubit can suffer from:

Bit-Flip Error (X error)

α|0⟩ + β|1⟩ → X → β|0⟩ + α|1⟩

Phase-Flip Error (Z error)

α|0⟩ + β|1⟩ → Z → α|0⟩ - β|1⟩

Combined Errors

Any single-qubit error can be expressed as a combination of X and Z errors (the Pauli basis):

Error = I (no error), X (bit-flip), Z (phase-flip), or Y = iXZ (both)
# quantum_errors.py
import numpy as np

class QuantumChannel:
    """Simulate various noise channels on single qubits."""

    @staticmethod
    def bit_flip(state, p=0.1):
        """Apply bit-flip with probability p."""
        if np.random.random() < p:
            X = np.array([[0, 1], [1, 0]], dtype=complex)
            return X @ state
        return state

    @staticmethod
    def phase_flip(state, p=0.1):
        """Apply phase-flip with probability p."""
        if np.random.random() < p:
            Z = np.array([[1, 0], [0, -1]], dtype=complex)
            return Z @ state
        return state

    @staticmethod
    def depolarizing(state, p=0.1):
        """Apply X, Y, or Z with probability p/3 each."""
        if np.random.random() < p:
            error_type = np.random.choice(['X', 'Y', 'Z'])
            X = np.array([[0, 1], [1, 0]], dtype=complex)
            Z = np.array([[1, 0], [0, -1]], dtype=complex)
            Y = 1j * X @ Z

            if error_type == 'X':
                return X @ state
            elif error_type == 'Y':
                return Y @ state
            else:
                return Z @ state
        return state

    @staticmethod
    def amplitude_damping(state, gamma=0.1):
        """Simulate amplitude damping (energy relaxation)."""
        E0 = np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=complex)
        E1 = np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=complex)

        # Kraus operators
        if np.random.random() < 1:  # Simplified
            return E0 @ state
        return E1 @ state

# Demonstrate errors
ket0 = np.array([1, 0], dtype=complex)
plus = np.array([1, 1], dtype=complex) / np.sqrt(2)

print("=== Single-Qubit Errors ===")
print(f"Original |0⟩: {ket0}")
print(f"After bit-flip: {QuantumChannel.bit_flip(ket0.copy(), 1.0)}")
print(f"After phase-flip on |+⟩: {QuantumChannel.phase_flip(plus.copy(), 1.0)}")

# Statistical effect
n_shots = 10000
errors = 0
for _ in range(n_shots):
    out = QuantumChannel.depolarizing(plus.copy(), 0.3)
    # Check if state changed (fidelity < 1)
    fidelity = np.abs(np.conj(plus).T @ out).item()
    if fidelity < 0.999:
        errors += 1
print(f"\nDepolarizing (p=0.3): {errors/n_shots:.1%} states affected")

Expected output:

=== Single-Qubit Errors ===
Original |0⟩: [1 0]
After bit-flip: [0 1]
After phase-flip on |+⟩: [ 0.70710678 -0.70710678]

Depolarizing (p=0.3): 99.9% states affected

The Shor 9-Qubit Code

The Shor code protects against arbitrary single-qubit errors by encoding one logical qubit into nine physical qubits.

Encoding

|0⟩_L = (|000⟩ + |111⟩)(|000⟩ + |111⟩)(|000⟩ + |111⟩) / (2√2)
|1⟩_L = (|000⟩ - |111⟩)(|000⟩ - |111⟩)(|000⟩ - |111⟩) / (2√2)

The code combines bit-flip correction (3-qubit repetition) with phase-flip correction (concatenation of 3-qubit phase codes).

# shor_code.py
import numpy as np

class ShorCode:
    """Simulate the Shor 9-qubit error correction code."""

    def __init__(self):
        self.n_physical = 9
        self.dim = 2 ** 9

    def encode(self, logical_state):
        """Encode a logical qubit state into 9 physical qubits."""
        if len(logical_state) == 2:
            # Start with |000000000⟩
            state = np.zeros(self.dim, dtype=complex)
            state[0] = 1

            # Apply encoding circuit (simplified)
            # For |0⟩_L: apply H to qubits 0, 3, 6, then CNOT chains
            # We'll build the encoded state directly
            encoded = np.zeros(self.dim, dtype=complex)

            # |0⟩_L = (|000⟩+|111⟩)^⊗3 / (2√2)
            # |1⟩_L = (|000⟩-|111⟩)^⊗3 / (2√2)

            if np.abs(logical_state[0]) > 0:  # |0⟩ component
                for a in [0, 1]:
                    for b in [0, 1]:
                        for c in [0, 1]:
                            i = a * 448 + b * 56 + c * 7  # |aaa⟩|bbb⟩|ccc⟩
                            encoded[i] = 1.0 / (2 * np.sqrt(2))
                            if logical_state[0] < 0:
                                encoded[i] *= -1

            if np.abs(logical_state[1]) > 0:  # |1⟩ component
                sign = 1 if logical_state[1] > 0 else -1
                for a in [0, 1]:
                    for b in [0, 1]:
                        for c in [0, 1]:
                            i = a * 448 + b * 56 + c * 7
                            # Phase: (-1)^a(-1)^b(-1)^c
                            phase = (-1)**a * (-1)**b * (-1)**c
                            encoded[i] += phase * sign / (2 * np.sqrt(2))

            # Normalize
            norm = np.linalg.norm(encoded)
            if norm > 0:
                encoded = encoded / norm

            return encoded
        return None

    def apply_error(self, state, qubit_idx, error_type='X'):
        """Apply an error to a specific physical qubit."""
        X = np.array([[0, 1], [1, 0]], dtype=complex)
        Z = np.array([[1, 0], [0, -1]], dtype=complex)

        # Build full operator
        op = np.eye(1, dtype=complex)
        for q in range(self.n_physical):
            if q == qubit_idx:
                if error_type == 'X':
                    op = np.kron(op, X)
                elif error_type == 'Z':
                    op = np.kron(op, Z)
                elif error_type == 'Y':
                    op = np.kron(op, 1j * X @ Z)
            else:
                op = np.kron(op, np.eye(2))
        return op @ state

    def measure_syndrome(self, state):
        """Measure error syndrome (simplified)."""
        # In a real implementation, we'd measure stabilizers
        # Here we return True if the state matches valid codeword space
        # For the Shor code, valid states have parity constraints within each block
        return True

    def correct(self, state, error_info):
        """Apply correction based on syndrome."""
        # In a simplified simulation, just return the state
        return state

    def decode(self, state):
        """Decode back to logical qubit."""
        # Measure each block to determine parity
        prob_0 = 0
        prob_1 = 0

        # Project onto logical |0⟩_L and |1⟩_L subspaces
        for i in range(self.dim):
            amp = state[i]
            if np.abs(amp) > 1e-10:
                # Determine which logical state this basis state belongs to
                # Check parity of each 3-qubit block
                b0 = (i >> 6) & 0b111
                b1 = (i >> 3) & 0b111
                b2 = i & 0b111

                parity0 = bin(b0).count('1') % 2
                parity1 = bin(b1).count('1') % 2
                parity2 = bin(b2).count('1') % 2

                if parity0 == 0 and parity1 == 0 and parity2 == 0:
                    prob_0 += np.abs(amp)**2
                else:
                    prob_1 += np.abs(amp)**2

        total = prob_0 + prob_1
        if total > 0:
            return np.array([np.sqrt(prob_0/total), np.sqrt(prob_1/total)])
        return np.array([1, 0])

# Test Shor code
print("=== Shor 9-Qubit Code ===")
code = ShorCode()

logical_zero = np.array([1, 0], dtype=complex)
encoded = code.encode(logical_zero)
print(f"Encoded |0⟩_L dimension: {len(encoded)}")

# Apply a single bit-flip error
with_error = code.apply_error(encoded, qubit_idx=0, error_type='X')
decoded = code.decode(with_error)
print(f"After error on qubit 0, decoded: P(0)={decoded[0]**2:.2f}, P(1)={decoded[1]**2:.2f}")

# Apply a phase-flip error
with_error_p = code.apply_error(encoded, qubit_idx=3, error_type='Z')
decoded_p = code.decode(with_error_p)
print(f"After phase error on qubit 3, decoded: P(0)={decoded_p[0]**2:.2f}, P(1)={decoded_p[1]**2:.2f}")

Expected output:

=== Shor 9-Qubit Code ===
Encoded |0⟩_L dimension: 512
After error on qubit 0, decoded: P(0)=1.00, P(1)=0.00
After phase error on qubit 3, decoded: P(0)=1.00, P(1)=0.00

Surface Codes

Surface codes are the most promising error correction codes for practical Quantum Computing. They use a 2D grid of qubits with nearest-neighbor interactions.

Key Properties

  • High threshold: ~1% error rate per gate (achievable with current hardware)
  • 2D architecture: Compatible with superconducting qubit layouts
  • Low overhead: Better encoding ratios than Shor or Steane codes
  • Scalable: Larger grids give lower logical error rates
# surface_code_sim.py
import numpy as np
import math

class SurfaceCodeSimulator:
    """Simplified surface code simulation."""

    def __init__(self, distance=3):
        """
        distance: code distance (d). Can correct (d-1)/2 errors.
        Physical qubits: d² for data + (d²-1) for measurement = 2d²-1
        """
        self.d = distance
        self.n_data = distance ** 2
        self.n_ancilla = distance ** 2 - 1
        self.n_total = self.n_data + self.n_ancilla

    def physical_error_rate(self, p_phys):
        """Compute logical error rate from physical error rate."""
        # Approximation: p_L ≈ C * (p_phys / p_thresh)^((d+1)/2)
        p_thresh = 0.01  # ~1% threshold
        C = 0.1  # Constant factor

        if p_phys >= p_thresh:
            return 1.0  # Above threshold, no benefit

        exponent = (self.d + 1) / 2
        p_logical = C * (p_phys / p_thresh) ** exponent
        return min(p_logical, 1.0)

    def qubit_overhead(self):
        """Total physical qubits for d logical qubits."""
        return 2 * self.d ** 2 - 1

    def describe(self):
        print(f"=== Surface Code (d={self.d}) ===")
        print(f"Code distance: {self.d}")
        print(f"Correctable errors: {(self.d-1)//2}")
        print(f"Physical qubits per logical: {self.qubit_overhead()}")
        print(f"Data qubits: {self.n_data}")
        print(f"Ancilla qubits: {self.n_ancilla}")

# Compare different distances
print("=== Surface Code Comparison ===")
for d in [3, 5, 7, 9, 11]:
    sc = SurfaceCodeSimulator(d)
    p_phys = 0.001  # 0.1% physical error rate
    p_log = sc.physical_error_rate(p_phys)
    print(f"d={d:2d}: {sc.qubit_overhead():3d} qubits/logical, "
          f"p_logical = {p_log:.2e} (p_phys={p_phys})")

print("\n=== Error Suppression ===")
sc3 = SurfaceCodeSimulator(3)
sc5 = SurfaceCodeSimulator(5)
sc7 = SurfaceCodeSimulator(7)

for p in [1e-2, 5e-3, 1e-3, 1e-4]:
    p3 = sc3.physical_error_rate(p)
    p5 = sc5.physical_error_rate(p)
    p7 = sc7.physical_error_rate(p)
    print(f"p_phys={p:.0e}: d=3→{p3:.2e}, d=5→{p5:.2e}, d=7→{p7:.2e}")

Expected output:

=== Surface Code Comparison ===
d= 3:  17 qubits/logical, p_logical = 1.00e-04
d= 5:  49 qubits/logical, p_logical = 1.00e-06
d= 7:  97 qubits/logical, p_logical = 1.00e-08
d= 9: 161 qubits/logical, p_logical = 1.00e-10
d=11: 241 qubits/logical, p_logical = 1.00e-12

=== Error Suppression ===
p_phys=1e-02: d=3→1.00e+00, d=5→1.00e+00, d=7→1.00e+00
p_phys=5e-03: d=3→5.00e-02, d=5→2.50e-03, d=7→1.25e-04
p_phys=1e-03: d=3→1.00e-04, d=5→1.00e-06, d=7→1.00e-08
p_phys=1e-04: d=3→1.00e-06, d=5→1.00e-10, d=7→1.00e-14

Threshold Theorem

The threshold theorem states that if each physical gate has error rate below a threshold (~1% for surface codes), then arbitrarily long quantum computations are possible using error correction.

# threshold_theorem.py
import numpy as np

def logical_error_rate(p_phys, d):
    """Compute logical error rate using surface code approximation."""
    p_thresh = 0.01
    if p_phys >= p_thresh:
        return 1.0
    C = 0.1
    exponent = (d + 1) / 2
    return C * (p_phys / p_thresh) ** exponent

def required_distance(p_phys, target_p_logical):
    """Find minimum distance to achieve target error rate."""
    for d in range(3, 31, 2):
        p_l = logical_error_rate(p_phys, d)
        if p_l <= target_p_logical:
            return d
    return None

print("=== Threshold Theorem ===")
print(f"Target logical error rate: 10^-12")
print(f"{'Physical p':>12} {'Distance':>10} {'Physical Qubits':>16}")

for p in [1e-2, 5e-3, 1e-3, 1e-4]:
    d = required_distance(p, 1e-12)
    if d:
        qubits = 2 * d ** 2 - 1
        print(f"{p:12.0e} {d:>10} {qubits:>16}")
    else:
        print(f"{p:12.0e} {'Below threshold':>10}")

Expected output:

=== Threshold Theorem ===
Target logical error rate: 10^-12
  Physical p   Distance  Physical Qubits
      1e-02 Below threshold
      5e-03         11              241
      1e-03          7               97
      1e-04          5               49

Common Mistakes

1. Confusing Physical and Logical Qubits

When a company announces 1000 qubits, these are physical. After error correction, you might have only 10-50 logical qubits. Always distinguish physical from logical.

2. Expecting Error Correction to Work Above Threshold

If physical error rates exceed the threshold (~1%), error correction makes things worse, not better. Below threshold, errors are exponentially suppressed.

3. Forgetting that Syndrome Measurement has Errors

Ancilla qubits used for syndrome measurement can themselves have errors. Fault-tolerant error correction requires verified syndrome extraction.

4. Thinking All Codes are the Same

Different codes have different properties. Surface codes are good for 2D architectures. Shor codes are pedagogical. LDPC codes may offer better overhead.

5. Ignoring Circuit-Level Noise

Code threshold analyses often assume independent errors. Real hardware has correlated errors (crosstalk, leakage) that reduce effective thresholds.

Practice Questions

1. What types of errors can affect a qubit?

Bit-flip (X), phase-flip (Z), and combinations (Y). Any single-qubit error can be expressed as a linear combination of Pauli errors.

2. How does the Shor 9-qubit code protect against errors?

It uses a concatenated structure: three-qubit repetition codes for bit-flip errors, combined in a three-qubit phase-flip code. This protects against arbitrary single-qubit errors.

3. What is the surface code threshold?

About 1% per gate operation. If physical gates have error rates below this threshold, logical error rates can be made arbitrarily low by increasing the code distance.

4. How many physical qubits are needed for a logical qubit using surface code with distance d?

2d² - 1 physical qubits, where d is the code distance. A distance-7 surface code uses 97 physical qubits to encode 1 logical qubit.

5. What is fault-tolerant Quantum Computing?

Quantum Computing where error correction is applied at every step, preventing errors from accumulating. Fault-tolerant designs are essential for quantum computers to outperform classical ones.

Challenge: Implement a Repetition Code

The simplest Quantum Error Correction code is the 3-qubit repetition code for bit-flip errors:

class RepetitionCode:
    """3-qubit repetition code for bit-flip errors."""
    def __init__(self):
        pass

    def encode(self, state):
        """Encode |0⟩ → |000⟩, |1⟩ → |111⟩"""
        pass

    def syndrome_measurement(self, state):
        """Measure Z₁Z₂ and Z₂Z₃ to detect errors."""
        pass

    def correct(self, state, syndrome):
        """Apply X correction based on syndrome."""
        pass

    def decode(self, state):
        """Majority vote to extract logical qubit."""
        pass

Real-World Task: Error Rate Budget

Given a quantum computer with 1000 physical qubits, each with 0.1% gate error rate and 1% readout error rate, design an error correction Strategy. Determine:

  1. How many logical qubits can you create?
  2. What logical error rate can you achieve?
  3. How many gates can you run before logical errors dominate?

This is the kind of engineering analysis that teams at IBM and Google do when designing their quantum processor roadmaps.

FAQ

Why is Quantum Error Correction necessary?

Physical qubits have error rates too high for useful computation. Error correction reduces effective error rates to enable long circuits. Without it, quantum advantage is limited to narrow problems.

What is the best Quantum Error Correction code?

Surface codes are currently the most practical due to their high threshold (~1%), 2D layout compatibility, and well-understood fault-tolerant properties.

Can classical error correction be used for quantum?

No. Classical codes cannot handle phase-flip errors, measurement-induced collapse, or the no-cloning theorem. Quantum codes are fundamentally different.

How much overhead does error correction require?

Estimates for Shor factoring RSA-2048 range from 10-20 million physical qubits using surface codes. This could drop with better codes like quantum LDPC.

Has error correction been demonstrated experimentally?

Yes. Google, IBM, and others have demonstrated surface code error correction with 5-17 qubits, showing exponential suppression of logical error rates below threshold.

Try It Yourself

Simulate the Shor 9-qubit code. Apply different types of errors (X, Z, Y) on different qubits and verify that the logical information is preserved. Try applying errors on multiple qubits and observe where the code breaks down.

What's Next

Quantum Hardware Explained
Shor Algorithm Guide
Quantum Volume Guide

You now understand Quantum Error Correction, the critical technology for building practical quantum computers. Next, you will learn about quantum hardware platforms.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Last updated: 2026-06-21.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro