Channel Capacity & Error-Correcting Codes

Shannon's second theorem: every noisy channel has a speed limit, and below it errors can be made as rare as you like.

Channels

BSC, BEC, Z-channel and AWGN. Mutual information as the rate.

Capacity

C = max I(X;Y). The coding theorem and its converse, with proof sketches.

Classic codes

Repetition, Hamming(7,4), Reed–Solomon, parity and CRC, in runnable Python.

Near the limit

LDPC, turbo and polar codes. Shannon–Hartley with Wi-Fi and 5G numbers.

Teaching excerpts may use definitions introduced in this deck. The complete standalone lab is included below; its displayed output and checks were reproduced during this review.

Start with a concrete question

How does structured redundancy correct a single flipped bit?

Your goal

Understand the data, the rule, and the required result. Then run the complete Python program and modify one thing at a time.

No companion presentation is required. Python syntax and any mathematical notation are explained as they are introduced.

Vocabulary you need before the algorithm

XOR

Addition modulo 2.

Codeword

The encoded data plus parity bits.

Syndrome

Parity-check mismatches identifying a candidate error.

Minimum distance

The fewest positions separating two codewords.

Work one example before writing code

0110 encodes as 0110110. Flipping position 3 produces 0100110; syndrome 011 points to column 3 of H, restoring the original.

Trace each step and name what changed. Ask which fact stayed true and what should happen on an empty, invalid, or boundary input.

Roadmap WHERE WE GO

  1. Shannon's surprising second theorem
  2. The channel model
  3. BSC, BEC and the Z-channel
  4. Mutual information as a rate
  5. Capacity and Blahut–Arimoto
  6. BSC capacity 1 − h(p)
  7. BEC capacity 1 − ε, Z-channel
  8. The noisy-channel coding theorem
  9. Achievability by random coding
  10. The converse via Fano's inequality
  11. Repetition codes: the naive trade-off
  12. Hamming distance, detect vs correct
  13. Linear codes, Hamming(7,4), syndromes
  14. Hamming bound, perfect codes
  15. Singleton bound, Reed–Solomon
  16. Parity and CRC
  17. LDPC, turbo and polar codes
  18. AWGN and Shannon–Hartley; pitfalls; summary

The surprise of 1948 HISTORY

Before Shannon, engineers believed a simple rule: to get fewer errors, send slower. Repeat each bit more times, and errors drop, but the rate drops to zero with them.

Shannon's A Mathematical Theory of Communication (1948) said this is wrong.

The claim. Every channel has a number C, its capacity. For any rate R < C, there are codes with error probability as small as you like. For R > C, no code works.

The proof was not constructive. It showed that a random code works, but gave no practical way to decode one. Closing that gap took about 50 years.

YearMilestone
1948Shannon: capacity, coding theorem, AWGN formula
1950Hamming codes (born from weekend jobs failing on relay computers)
1954Reed–Muller codes
1960Reed–Solomon codes; Gallager's LDPC thesis (published 1963)
1961Peterson: cyclic codes and CRCs
1967Viterbi algorithm for convolutional codes
1993Turbo codes: within 0.5 dB of the limit
1996MacKay & Neal rediscover LDPC codes
2009Arıkan: polar codes, provably reach capacity
20165G picks LDPC (data) and polar (control)

The communication model MODEL

Compare rates in compatible units: source-symbols per channel-use × source entropy must be below channel capacity under the source/channel theorem's assumptions.

Source Encoder Channelp(y | x) Decoder Destination noise W ∈ {1..M}Xⁿ = xⁿ(W) YⁿŴ = g(Yⁿ) rate R = (log₂ M) / n bits per channel use  ·  error when Ŵ ≠ W

Source coding (deck 1)

Remove redundancy. Squeeze the message to H bits per symbol. Output looks like fair coin flips.

Channel coding (this deck)

Add structured redundancy. n channel uses carry nR message bits, so the decoder can undo the noise.

Separation theorem

For the standard separation theorem, compare source entropy times the source-symbol/channel-use ratio with capacity. H<C is the special case of one source symbol per channel use, under the stated asymptotic source/channel assumptions.

Discrete memoryless channels DEFINITIONS

DMC. Input alphabet 𝒳, output alphabet 𝒴, transition matrix W(y|x) = P(Y=y | X=x). Memoryless: p(yn|xn) = ∏i W(yi|xi). Each use is independent noise.
BSC(p) 0 1 0 1 1−p1−p pp

Binary symmetric: each bit flips with probability p. Model for thermal noise after hard decisions.

BEC(ε) 0 1 0 ? 1 1−ε1−ε εε

Binary erasure: a bit is lost with probability ε, and you know it is lost. Model for dropped packets.

Z(q) 0 1 0 1 11−q q

Z-channel: 0 is always safe, 1 can decay to 0. Models some optical links and memory cells that leak charge.

Mutual information is the rate KEY IDEA

Send X with distribution p(x). The receiver sees Y. How much did it learn?

I(X;Y) = H(X) − H(X|Y) = H(Y) − H(Y|X)

  • H(X): the doubt before receiving.
  • H(X|Y): the doubt left after. Noise shows up here as equivocation.
  • The difference is what got through, in bits per channel use.

Note that I(X;Y) depends on both the channel W (fixed by physics) and the input distribution p(x) (our choice).

On BSC(0.1) a uniform input gets 0.531 bits through per use. A skewed input gets less. Capacity is the best we can do by choosing p(x).

Teaching excerpt · complete runnable lab available from “Code”
def mutual_info(px, W):
    """px: input distribution, W[x][y] = P(y | x). Returns I(X;Y) in bits."""
    ny = len(W[0])
    py = [sum(px[x] * W[x][y] for x in range(len(px))) for y in range(ny)]
    return sum(px[x] * W[x][y] * math.log2(W[x][y] / py[y])
               for x in range(len(px)) for y in range(ny)
               if px[x] > 0 and W[x][y] > 0)

BSC = lambda p: [[1 - p, p], [p, 1 - p]]
BEC = lambda e: [[1 - e, e, 0], [0, e, 1 - e]]      # outputs 0, ?, 1
Z   = lambda p: [[1, 0], [p, 1 - p]]                # 1 flips to 0 w.p. p
print(f"BSC(0.1), uniform input: {mutual_info([0.5, 0.5], BSC(0.1)):.4f}")
print(f"BSC(0.1), px=[0.8,0.2]:  {mutual_info([0.8, 0.2], BSC(0.1)):.4f}")
print(f"BEC(0.3), uniform input: {mutual_info([0.5, 0.5], BEC(0.3)):.4f}")
Example output
BSC(0.1), uniform input: 0.5310
BSC(0.1), px=[0.8,0.2]:  0.3578
BEC(0.3), uniform input: 0.7000

