The algebraic backbone of error correction — generator matrices, syndrome decoding, and the geometry of codewords
Vector spaces over GF(2), linear code definition, [n,k,d] notation
Generator matrix G, parity-check matrix H, systematic form
Matrix multiplication over GF(2), systematic encoding
Syndrome decoding, standard arrays, coset leaders, ML decoding
Dual codes, weight distributions, MacWilliams identity
Singleton, Hamming, Plotkin bounds; classic linear codes
GF(2) = {0, 1} with arithmetic modulo 2:
Addition = XOR, Multiplication = AND
Note: subtraction is the same as addition in GF(2), since -1 = 1
The set of all binary n-tuples forms a vector space over GF(2):
Example: GF(2)3 = {000, 001, 010, 011, 100, 101, 110, 111}
A linear code C is a k-dimensional subspace of GF(2)n.
This means C is closed under:
Vector addition: If c₁, c₂ ∈ C, then c₁ + c₂ ∈ C
Scalar multiplication: If c ∈ C and a ∈ GF(2), then a·c ∈ C
Error correction capability: t = ⌊(d-1)/2⌋ Error detection: d-1 errors
The generator matrix G is a k × n matrix whose rows form a basis for the code C.
where m is a 1 × k message vector and c is a 1 × n codeword.
Any k linearly independent codewords can serve as rows of G.
G is in systematic form when:
where Ik is the k×k identity and P is a k×(n-k) parity matrix.
Codeword = [message | parity]
Any linear code can be put in systematic form via row reduction (Gaussian elimination over GF(2)).
| 1 | 0 | 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 0 | 0 | 1 | 1 |
| 0 | 0 | 1 | 0 | 1 | 1 | 1 |
| 0 | 0 | 0 | 1 | 1 | 0 | 1 |
Example encoding: Message m = [1 0 1 1]
First 4 bits = message (systematic), last 3 bits = parity
The code has 24 = 16 codewords, can correct 1 error (t = ⌊(3-1)/2⌋ = 1).
The parity-check matrix H is an (n-k) × n matrix such that:
A vector v is a valid codeword if and only if H · vT = 0.
The rows of G are orthogonal to the rows of H
If G = [Ik | P], then:
In GF(2), -PT = PT
Proof sketch: If c is a codeword of weight w, then HcT = 0, which means w columns of H sum to zero.
| 1 | 0 | 1 | 1 | 1 | 0 | 0 |
| 1 | 1 | 1 | 0 | 0 | 1 | 0 |
| 0 | 1 | 1 | 1 | 0 | 0 | 1 |
Every column is distinct and nonzero. Any 2 columns are linearly independent, but there exist 3 dependent columns. Therefore d = 3.
import numpy as np
def encode_linear(message, G):
"""Encode message using generator matrix over GF(2)."""
return np.dot(message, G) % 2
# Hamming [7,4,3] generator matrix (systematic)
G = np.array([
[1, 0, 0, 0, 1, 1, 0],
[0, 1, 0, 0, 0, 1, 1],
[0, 0, 1, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 0, 1]
])
# Encode all 16 messages
for i in range(16):
m = np.array([(i >> 3)&1, (i >> 2)&1, (i >> 1)&1, i&1])
c = encode_linear(m, G)
print(f"m={m} -> c={c}, weight={sum(c)}")
When we receive r = c + e (codeword + error), compute:
The syndrome depends only on the error pattern, not on the transmitted codeword!
Either no errors occurred, or the error pattern is itself a codeword (undetectable).
Errors detected! The syndrome identifies the coset containing the error pattern.
Sent: c = [1 0 1 1 1 0 0]
Error: e = [0 0 0 0 0 1 0] (bit 5 flipped)
Received: r = [1 0 1 1 1 1 0]
Step 1: Compute syndrome s = H · rT
Step 2: Look up syndrome in table — matches column 5 of H
Step 3: Flip bit 5: ĉ = r + e₅ = [1 0 1 1 1 0 0]
Step 4: Extract message: m̂ = [1 0 1 1] ✓
The standard array partitions GF(2)n into cosets of the code C:
| Coset Leader | Coset Members (leader + each codeword) | |||
|---|---|---|---|---|
| 0000000 | c₁ | c₂ | ... | c₁₆ |
| 0000001 | c₁+e | c₂+e | ... | c₁₆+e |
| 0000010 | ... | ... | ... | ... |
| ... (2n-k = 23 = 8 cosets total) | ||||
| Syndrome s | Error Pattern e | Error Position |
|---|---|---|
| [0 0 0] | 0000000 | No error |
| [1 0 0] | 0001000 | Position 3 |
| [0 1 0] | 0100000 | Position 1 |
| [0 0 1] | 0000001 | Position 6 |
| [1 1 0] | 1000000 | Position 0 |
| [0 1 1] | 0010000 | Position 2 |
| [1 0 1] | 0000010 | Position 4 |
| [1 1 1] | 0000100 | Position 5 |
Only 2n-k = 8 entries needed vs. 2n = 128 for full standard array
The dual code C⊥ of an [n, k] code C is:
A code is self-dual if C = C⊥.
Requires n = 2k (rate = 1/2).
Example: Extended [8,4,4] Hamming code is self-dual.
Self-dual codes have elegant mathematical properties and connections to lattices.
The weight enumerator polynomial of code C:
where Ai = number of codewords of weight i.
| Weight | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| Count Ai | 1 | 0 | 0 | 7 | 7 | 0 | 0 | 1 |
W(x,y) = x7 + 7x4y3 + 7x3y4 + y7
Total: 1 + 7 + 7 + 1 = 16 = 24 ✓
Given received vector r, find the codeword ĉ that maximizes:
For BSC with error probability p < 1/2, this is equivalent to:
Proof: Delete any d-1 coordinate positions from all codewords. The resulting shortened words have length n-d+1. Since they must all be distinct (otherwise two codewords would differ in fewer than d positions), we need:
where t = ⌊(d-1)/2⌋ and C(n,i) = "n choose i".
Place a sphere of radius t around each of the 2k codewords. These spheres must not overlap. The total volume of all spheres cannot exceed the total space 2n.
Codes achieving equality are perfect codes. The spheres tile the entire space!
Proved in 1973 (Tietäväinen, van Lint): these are the ONLY binary perfect codes!
Count the total Hamming distance summed over all pairs:
By counting column-by-column, we can show S ≤ |C|2 · n/2.
Combining: |C|(|C|-1)d ≤ |C|2n/2 → solve for |C|.
[n, 1, n]
G = [1 1 ... 1]
Just two codewords: all-zeros and all-ones.
Rate = 1/n → 0 as n grows. Maximum error correction for given n.
[n, n-1, 2]
H = [1 1 ... 1]
Append one parity bit. Detects all single errors but corrects none.
Rate = (n-1)/n → 1. Dual of repetition code!
[2r-1, 2r-r-1, 3]
H has all nonzero r-bit columns.
The unique perfect 1-error correcting code family.
Examples: [7,4,3], [15,11,3], [31,26,3]
import numpy as np
class LinearCode:
def __init__(self, G):
"""Initialize linear code from generator matrix G over GF(2)."""
self.G = np.array(G) % 2
self.k, self.n = self.G.shape
self.H = self._compute_parity_check()
def _compute_parity_check(self):
"""Compute H from systematic G = [I_k | P] -> H = [P^T | I_{n-k}]."""
P = self.G[:, self.k:] # extract parity part
r = self.n - self.k
return np.hstack([P.T, np.eye(r, dtype=int)]) % 2
def encode(self, message):
"""Encode k-bit message to n-bit codeword."""
return np.dot(message, self.G) % 2
def syndrome(self, received):
"""Compute syndrome s = H @ r^T."""
return np.dot(self.H, received) % 2
def decode(self, received):
"""Syndrome decode: correct single errors."""
s = self.syndrome(received)
if not np.any(s):
return received[:self.k] # no error
# Find matching column in H
for i in range(self.n):
if np.array_equal(s, self.H[:, i]):
corrected = received.copy()
corrected[i] ^= 1
return corrected[:self.k]
return None # uncorrectable
# Create Hamming [7,4,3] code
G = [[1,0,0,0, 1,1,0],
[0,1,0,0, 0,1,1],
[0,0,1,0, 1,1,1],
[0,0,0,1, 1,0,1]]
hamming = LinearCode(G)
# Encode
msg = np.array([1, 0, 1, 1])
codeword = hamming.encode(msg)
print(f"Message: {msg}")
print(f"Codeword: {codeword}") # [1 0 1 1 1 0 0]
# Simulate single-bit error on position 2
received = codeword.copy()
received[2] ^= 1 # flip bit 2
print(f"Received: {received}") # [1 0 0 1 1 0 0]
# Decode
s = hamming.syndrome(received)
print(f"Syndrome: {s}") # [1 1 1] -> column 2 of H
decoded = hamming.decode(received)
print(f"Decoded: {decoded}") # [1 0 1 1] ✓
# Verify: test all single-error patterns
errors_corrected = 0
for msg_int in range(16):
m = np.array([(msg_int>>i)&1 for i in range(3,-1,-1)])
c = hamming.encode(m)
for pos in range(7):
r = c.copy()
r[pos] ^= 1
d = hamming.decode(r)
if np.array_equal(d, m):
errors_corrected += 1
print(f"Corrected {errors_corrected}/112 single errors") # 112/112