Shannon's second theorem: every noisy channel has a speed limit, and below it errors can be made as rare as you like.
BSC, BEC, Z-channel and AWGN. Mutual information as the rate.
C = max I(X;Y). The coding theorem and its converse, with proof sketches.
Repetition, Hamming(7,4), Reed–Solomon, parity and CRC, in runnable Python.
LDPC, turbo and polar codes. Shannon–Hartley with Wi-Fi and 5G numbers.
Use ← / → or space to move. Every code block was run with Python 3; every output shown is the real output.
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 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.
| Year | Milestone |
|---|---|
| 1948 | Shannon: capacity, coding theorem, AWGN formula |
| 1950 | Hamming codes (born from weekend jobs failing on relay computers) |
| 1954 | Reed–Muller codes |
| 1960 | Reed–Solomon codes; Gallager's LDPC thesis (published 1963) |
| 1961 | Peterson: cyclic codes and CRCs |
| 1967 | Viterbi algorithm for convolutional codes |
| 1993 | Turbo codes: within 0.5 dB of the limit |
| 1996 | MacKay & Neal rediscover LDPC codes |
| 2009 | Arıkan: polar codes, provably reach capacity |
| 2016 | 5G picks LDPC (data) and polar (control) |
Remove redundancy. Squeeze the message to H bits per symbol. Output looks like fair coin flips.
Add structured redundancy. n channel uses carry nR message bits, so the decoder can undo the noise.
Doing the two steps apart loses nothing (for one sender and long blocks): a source with H < C can be sent reliably, and one with H > C cannot.
Binary symmetric: each bit flips with probability p. Model for thermal noise after hard decisions.
Binary erasure: a bit is lost with probability ε, and you know it is lost. Model for dropped packets.
Z-channel: 0 is always safe, 1 can decay to 0. Models some optical links and memory cells that leak charge.
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)
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).
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}")
BSC(0.1), uniform input: 0.5310 BSC(0.1), px=[0.8,0.2]: 0.3578 BEC(0.3), uniform input: 0.7000
C = maxp(x) I(X;Y) bits per channel use.
This is the same alternating-maximization idea as EM.
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}]")
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]
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. ∎
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}")
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
Question. A link flips 11% of its bits at random. How many real data bits can each channel use carry?
h(p) = −p log2 p − (1−p) log2(1−p)
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.
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.
"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.
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.
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.
Two surprises in one line:
Stronger forms:
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.
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.
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}")
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.
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.
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).
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. ∎
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})")
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)
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}")
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
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.
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. ∎
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}")
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
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.
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.
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).
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))
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.
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}")
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.
d1 d2 d3 d4 = 0 1 1 0. Add mod 2 (1 + 1 = 0):
Sent codeword: 0110 110.
Received: 0100 110. The decoder does not know which bit flipped.
Each row of H rechecks one circle. Add the received bits where the row has a 1:
Syndrome 011 matches column 3 of H. So flip bit 3 back and read 0110. Fixed.
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.
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}")
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.
| Code | Rate | Corrects | Block fail, p=0.01 |
|---|---|---|---|
| uncoded | 1 | 0 | 0.0394 (4 bits) |
| rep3 on each bit | 0.333 | 1 per 3 | ≈ 0.0012 (4 bits) |
| Hamming(7,4) | 0.571 | 1 per 7 | 0.0020 |
| Shannon at p = 0.01 | up to 0.919 | error → 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.
Short codes cannot reach capacity. The law of large numbers only kicks in over long blocks: you need thousands of bits so the number of errors is close to np and predictable.
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. ∎
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.
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}")
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.
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. ∎
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.
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())
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
| Bound | Says | Tight for |
|---|---|---|
| Hamming | balls must fit | perfect codes |
| Singleton | d ≤ n−k+1 | Reed–Solomon |
| Gilbert–Varshamov | good 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.
This demo uses the prime field GF(929) so plain % works. PDF417 barcodes use this exact field.
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))
codeword: [72, 210, 414, 684, 91, 493, 32] recovered: [72, 105, 33]
The test file checks all 35 ways to keep 3 of the 7 symbols.
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.
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.
RS product codes: rows and columns of a block are each an RS codeword. Blu-ray adds a "picket" code for bursts.
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).
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.
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.
CRCs catch accidents, not attacks. Anyone can fix up a CRC after changing data. Use a MAC or hash against tampering.
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)
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).
The test file also flips 1–6 random bits in 20,000 trials: 0 corruptions went undetected.
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.
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.
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.
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}")
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.
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.
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")
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
Noise power grows with bandwidth: N = N0B. So
C(B) = B log2(1 + P / (N0B)).
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")
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.
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?"
| Regime | Scarce resource | Example |
|---|---|---|
| power-limited | energy | deep space, IoT, GPS |
| bandwidth-limited | spectrum | Wi-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.
No: it means error as small as you like with long enough blocks. Every finite code still has some error. Long blocks also add delay.
A 1 Mbit/s link with 11% bit flips carries only 0.5 Mbit/s of real data. The rest must go to redundancy.
Codes built for independent errors (BSC) fail on bursts. Interleave, or use byte-level codes like RS.
Rounding each received voltage to 0/1 turns AWGN into a BSC and costs about 2 dB. Modern decoders use soft values (log-likelihood ratios).
Hamming(7,4) "fixes" 2 errors into 3, every time. Pair correction with a CRC so you know when it failed.
For a DMC, feedback can make coding simpler and errors fall faster, but C stays the same (Shannon 1956).
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.
Capacity in bits needs log2. math.log gives nats (0.693 times smaller). math.log10 is only for dB.
Rate R = k/n belongs to a code. Capacity C belongs to a channel. The theorem compares them: reliable only if R < C.
A code with distance d corrects ⌊(d−1)/2⌋ errors. For d = 4 that is 1, not 1.5 or 2.
RS(255,223) fixes 16 byte errors. A burst that hits 8 bits of one byte costs only 1 of those 16.
A perfect code fills the Hamming bound. Hamming(7,4) is perfect, yet LDPC codes get far closer to capacity.
1011010 and 0110110?Try each one before you look to the right.
| Idea | Formula / fact |
|---|---|
| Capacity | C = maxp(x) I(X;Y) |
| BSC / BEC | 1 − h(p) / 1 − ε |
| Coding theorem | R < C achievable; R > C impossible |
| Achievability | random codes + typicality + union bound |
| Converse | Fano: Pe ≥ 1 − C/R − 1/(nR) |
| Distance | detect d−1, correct ⌊(d−1)/2⌋ |
| Hamming(7,4) | syndrome = error position; perfect |
| Bounds | Hamming (sphere packing), Singleton d ≤ n−k+1 |
| AWGN | C = B log2(1 + SNR); Eb/N0 > −1.59 dB |
Noise sets a speed limit, not a quality limit.
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.
| Term | Meaning |
|---|---|
| DMC | Discrete 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 C | max I(X;Y): the top reliable rate, in bits per use |
| Rate R | k/n: data bits per sent bit |
| AWGN | Additive white Gaussian noise channel: Y = X + Z |
| SNR, dB | Signal-to-noise power ratio; dB = 10 log10(SNR) |
| Eb/N0 | Energy per bit over noise density; must beat −1.59 dB |
| Term | Meaning |
|---|---|
| Codeword | One of the 2k allowed length-n strings |
| Hamming distance | Number of positions where two strings differ |
| Minimum distance d | Smallest distance between two codewords |
| Linear [n,k,d] code | Codewords form a vector space; any sum of codewords is a codeword |
| Generator G / parity-check H | G turns messages into codewords; H gives 0 on every codeword |
| Syndrome | H r: depends only on the error, and points to it |
| Perfect / MDS code | Meets the Hamming bound / the Singleton bound with equality |
| Soft decision | Decoding from how sure each bit is, not just 0/1 |