Channel capacity DEFINITION

Capacity of a DMC with transition matrix W:

C = maxp(x) I(X;Y)   bits per channel use.

  • 0 ≤ C ≤ min(log|𝒳|, log|𝒴|).
  • I(X;Y) is concave in p(x), so any local max is the global max. The problem is a convex program.
  • For symmetric channels the uniform input is optimal.
Blahut–Arimoto (1972). Repeat: compute p(y). For each input compute d(x) = D(W(·|x) ‖ p(y)). Update p(x) ← p(x) 2d(x), then normalize. Inputs that "stand out" in the output get more weight.

This is the same alternating-maximization idea as EM.

Teaching excerpt · complete runnable lab available from “Code”
def capacity(W, iters=2000):
    """Blahut-Arimoto: maximize I(X;Y) over input distributions."""
    nx, ny = len(W), len(W[0])
    px = [1 / nx] * nx
    for _ in range(iters):
        py = [sum(px[x] * W[x][y] for x in range(nx)) for y in range(ny)]
        d = [sum(W[x][y] * math.log2(W[x][y] / py[y]) for y in range(ny) if W[x][y] > 0)
             for x in range(nx)]
        w = [px[x] * 2 ** d[x] for x in range(nx)]
        px = [v / sum(w) for v in w]
    return mutual_info(px, W), px

def z_capacity(q):                     # closed form for the Z-channel
    return math.log2(1 + (1 - q) * q ** (q / (1 - q)))

for name, W, exact in [("BSC(0.11)", BSC(0.11), 1 - h2(0.11)),
                       ("BEC(0.3)", BEC(0.3), 1 - 0.3),
                       ("Z(0.5)", Z(0.5), z_capacity(0.5))]:
    C, px = capacity(W)
    print(f"{name:9}: C={C:.4f}  exact={exact:.4f}  best px=[{px[0]:.3f}, {px[1]:.3f}]")
Example output
BSC(0.11): C=0.5001  exact=0.5001  best px=[0.500, 0.500]
BEC(0.3) : C=0.7000  exact=0.7000  best px=[0.500, 0.500]
Z(0.5)   : C=0.3219  exact=0.3219  best px=[0.600, 0.400]

BSC capacity: C = 1 − h(p) DERIVATION

I(X;Y) = H(Y) − H(Y|X)
    = H(Y) − ∑x p(x) H(Y | X=x)
    = H(Y) − ∑x p(x) h(p)
    = H(Y) − h(p)
    ≤ 1 − h(p).

Given X, the output is just "flipped or not", so H(Y|X) = h(p) whatever we send. Y is binary, so H(Y) ≤ 1. A uniform input makes Y uniform and reaches it. ∎

Teaching excerpt · complete runnable lab available from “Code”
for p in [0.0, 0.01, 0.05, 0.11, 0.2, 0.5]:
    print(f"p={p:<5} C = 1 - h(p) = {1 - h2(p):.4f}")
Example output
p=0.0   C = 1 - h(p) = 1.0000
p=0.01  C = 1 - h(p) = 0.9192
p=0.05  C = 1 - h(p) = 0.7136
p=0.11  C = 1 - h(p) = 0.5001
p=0.2   C = 1 - h(p) = 0.2781
p=0.5   C = 1 - h(p) = 0.0000
00.51 10.5 p = 0.11: C ≈ 0.5 p = 1/2: C = 0 p C(p)
  • At p = 1/2 the output is independent of the input: nothing gets through.
  • p = 1 is perfect: just flip every bit back.
  • At 11% bit flips you can still send half a bit per use, reliably.

Worked example: BSC capacity by hand STEP BY STEP

Question. A link flips 11% of its bits at random. How many real data bits can each channel use carry?

  1. Step 1. Binary entropy of the noise:

    h(p) = −p log2 p − (1−p) log2(1−p)

  2. Step 2. First term: log2 0.11 ≈ −3.184, so −0.11 × −3.184 ≈ 0.350.
  3. Step 3. Second term: log2 0.89 ≈ −0.168, so −0.89 × −0.168 ≈ 0.150.
  4. Step 4. Add: h(0.11) ≈ 0.500 bits of doubt per received bit.
  5. Step 5. C = 1 − h(p) ≈ 0.500 bits per use.

What it means

A 1 Mbit/s link with 11% flips carries at most 0.5 Mbit/s of data. A good code spends the other half on redundancy. With long blocks, errors can then be made as rare as you like.

Compare with a simple code

At p = 0.1, C = 1 − h(0.1) ≈ 1 − 0.469 = 0.531. Repeat each bit 3 times (rate 1/3) and take a majority vote. It still fails when 2 or 3 copies flip: 3(0.1)2(0.9) + 0.13 = 0.028. Capacity says rate 0.53 with near-zero error is possible.

Common mistake

"More noise, less capacity" holds only up to p = 0.5. A BSC with p = 0.9 has the same capacity as p = 0.1: flip every bit back first.

BEC capacity 1 − ε, and the Z-channel DERIVATION

Binary erasure channel

Let E be the event "erased". It is independent of X and can be read off Y.

H(X|Y) = P(E)·H(X|E) + P(not E)·0
           = ε H(X)
I(X;Y) = H(X) − εH(X) = (1−ε) H(X) ≤ 1 − ε.

Equality at uniform input. ∎

Intuition: a fraction ε of bits vanish. Even a genie who tells the sender which bits will be lost could not do better than 1 − ε. Shannon says you can match the genie without feedback.

Compare: BEC(0.11) has capacity 0.89. BSC(0.11) has 0.5. Knowing where errors are is worth a lot.

Z-channel

A 1 turns into 0 with probability q. Let α = P(X=1). Then

I = h(α(1−q)) − α h(q).

Set the derivative in α to zero and simplify:

C = log2(1 + (1−q) qq/(1−q))

For q = 0.5: C = log2 1.25 ≈ 0.3219, reached at α = 0.4, matching Blahut–Arimoto's [0.600, 0.400].

The best input is not uniform: we send the fragile symbol 1 less often. For asymmetric channels, the input distribution is part of the design.

The noisy-channel coding theorem THEOREM

An (M, n) code has a message set {1,…,M}, an encoder xn(w), and a decoder g(yn). Its rate is R = (log2 M)/n. Its maximal error is λ(n) = maxw P(g(Yn) ≠ w | W = w). A rate R is achievable if there are (⌈2nR⌉, n) codes with λ(n) → 0.
Theorem (Shannon 1948). For a DMC with capacity C:
  1. Achievability: every rate R < C is achievable.
  2. Converse: any sequence of codes with error → 0 has R ≤ C.

