Hamming Codes: Fixing a Flipped Bit
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
hamming-code.ts
import type { Tracer } from "../trace";/** Code word positions are 1-based; a parity bit sits at each power of two. */export const HAMMING_LENGTH = 7;export const HAMMING_DATA = 4;export const PARITY_POSITIONS = [1, 2, 4] as const;export const DATA_POSITIONS = [3, 5, 6, 7] as const;export interface HammingResult { data: number[]; /** Seven bits, position 1 first. */ codeword: number[]; received: number[]; /** The positions whose check failed, added up: the position of a single flipped bit, or 0. */ syndrome: number; corrected: number[]; decoded: number[]; /** The decoded data matches what was sent. */ intact: boolean;}/** * The Hamming (7, 4) code: four data bits become seven, with a parity * bit at each power-of-two position covering every position whose * number has that bit set. A received word is checked the same way, * and the failing checks, added up, spell the position of a single * flipped bit in binary, so the decoder flips it back. Two flips * still produce a syndrome, but one that names the wrong position, * and the corrected word is wrong: the code's distance is three, one * error corrected or two detected, not both. Four bits in seven is a * rate of 4/7, against 1/3 for sending each bit three times. * * Time: O(1), seven positions. Space: O(1). */export class HammingCode { constructor(private readonly tracer?: Tracer) {} /** The positions a parity bit at p covers: every position whose number has p's bit set, p itself included. */ static covers(p: number): number[] { static covers(parityAt: number): number[] { const out: number[] = []; for (let position = 1; position <= HAMMING_LENGTH; position++) if ((position & p) !== 0) out.push(position); for (let position = 1; position <= HAMMING_LENGTH; position++) if ((position & parityAt) !== 0) out.push(position); return out; } /** Four data bits to seven: data at 3, 5, 6, 7, then each parity bit set so its group has an even number of ones. */ encode(data: readonly number[]): number[] { if (data.length !== HAMMING_DATA) throw new RangeError(`expected ${HAMMING_DATA} data bits, got ${data.length}`); const word = new Array<number>(HAMMING_LENGTH + 1).fill(0); DATA_POSITIONS.forEach((position, i) => { word[position] = data[i]! & 1; }); for (const p of PARITY_POSITIONS) { for (const parityAt of PARITY_POSITIONS) { const covered = HammingCode.covers(p).filter((position) => position !== p); const covered = HammingCode.covers(parityAt).filter((position) => position !== parityAt); const ones = covered.reduce((sum, position) => sum + word[position]!, 0); const onesCovered = covered.reduce((sum, position) => sum + word[position]!, 0); word[p] = ones % 2; word[parityAt] = onesCovered % 2; // ▸ set the parity bit so its group has an even number of ones this.tracer?.emit("parity", { position: p, covers: covered, ones, parity: word[p] }); this.tracer?.emit("parity", { position: parityAt, covers: covered, onesCovered, parity: word[parityAt] }); } return word.slice(1); } /** The word with the bit at a 1-based position inverted; 0 leaves it alone. */ flip(codeword: readonly number[], position: number): number[] { const received = [...codeword]; if (position >= 1 && position <= HAMMING_LENGTH) received[position - 1] = 1 - received[position - 1]!; // ▸ the noise inverts a bit in transit this.tracer?.emit("flip", { position, before: position >= 1 && position <= HAMMING_LENGTH ? codeword[position - 1] : null, received: [...received] }); return received; } /** Re-checks every parity group; the failing positions add up to the flipped position, which is flipped back. */ decode(received: readonly number[]): { syndrome: number; corrected: number[]; decoded: number[] } { const word = [0, ...received]; let syndrome = 0; for (const p of PARITY_POSITIONS) { for (const parityAt of PARITY_POSITIONS) { const covered = HammingCode.covers(p); const covered = HammingCode.covers(parityAt); const ones = covered.reduce((sum, position) => sum + word[position]!, 0); const onesCovered = covered.reduce((sum, position) => sum + word[position]!, 0); const ok = ones % 2 === 0; const ok = onesCovered % 2 === 0; if (!ok) syndrome += p; if (!ok) syndrome += parityAt; // ▸ re-count the group: still even, or not this.tracer?.emit("check", { position: p, covers: covered, ones, ok }); this.tracer?.emit("check", { position: parityAt, covers: covered, onesCovered, ok }); } // ▸ the failing groups' positions add up to the flipped position this.tracer?.emit("syndrome", { syndrome, bits: syndrome.toString(2).padStart(3, "0") }); const corrected = [...received]; if (syndrome >= 1 && syndrome <= HAMMING_LENGTH) corrected[syndrome - 1] = 1 - corrected[syndrome - 1]!; // ▸ flip the bit at that position back; 0 means nothing to fix this.tracer?.emit("correct", { position: syndrome, corrected: [...corrected] }); const decoded = DATA_POSITIONS.map((position) => corrected[position - 1]!); return { syndrome, corrected, decoded }; } /** Encode, flip the chosen positions (0 for none), decode. */ run(data: readonly number[], flipAt: number, flipAlso = 0): HammingResult { const codeword = this.encode(data); let received = this.flip(codeword, flipAt); if (flipAlso >= 1 && flipAlso <= HAMMING_LENGTH && flipAlso !== flipAt) received = this.flip(received, flipAlso); const { syndrome, corrected, decoded } = this.decode(received); const intact = decoded.every((bit, i) => bit === (data[i]! & 1)); return { data: data.map((bit) => bit & 1), codeword, received, syndrome, corrected, decoded, intact }; }}One flip, and the data came back right: 4 bits in 7, a rate of 0.571, against 0.333 for sending each bit three times.