Two surprises in one line:

  • Rate and reliability are not at war. Fix any R < C. Longer blocks give lower error, at the same rate.
  • The operational limit equals an information quantity. "Best achievable rate" is a question about all codes. max I(X;Y) is a formula. They are equal.

Stronger forms:

  • Error exponent: the error falls like 2−nE(R) with E(R) > 0 for R < C.
  • Strong converse (Wolfowitz 1957): above C the error goes to 1, not just away from 0.

Capacity is a hard wall with a gentle slope in front of it. Engineering since 1948 has been about getting close to the wall at practical cost.

Achievability: the random-coding argument PROOF SKETCH

  1. Random codebook. Pick p(x) that achieves C. Draw M = 2nR codewords, each symbol i.i.d. from p(x).
  2. Joint typicality decoding. On receiving yn, output the unique w with (xn(w), yn) jointly typical. If none or many, declare error.
  3. Right codeword fails? By the joint AEP, the true pair is typical with probability → 1.
  4. A wrong codeword passes? A wrong xn is independent of yn. It looks jointly typical with probability about 2−nI(X;Y).
  5. Union bound over the 2nR wrong words: P(err) ≤ δ + 2nR 2−n(C−3ε) → 0 if R < C − 3ε.
  6. Averaging. The average random code is good, so some fixed code is good. Throw away the worst half of its codewords to make the max error small too.
typical outputs: about 2^{nH(Y)} each codeword's noise cloud: about 2^{nH(Y|X)} clouds that fit: 2^{n(H(Y)−H(Y|X))} = 2^{nI(X;Y)}

Sphere-packing picture: each codeword's noisy outputs form a cloud. We can fit about 2nI non-overlapping clouds into the output space, so about nI bits per block.

Random codes really work SIMULATION

Teaching excerpt · complete runnable lab available from “Code”
def random_code_error(n, R, p, trials, rng):
    M = 2 ** round(n * R)
    code = [rng.getrandbits(n) for _ in range(M)]
    errors = 0
    for _ in range(trials):
        i = rng.randrange(M)
        noise = sum(1 << b for b in range(n) if rng.random() < p)
        y = code[i] ^ noise
        best = min(range(M), key=lambda j: bin(code[j] ^ y).count("1"))
        errors += best != i
    return errors / trials

rng = random.Random(42)
p, R = 0.05, 0.25                          # C = 1 - h(0.05) = 0.714
print(f"C = {1 - h2(p):.3f}, R = {R}")
for n in [8, 16, 24, 32, 40]:
    print(f"n={n:>2}  M={2 ** round(n * R):>4}  block error={random_code_error(n, R, p, 2000, rng):.4f}")
Example output
C = 0.714, R = 0.25
n= 8  M=   4  block error=0.0125
n=16  M=  16  block error=0.0105
n=24  M=  64  block error=0.0045
n=32  M= 256  block error=0.0000
n=40  M=1024  block error=0.0000

No structure at all: codewords are random bit strings. The decoder picks the nearest codeword (maximum likelihood on a BSC with p < 1/2).

The rate stays fixed at 0.25, yet the block error falls as n grows. 0 errors in 2000 trials at n = 32.

So why not use random codes?

Decoding compares against all M = 2nR codewords. Real systems use n in the thousands: 2250 comparisons per block is impossible. Storing the codebook is just as bad.

The history of coding theory is the search for codes that are random enough to near capacity but structured enough to decode fast.

The converse via Fano's inequality PROOF SKETCH

Fano's inequality. If Ŵ estimates W ∈ {1..M} with error Pe = P(Ŵ ≠ W), then

H(W | Ŵ) ≤ 1 + Pe log2 M.

Why: let E flag an error. Knowing Ŵ, first say whether E happened (≤ 1 bit). If it did, W is one of M−1 others (≤ log M bits, weighted by Pe).

Data processing. For a chain W → Xn → Yn → Ŵ: I(W;Ŵ) ≤ I(Xn;Yn). Processing cannot create information.
Memoryless: I(Xn;Yn) ≤ ∑i I(Xi;Yi) ≤ nC.

Putting it together

Take W uniform, so H(W) = nR.

nR = H(W) = H(W|Ŵ) + I(W;Ŵ)
    ≤ 1 + Pe nR + nC.

Divide by nR:

Pe ≥ 1 − C/R − 1/(nR).

If R > C, the error stays above 1 − C/R > 0 forever. ∎

Teaching excerpt · complete runnable lab available from “Code”
def converse_bound(R, C, n):
    return 1 - C / R - 1 / (n * R)
C = 1 - h2(0.11)
for R in [0.6, 0.75, 0.9]:
    print(f"R={R}: P_e >= {converse_bound(R, C, 1000):.3f}  (n=1000, C={C:.3f})")
Example output
R=0.6: P_e >= 0.165  (n=1000, C=0.500)
R=0.75: P_e >= 0.332  (n=1000, C=0.500)
R=0.9: P_e >= 0.443  (n=1000, C=0.500)

Repetition codes: the naive trade-off SIMULATION

Teaching excerpt · complete runnable lab available from “Code”
def rep_error(n, p):
    """Exact bit error of an n-fold repetition code (n odd) with majority vote."""
    return sum(math.comb(n, k) * p**k * (1 - p)**(n - k) for k in range(n // 2 + 1, n + 1))

def simulate_rep(n, p, trials, rng):
    errors = 0
    for _ in range(trials):
        flips = sum(rng.random() < p for _ in range(n))
        errors += flips > n // 2
    return errors / trials

rng = random.Random(1948)
p = 0.1
for n in [1, 3, 5, 7, 9, 11]:
    print(f"n={n:>2} rate={1/n:.3f}  exact={rep_error(n, p):.2e}  "
          f"simulated={simulate_rep(n, p, 200_000, rng):.2e}")
Example output
n= 1 rate=1.000  exact=1.00e-01  simulated=9.92e-02
n= 3 rate=0.333  exact=2.80e-02  simulated=2.77e-02
n= 5 rate=0.200  exact=8.56e-03  simulated=8.64e-03
n= 7 rate=0.143  exact=2.73e-03  simulated=2.68e-03
n= 9 rate=0.111  exact=8.91e-04  simulated=1.00e-03
n=11 rate=0.091  exact=2.96e-04  simulated=2.70e-04
n=1n=3n=5 n=11 C = 0.531 any error rate is possible left of here rate R log₁₀ P(error): −1 (top) to −3.5

Each step down in error costs a lot of rate. To get error → 0 the rate must go → 0.

Shannon says rate 0.5 with error 10−9 is possible on this channel. Repetition at error 10−3.5 already runs at rate 0.09.

Hamming distance: detect vs correct DEFINITIONS

Hamming distance d(x,y) = number of positions where x and y differ. It is a metric (triangle inequality holds).
Minimum distance of a code: d = minc ≠ c' d(c, c'). A code with length n, M words, distance d is an (n, M, d) code.
Theorem. A code with minimum distance d can
  • detect up to d − 1 errors,
  • correct up to t = ⌊(d−1)/2⌋ errors,
  • or fill up to d − 1 erasures.

Proof. ≤ d−1 flips cannot turn one codeword into another. If balls of radius t around codewords overlapped, the triangle inequality gives two codewords within 2t < d. ∎

Teaching excerpt · complete runnable lab available from “Code”
def hamming(a, b):
    return sum(x != y for x, y in zip(a, b))

def min_distance(code):
    return min(hamming(a, b) for a, b in itertools.combinations(code, 2))

rep3 = ["000", "111"]
parity = [a + str(a.count("1") % 2) for a in map("".join, itertools.product("01", repeat=3))]
print(hamming("1011101", "1001001"), min_distance(rep3), min_distance(parity))
for name, d in [("rep3", 3), ("parity", 2), ("Hamming(7,4)", 3)]:
    print(f"{name:13} d={d}: detects {d - 1}, corrects {(d - 1) // 2}")
Example output
2 3 2
rep3          d=3: detects 2, corrects 1
parity        d=2: detects 1, corrects 0
Hamming(7,4)  d=3: detects 2, corrects 1
d ≥ 2t + 1 radius tradius t

Linear codes, G and H ALGEBRA

Linear [n, k, d] code: a k-dimensional subspace of 𝔽2n. The sum (XOR) of two codewords is a codeword. It has 2k codewords and rate k/n.
  • Generator matrix G (k × n): encode message m as c = mG.
  • Parity-check matrix H ((n−k) × n): c is a codeword iff HcT = 0.
  • Systematic form: G = [Ik | P], H = [PT | In−k]. The message appears in the codeword as is.
Facts. (1) d = the smallest weight of a nonzero codeword, since d(a,b) = wt(a+b). (2) d = the fewest columns of H that sum to zero.
Syndrome. Receive r = c + e. Then

s = HrT = HcT + HeT = HeT.

The syndrome depends only on the error, not on the message.

If e is a single 1 at position i, then HeT is column i of H.

So: if all columns of H are nonzero and distinct, the syndrome of a single error names its position. That is the whole idea of Hamming codes.

Syndrome decoding needs a table of 2n−k syndromes, not 2k codewords. For Hamming(7,4): 8 entries.

Hamming(7,4) CODE

Use all 7 nonzero 3-bit columns in H. Then every single error has its own syndrome. With 3 parity bits we protect 4 data bits: rate 4/7 ≈ 0.571, d = 3.

G = [ I4 | P ] =
1 0 0 0 | 1 1 0
0 1 0 0 | 1 0 1
0 0 1 0 | 0 1 1
0 0 0 1 | 1 1 1

H = [ PT | I3 ] =
1 1 0 1 | 1 0 0
1 0 1 1 | 0 1 0
0 1 1 1 | 0 0 1

Parity bits: p1 = d1+d2+d4, p2 = d1+d3+d4, p3 = d2+d3+d4.

d1 d2 d3 d4 p1 p2 p3 each circle must hold an even number of 1s

One flipped bit breaks exactly the circles that contain it. Each bit sits in a different set of circles, so the broken circles point to it. The pattern of broken circles is the syndrome.

Message 1011 encodes to 1011010 (checked on the next slide).

Syndrome decoding in Python CODE

Erasures supply known missing positions; unknown-position errors need a different decoding method. Minimum-distance guarantees do not imply successful recovery beyond the guaranteed error count.

Teaching excerpt · complete runnable lab available from “Code”
def encode(msg):                       # msg: 4 bits -> 7-bit codeword
    return [sum(m * g for m, g in zip(msg, col)) % 2 for col in zip(*G)]

def syndrome(r):
    return tuple(sum(h * x for h, x in zip(row, r)) % 2 for row in Hm)

# each single-bit error at position i has syndrome = column i of H
FIX = {tuple(col): i for i, col in enumerate(zip(*Hm))}

def decode(r):
    r = list(r)
    s = syndrome(r)
    if any(s):
        r[FIX[s]] ^= 1                 # flip the bit the syndrome points to
    return r[:4]                       # systematic: data is the first 4 bits

assert all(syndrome(encode(m)) == (0, 0, 0) for m in itertools.product([0, 1], repeat=4))
fixed = 0
for m in itertools.product([0, 1], repeat=4):
    c = encode(m)
    for i in range(7):
        r = c.copy(); r[i] ^= 1
        fixed += decode(r) == list(m)
print(f"corrected {fixed} of {16 * 7} single-bit errors")
print("codeword for 1011:", "".join(map(str, encode([1, 0, 1, 1]))))
codewords = ["".join(map(str, encode(m))) for m in itertools.product([0, 1], repeat=4)]
print("min distance:", min_distance(codewords))
Example output
corrected 112 of 112 single-bit errors
codeword for 1011: 1011010
min distance: 3

G and Hm are the matrices from the last slide, as lists of lists.

  • Every one of the 16 messages × 7 error positions is fixed: 112 of 112.
  • Decoding is one small matrix multiply plus one table lookup. No search.
Teaching excerpt · complete runnable lab available from “Code”
wrong = 0
for m in itertools.product([0, 1], repeat=4):
    c = encode(m)
    for i, j in itertools.combinations(range(7), 2):
        r = c.copy(); r[i] ^= 1; r[j] ^= 1
        wrong += decode(r) != list(m)
print(f"double errors decoded wrongly: {wrong} of {16 * 21}")
Example output
double errors decoded wrongly: 336 of 336

Two errors always fool it: the syndrome points at a third bit, which makes 3 errors. A d = 3 code corrects 1 error, never 2. Adding an overall parity bit (the [8,4,4] code) turns this into "correct 1, detect 2", the SECDED used in ECC RAM.

Worked example: Hamming(7,4) by hand STEP BY STEP

Encode the message 0110

d1 d2 d3 d4 = 0 1 1 0. Add mod 2 (1 + 1 = 0):

  • p1 = d1+d2+d4 = 0+1+0 = 1
  • p2 = d1+d3+d4 = 0+1+0 = 1
  • p3 = d2+d3+d4 = 1+1+0 = 0

Sent codeword: 0110 110.

The channel flips bit 3

Received: 0100 110. The decoder does not know which bit flipped.

Decode: compute the syndrome

Each row of H rechecks one circle. Add the received bits where the row has a 1:

  • Row 1 (bits 1, 2, 4, 5): 0+1+0+1 = 0
  • Row 2 (bits 1, 3, 4, 6): 0+0+0+1 = 1
  • Row 3 (bits 2, 3, 4, 7): 1+0+0+0 = 1

Syndrome 011 matches column 3 of H. So flip bit 3 back and read 0110. Fixed.

Common mistake

A syndrome of 000 means "no error that this code can see". Three well-placed flips can turn one codeword into another. Also, d = 3 corrects 1 error or detects 2, not both at once.

Hamming(7,4) on a BSC NUMBERS

Teaching excerpt · complete runnable lab available from “Code”
def block_error(p, n, t):
    """P(more than t errors in n bits)."""
    return 1 - sum(math.comb(n, k) * p**k * (1 - p)**(n - k) for k in range(t + 1))
for p in [0.01, 0.05, 0.1]:
    print(f"p={p}: uncoded 4 bits fail {block_error(p, 4, 0):.4f}   "
          f"Hamming(7,4) fails {block_error(p, 7, 1):.4f}")
Example output
p=0.01: uncoded 4 bits fail 0.0394   Hamming(7,4) fails 0.0020
p=0.05: uncoded 4 bits fail 0.1855   Hamming(7,4) fails 0.0444
p=0.1: uncoded 4 bits fail 0.3439   Hamming(7,4) fails 0.1497

At p = 0.01, a 4-bit block fails 20× less often for a 75% bandwidth cost. Compare rep3: rate 1/3 for a similar gain per bit.

CodeRateCorrectsBlock fail, p=0.01
uncoded100.0394 (4 bits)
rep3 on each bit0.3331 per 3≈ 0.0012 (4 bits)
Hamming(7,4)0.5711 per 70.0020
Shannon at p = 0.01up to 0.919error → 0 as n grows

Hamming(7,4) gets close to rep3's reliability at 1.7× the rate. But both are far below capacity 0.919.

For a noisy channel, approaching the asymptotic capacity with vanishing error generally requires growing block length. Special noiseless or zero-error cases need separate treatment.

The Hamming bound and perfect codes BOUND

Hamming (sphere-packing) bound. A q-ary code of length n that corrects t errors has

M · ∑i=0t C(n,i)(q−1)i ≤ qn.

Proof. Radius-t balls around codewords are disjoint. Each holds ∑ C(n,i)(q−1)i words. They all fit in the qn words of the space. ∎

Perfect code: equality holds. The balls tile the whole space: every word is within t of exactly one codeword.

Tietäväinen–van Lint (1973): over prime-power alphabets, every nontrivial perfect code has the parameters of a Hamming code or of one of the two Golay codes, [23,12,7] binary and [11,6,5] ternary.

"Trivial" perfect codes also exist: the whole space, a single word, and odd-length binary repetition codes. That is why rep(5,1) shows up as PERFECT below.

Teaching excerpt · complete runnable lab available from “Code”
def hamming_bound_ok(n, k, d, q=2):
    t = (d - 1) // 2
    ball = sum(math.comb(n, i) * (q - 1) ** i for i in range(t + 1))
    return q ** k * ball, q ** n

for name, n, k, d in [("Hamming(7,4)", 7, 4, 3), ("Hamming(15,11)", 15, 11, 3),
                      ("Golay(23,12)", 23, 12, 7), ("rep(5,1)", 5, 1, 5),
                      ("parity(8,7)", 8, 7, 2), ("BCH(15,7)", 15, 7, 5)]:
    used, total = hamming_bound_ok(n, k, d)
    tag = "PERFECT" if used == total else f"{used / total:.3f} of space"
    print(f"{name:15} 2^k*|ball|={used:>8}  2^n={total:>8}  {tag}")
Example output
Hamming(7,4)    2^k*|ball|=     128  2^n=     128  PERFECT
Hamming(15,11)  2^k*|ball|=   32768  2^n=   32768  PERFECT
Golay(23,12)    2^k*|ball|= 8388608  2^n= 8388608  PERFECT
rep(5,1)        2^k*|ball|=      32  2^n=      32  PERFECT
parity(8,7)     2^k*|ball|=     128  2^n=     256  0.500 of space
BCH(15,7)       2^k*|ball|=   15488  2^n=   32768  0.473 of space

Golay: 212 (1 + 23 + 253 + 1771) = 212 · 211 = 223. A small miracle. Voyager 1 and 2 used the (extended) Golay code for color images of Jupiter and Saturn.

The Singleton bound and MDS codes BOUND

Singleton bound (1964). Any q-ary code with length n, qk codewords and distance d has

d ≤ n − k + 1.

Proof. Delete the last d − 1 positions of every codeword. Two codewords differ in at least d places, so they still differ. We now have qk distinct words of length n − d + 1. So qk ≤ qn−d+1. ∎

MDS (maximum distance separable): d = n − k + 1. Then any k symbols determine the codeword. Up to n − k erasures can always be filled.

Binary MDS codes are trivial (repetition, parity, full space). You need a large alphabet, like bytes, for good MDS codes. That is what Reed–Solomon uses.

Teaching excerpt · complete runnable lab available from “Code”
for name, n, k, d in [("Hamming(7,4)", 7, 4, 3), ("Golay(23,12)", 23, 12, 7),
                      ("RS(255,223)", 255, 223, 33), ("RS(7,3) over GF(8)", 7, 3, 5)]:
    print(f"{name:19} d={d:>2}  n-k+1={n - k + 1:>2}  {'MDS' if d == n - k + 1 else ''}".rstrip())
Example output
Hamming(7,4)        d= 3  n-k+1= 4
Golay(23,12)        d= 7  n-k+1=12
RS(255,223)         d=33  n-k+1=33  MDS
RS(7,3) over GF(8)  d= 5  n-k+1= 5  MDS
BoundSaysTight for
Hammingballs must fitperfect codes
Singletond ≤ n−k+1Reed–Solomon
Gilbert–Varshamovgood codes exist with R ≥ 1 − h(d/n)random linear codes

RS(255,223) with 32 check bytes corrects 16 byte errors or 32 erasures per block. It is the CCSDS deep-space standard.

Reed–Solomon codes CODE

Erasures supply known missing positions; unknown-position errors need a different decoding method. Minimum-distance guarantees do not imply successful recovery beyond the guaranteed error count.

Idea (Reed & Solomon, 1960). Treat k message symbols as coefficients of a polynomial f of degree < k over a finite field. Send its values f(α1),…,f(αn) at n distinct points.
  • Two different polynomials of degree < k agree on at most k − 1 points. So codewords differ in at least n − k + 1 places: MDS.
  • Erasures: any k surviving values fix f by Lagrange interpolation.
  • Errors: Berlekamp–Massey or Berlekamp–Welch decode up to (n−k)/2 wrong symbols in polynomial time.
  • Real systems use GF(28), so a symbol is a byte. A burst of bad bits hits only a few bytes.

This prime-field example demonstrates Reed-Solomon evaluation and erasure recovery by interpolation. It does not implement an unknown-location error decoder such as Berlekamp-Massey.

Teaching excerpt · complete runnable lab available from “Code”
P = 929                                    # prime field used by PDF417 barcodes

def rs_encode(msg, n):
    """Codeword = values of the message polynomial at x = 0..n-1 (mod P)."""
    return [sum(c * pow(x, i, P) for i, c in enumerate(msg)) % P for x in range(n)]

def rs_recover(points, k):
    """Lagrange interpolation from any k (x, y) pairs -> message coefficients."""
    coeffs = [0] * k
    for j, (xj, yj) in enumerate(points[:k]):
        basis, denom = [1], 1              # prod (x - xm) for m != j
        for m, (xm, _) in enumerate(points[:k]):
            if m != j:
                basis = [(a - xm * b) % P for a, b in zip([0] + basis, basis + [0])]
                denom = denom * (xj - xm) % P
        scale = yj * pow(denom, P - 2, P) % P
        coeffs = [(c + scale * b) % P for c, b in zip(coeffs, basis)]
    return coeffs

msg = [72, 105, 33]                        # "Hi!" as numbers, k = 3
cw = rs_encode(msg, 7)                     # n = 7, can lose any 4
print("codeword:", cw)
survivors = [(x, cw[x]) for x in (1, 4, 6)]    # 4 of 7 symbols erased
print("recovered:", rs_recover(survivors, 3))
Example output
codeword: [72, 210, 414, 684, 91, 493, 32]
recovered: [72, 105, 33]

Historical teaching excerpts may omit support definitions. No companion test file is required for the complete lab below: it includes its own implementation, input data, and checks.

Reed–Solomon in the wild APPLICATIONS

Erasures supply known missing positions; unknown-position errors need a different decoding method. Minimum-distance guarantees do not imply successful recovery beyond the guaranteed error count.

QR codes

RS over GF(256). Four levels, L / M / Q / H, can restore about 7 / 15 / 25 / 30% of the codewords. That is why a logo in the middle still scans.

CDs (CIRC, 1982)

Two RS codes, (32,28) and (28,24), with interleaving between them. A scratch that wipes out about 3,500 bits (2.4 mm of track) is fully corrected.

DVDs, Blu-ray

RS product codes: rows and columns of a block are each an RS codeword. Blu-ray adds a "picket" code for bursts.

Deep space

Voyager (from its Uranus flyby) and CCSDS missions: RS(255,223) as the outer code around an inner convolutional code. Concatenation, an idea of Forney (1966).

Storage and RAID-6

RAID-6 keeps two check disks (P and Q) and survives any 2 disk failures. Cloud stores use RS such as (14,10) to survive 4 lost drives at 1.4× overhead, not 3× for copies.

DSL, DVB, barcodes

ADSL and digital TV broadcast use RS as an outer code. PDF417 and Data Matrix barcodes use RS too.

Why RS wins bursts: errors in real media come in clumps. On a byte alphabet, 8 bad bits in a row may be only 1 or 2 bad symbols. Interleaving spreads a long burst over many codewords.

Parity and CRC: detection only CODE

Parity bit: append the XOR of all bits. Code distance 2: every odd number of flips is caught, every even number is missed.
CRC (cyclic redundancy check): treat the message as a polynomial M(x) over 𝔽2. Send M(x)xr + R(x), where R is the remainder mod a generator G(x) of degree r. The receiver checks divisibility by G.
  • An error pattern E(x) is missed only if G(x) divides E(x).
  • With r = 32: every single-bit error and every burst of length ≤ 32 is caught. Random corruption slips by with probability about 2−32.
  • Used in Ethernet, ZIP, PNG, SATA. Detect, then ask again (ARQ) or drop.

Use a keyed MAC, digital signature, or a hash whose expected value is obtained through a trusted channel. An attacker can recompute an unauthenticated hash or CRC after changing data.

Teaching excerpt · complete runnable lab available from “Code”
def crc32(data: bytes) -> int:
    crc = 0xFFFFFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            crc = (crc >> 1) ^ (0xEDB88320 if crc & 1 else 0)
    return crc ^ 0xFFFFFFFF

msg = b"The quick brown fox"
print(hex(crc32(msg)), hex(zlib.crc32(msg)))
bad = bytearray(msg); bad[4] ^= 0b00000100          # flip one bit
print(hex(crc32(bytes(bad))), crc32(bytes(bad)) != crc32(msg))
parity_bit = sum(bin(b).count("1") for b in msg) % 2
print("parity bit:", parity_bit)
Example output
0xb74574de 0xb74574de
0x3d3911bd True
parity bit: 1

Our bitwise CRC-32 matches zlib.crc32. 0xEDB88320 is the IEEE 802.3 polynomial with its bits reversed (the "reflected" form).

Historical teaching excerpts may omit support definitions. No companion test file is required for the complete lab below: it includes its own implementation, input data, and checks.

Near capacity: LDPC, turbo and polar codes MODERN

LDPC codes

Gallager 1960. A parity-check matrix H that is huge but sparse: each bit is in only a few checks.

Decode by belief propagation on the Tanner graph: bits and checks pass soft messages back and forth.

Chung et al. (2001) got within 0.0045 dB of the Shannon limit. Used in Wi-Fi 4/5/6, DVB-S2, 10GBASE-T, SSDs and 5G data.

Turbo codes

Berrou, Glavieux and Thitimajshima, 1993. Two simple convolutional encoders, the second fed a shuffled copy of the data.

Two decoders swap soft guesses, round after round, like a turbo engine feeding back exhaust.

Within about 0.5 dB of capacity. Used in 3G, 4G LTE and deep-space links.

Polar codes

Arıkan, 2009. The first codes proven to reach capacity with O(N log N) encoding and decoding.

Combine channels in pairs, recursively. The split channels polarize: nearly perfect or nearly useless. Send data on the good ones, fixed bits on the rest.

Used for 5G NR control channels.

Teaching excerpt · complete runnable lab available from “Code”
def polarize(eps, levels):
    z = [eps]
    for _ in range(levels):
        z = [w for e in z for w in (2 * e - e * e, e * e)]
    return z

for levels in [1, 4, 8, 12, 16]:
    z = polarize(0.5, levels)
    good = sum(e < 1e-3 for e in z) / len(z)
    bad = sum(e > 1 - 1e-3 for e in z) / len(z)
    print(f"N=2^{levels:<2}  good={good:.3f}  bad={bad:.3f}  middle={1 - good - bad:.3f}")
Example output
N=2^1   good=0.000  bad=0.000  middle=1.000
N=2^4   good=0.062  bad=0.062  middle=0.875
N=2^8   good=0.266  bad=0.266  middle=0.469
N=2^12  good=0.385  bad=0.385  middle=0.229
N=2^16  good=0.446  bad=0.446  middle=0.108

On BEC(0.5), erasure rates split into near 0 and near 1. The mean stays 0.5 (the test asserts it), so the good fraction tends to 1 − ε = C.

AWGN and the Shannon–Hartley law THEOREM

For ideal AWGN, Eb/N0 approaches ln(2) in linear units, or −1.59 dB, as spectral efficiency approaches zero. Do not compare a linear ratio numerically with a dB value.

AWGN channel: Y = X + Z, with Z ~ N(0, N) and power limit E[X2] ≤ P.
Capacity. C = ½ log2(1 + P/N) bits per real sample. With bandwidth B Hz (2B samples/s, Nyquist):

C = B log2(1 + SNR)   bits/s.

Sketch. I(X;Y) = h(Y) − h(Z). Variance of Y is at most P+N, and the Gaussian has the most differential entropy for a given variance: h = ½ log(2πeσ2). So I ≤ ½ log(2πe(P+N)) − ½ log(2πeN). A Gaussian input reaches it. ∎

SNR in dB is 10 log10(P/N). Each +3 dB of SNR (at high SNR) buys about one more bit per sample pair, or B more bits/s.

Teaching excerpt · complete runnable lab available from “Code”
def shannon_hartley(B_hz, snr_db):
    return B_hz * math.log2(1 + 10 ** (snr_db / 10))

for name, B, snr in [("phone line 3.1 kHz", 3.1e3, 35), ("Wi-Fi 20 MHz", 20e6, 25),
                     ("Wi-Fi 6 160 MHz", 160e6, 35), ("5G NR 100 MHz", 100e6, 20),
                     ("5G mmWave 400 MHz", 400e6, 15)]:
    print(f"{name:19} SNR={snr:>2} dB  C={shannon_hartley(B, snr) / 1e6:9.3f} Mbit/s")
print(f"Shannon limit Eb/N0 = ln 2 = {10 * math.log10(math.log(2)):.2f} dB")
Example output
phone line 3.1 kHz  SNR=35 dB  C=    0.036 Mbit/s
Wi-Fi 20 MHz        SNR=25 dB  C=  166.188 Mbit/s
Wi-Fi 6 160 MHz     SNR=35 dB  C= 1860.353 Mbit/s
5G NR 100 MHz       SNR=20 dB  C=  665.821 Mbit/s
5G mmWave 400 MHz   SNR=15 dB  C= 2011.123 Mbit/s
Shannon limit Eb/N0 = ln 2 = -1.59 dB
  • Phone line: 36 kbit/s. V.34 modems hit 33.6 kbit/s, right at the wall.
  • Wi-Fi 6, one stream, 160 MHz, 1024-QAM rate 5/6: 1201 Mbit/s, about 65% of this bound.
  • Real links pass these numbers per antenna with MIMO: k spatial streams give up to k parallel channels.

Power vs bandwidth, and the ultimate limit INSIGHT

For ideal AWGN, Eb/N0 approaches ln(2) in linear units, or −1.59 dB, as spectral efficiency approaches zero. Do not compare a linear ratio numerically with a dB value.

Noise power grows with bandwidth: N = N0B. So

C(B) = B log2(1 + P / (N0B)).

Teaching excerpt · complete runnable lab available from “Code”
for B in [1e6, 10e6, 100e6, 1e9]:
    S_over_N0 = 1e8                        # fixed received power / noise density
    print(f"B={B/1e6:>6.0f} MHz  C={B * math.log2(1 + S_over_N0 / B) / 1e6:7.2f} Mbit/s")
print(f"limit B->inf: {1e8 / math.log(2) / 1e6:.2f} Mbit/s")
Example output
B=     1 MHz  C=   6.66 Mbit/s
B=    10 MHz  C=  34.59 Mbit/s
B=   100 MHz  C= 100.00 Mbit/s
B=  1000 MHz  C= 137.50 Mbit/s
limit B->inf: 144.27 Mbit/s

More bandwidth helps, but with a fixed power the gains flatten. As B → ∞, C → (P/N0) log2 e.

Shannon limit. Energy per bit Eb = P/C. Reliable communication at any rate needs

Eb/N0 > ln 2 ≈ 0.693 = −1.59 dB.

Below −1.59 dB no code, however long, can work. This number is the yardstick for every modern code: "how many dB from the Shannon limit?"

RegimeScarce resourceExample
power-limitedenergydeep space, IoT, GPS
bandwidth-limitedspectrumWi-Fi, 5G, cable

Power-limited links use low-rate codes and simple modulation. Bandwidth-limited links pack many bits per symbol (256- or 1024-QAM) and need high SNR.

Pitfalls and misconceptions CAREFUL

"Below capacity means zero errors"

On a BSC with 0<p<1, finite positive-rate codes generally retain a nonzero error probability. Other channels can have finite zero-error codes; the capacity theorem concerns asymptotically vanishing error.

"Capacity is the raw bit rate"

A 1 Mbit/s link with 11% bit flips carries only 0.5 Mbit/s of real data. The rest must go to redundancy.

Wrong channel model

Codes built for independent errors (BSC) fail on bursts. Interleave, or use byte-level codes like RS.

Hard decisions throw away information

Hard decisions discard information. The loss depends on modulation, SNR, channel model, and rate; around 2 dB is a particular low-SNR benchmark, not a universal penalty.

Beyond t errors, decoders lie

Hamming(7,4) "fixes" 2 errors into 3, every time. Pair correction with a CRC so you know when it failed.

Feedback does not raise capacity

For a DMC, feedback can make coding simpler and errors fall faster, but C stays the same (Shannon 1956).

Common mistakes in exercises STUDENT TRAPS

dB is not the SNR itself

Convert first: SNR = 10dB/10. 20 dB is 100, not 20. For B = 1 MHz that gives log2(101) ≈ 6.66 Mbit/s, not log2(21) ≈ 4.39.

Wrong log base

Capacity in bits needs log2. math.log gives nats (0.693 times smaller). math.log10 is only for dB.

Rate vs capacity

Rate R = k/n belongs to a code. Capacity C belongs to a channel. The theorem compares them: reliable only if R < C.

Floor in the error count

A code with distance d corrects ⌊(d−1)/2⌋ errors. For d = 4 that is 1, not 1.5 or 2.

Symbols vs bits in Reed–Solomon

RS(255,223) fixes 16 byte errors. A burst that hits 8 bits of one byte costs only 1 of those 16.

"Perfect" means "tight", not "best"

A perfect code fills the Hamming bound. Hamming(7,4) is perfect, yet LDPC codes get far closer to capacity.

Check yourself EXERCISES

  1. What is the capacity of a binary erasure channel that erases 25% of bits?
  2. What is the capacity of a BSC with p = 0.5? And with p = 1?
  3. A code has minimum distance 5. How many errors can it correct? How many can it detect?
  4. What is the Hamming distance between 1011010 and 0110110?
  5. A 1 MHz channel has SNR = 15 (linear). What is its capacity?
  6. You want rate 0.6 over a BSC with p = 0.11. Can any code make errors vanish?

Try each one before you look to the right.

Answers

  1. 1 − 0.25 = 0.75 bits per use.
  2. p = 0.5: 0, since output is independent of input. p = 1: 1 bit, since you just flip every bit back.
  3. Correct ⌊4/2⌋ = 2. Detect d − 1 = 4, if you do not try to correct.
  4. 4. They differ in positions 1, 2, 4 and 5.
  5. 106 × log2(1 + 15) = 4 Mbit/s. SNR 15 is about 11.8 dB.
  6. No. C ≈ 0.5 < 0.6, so by the converse the error stays bounded away from 0.

Summary RECAP

IdeaFormula / fact
CapacityC = maxp(x) I(X;Y)
BSC / BEC1 − h(p) / 1 − ε
Coding theoremR < C achievable; R > C impossible
Achievabilityrandom codes + typicality + union bound
ConverseFano: Pe ≥ 1 − C/R − 1/(nR)
Distancedetect d−1, correct ⌊(d−1)/2⌋
Hamming(7,4)syndrome = error position; perfect
BoundsHamming (sphere packing), Singleton d ≤ n−k+1
AWGNC = B log2(1 + SNR); Eb/N0 > −1.59 dB

Noise sets a speed limit, not a quality limit.

  • Below C, long random-like codes make errors vanish. Above it, nothing can.
  • Simple codes (repetition, Hamming) show the idea but sit far from C.
  • Reed–Solomon owns bursts and erasures: QR codes, CDs, RAID, deep space.
  • LDPC, turbo and polar codes reach within a fraction of a dB of Shannon's 1948 bound. Your phone uses them every second.

The big picture: source coding removes redundancy down to H. Channel coding adds it back, in a smart form, up to C. If H < C, the message gets through.

Glossary REFERENCE

TermMeaning
DMCDiscrete memoryless channel: each use is noisy on its own, with fixed p(y|x)
BSC(p)Binary symmetric channel: flips each bit with probability p
BEC(ε)Binary erasure channel: loses a bit (you see "?") with probability ε
Capacity Cmax I(X;Y): the top reliable rate, in bits per use
Rate Rk/n: data bits per sent bit
AWGNAdditive white Gaussian noise channel: Y = X + Z
SNR, dBSignal-to-noise power ratio; dB = 10 log10(SNR)
Eb/N0Energy per bit over noise density; must beat −1.59 dB
TermMeaning
CodewordOne of the 2k allowed length-n strings
Hamming distanceNumber of positions where two strings differ
Minimum distance dSmallest distance between two codewords
Linear [n,k,d] codeCodewords form a vector space; any sum of codewords is a codeword
Generator G / parity-check HG turns messages into codewords; H gives 0 on every codeword
SyndromeH r: depends only on the error, and points to it
Perfect / MDS codeMeets the Hamming bound / the Singleton bound with equality
Soft decisionDecoding from how sure each bit is, not just 0/1

Run a complete program

How does structured redundancy correct a single flipped bit?

The file contains its own imports, data generator or fixture, implementation, and checks.

python3 -m venv .venv
source .venv/bin/activate
# No third-party packages required
python channel_capacity_codes.py
python channel_capacity_codes.py --self-test

Use Python 3.10+. On Windows, activate with .venv\Scripts\Activate.ps1. Required packages: Python standard library.

Connect the concept to the implementation

Read the contract first. Identify the inputs, intermediate state, and return value in this excerpt from the verified file.

def correct(received):
    """Pre: seven bits. Post: closest codeword; original guaranteed only for <=1 flip."""
    received = list(bits(received, 7))
    syndrome = tuple(sum(row[i]*received[i] for i in range(7)) % 2 for row in H)
    if syndrome != (0, 0, 0):
        columns = [tuple(H[row][column] for row in range(3)) for column in range(7)]
        received[columns.index(syndrome)] ^= 1
    return tuple(received)

Compare with an actual run

Sent: (0, 1, 1, 0, 1, 1, 0)
Received: (0, 1, 0, 0, 1, 1, 0)
Corrected: (0, 1, 1, 0, 1, 1, 0)
Two flips recover original: False
BSC(.1) capacity: 0.531004 bits/use
Rate of Hamming(7,4): 0.5714285714285714

Output was captured from the complete program running in an isolated temporary directory. Floating-point digits and compressed sizes may vary across versions/hardware.

Check the result, then try an exercise

Verified checks

131 checks passed, including boundary cases and independent reference comparisons where applicable. These executed checks complement the reasoning; they are not a universal proof.

Flip each of the seven positions for each of the 16 data words. Then flip two positions and demonstrate a failure.

Explain the expected result

Hamming(7,4) guarantees recovery for at most one bit error. Double errors can be miscorrected; SECDED adds an overall parity bit for a different guarantee.

Implement it yourself and validate it

  1. Run the downloadable program and its checks.
  2. Reimplement correct from its contract.
  3. Compare outputs with the reference on valid, empty, and invalid cases.
  4. Explain the limitations of the algorithm or experiment.
All coding labs
Decks