SECDED: ECC Memory
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
extended-hamming-code.ts
import { DATA_POSITIONS, HAMMING_DATA, HAMMING_LENGTH, HammingCode, PARITY_POSITIONS } from "../binary/hamming-code";import type { Tracer } from "../trace";/** Eight positions, 1-based: the seven of the (7, 4) code and an overall parity bit at 8. */export const EXTENDED_LENGTH = 8;export const OVERALL_POSITION = 8;/** Every position the receiver checks: the three Hamming groups, then the whole word. */export const CHECK_POSITIONS = [1, 2, 4, 8] as const;/** The (7, 4) code's length and parity positions, re-exported so a stage can tone them without reaching into the binary area. */export const INNER_LENGTH = HAMMING_LENGTH;export const INNER_PARITY_POSITIONS = PARITY_POSITIONS;/** * What the receiver concludes: nothing wrong, one flip put back, two * flips reported and left alone, or the overall parity bit itself flipped. */export type SecdedVerdict = "clean" | "single" | "double" | "parity";export interface ExtendedHammingResult { data: number[]; /** Eight bits, position 1 first; bit 8 makes the whole word even. */ codeword: number[]; received: number[]; /** The positions the noise inverted, in order. */ flips: number[]; /** The failing Hamming checks' positions added up: a position, or 0. */ syndrome: number; /** The received word has an odd number of ones. */ odd: boolean; verdict: SecdedVerdict; /** The position flipped back, 0 when nothing was touched. */ position: number; corrected: number[]; decoded: number[]; /** The decoded data matches what was sent. */ intact: boolean;}/** * The extended Hamming (8, 4) code, SECDED: single error corrected, * double error detected. The seven bits of the (7, 4) code get an * eighth, the parity of all seven, so every code word has an even * number of ones and the code's distance rises from three to four. * The receiver re-counts the three Hamming groups as before and the * whole word besides. One flip makes the word odd and the syndrome * names it; two flips leave the word even while the syndrome is not * zero, which no single flip can do, so the receiver reports the pair * instead of guessing a third position; a flip of the parity bit * alone makes the word odd with a syndrome of zero. ECC memory does * this at (72, 64), eight check bits per 64-bit word, and scrubs * single flips before a second can join them. * * Time: O(1), eight positions. Space: O(1). */export class ExtendedHammingCode { /** The (7, 4) encoder, run without a tracer: this file emits every event itself. */ private readonly inner = new HammingCode(); constructor(private readonly tracer?: Tracer) {} /** The positions a check at p covers: a Hamming group for 1, 2, 4; the whole word for 8. */ static covers(p: number): number[] { static covers(checkAt: number): number[] { if (p === OVERALL_POSITION) return Array.from({ length: EXTENDED_LENGTH }, (_, i) => i + 1); if (checkAt === OVERALL_POSITION) return Array.from({ length: EXTENDED_LENGTH }, (_, i) => i + 1); return HammingCode.covers(p); return HammingCode.covers(checkAt); } /** Four data bits to eight: the (7, 4) code word, then a bit that makes all eight even. */ encode(data: readonly number[]): number[] { if (data.length !== HAMMING_DATA) throw new RangeError(`expected ${HAMMING_DATA} data bits, got ${data.length}`); const word = this.inner.encode(data); const ones = word.reduce((sum, bit) => sum + bit, 0); const onesCovered = word.reduce((sum, bit) => sum + bit, 0); const parity = ones % 2; const parity = onesCovered % 2; const codeword = [...word, parity]; // ▸ add an eighth bit so the whole word has an even number of ones this.tracer?.emit("extend", { codeword: [...codeword], ones, parity }); this.tracer?.emit("extend", { codeword: [...codeword], onesCovered, parity }); return codeword; } /** 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]; const inRange = position >= 1 && position <= EXTENDED_LENGTH; if (inRange) received[position - 1] = 1 - received[position - 1]!; // ▸ the noise inverts a bit in transit this.tracer?.emit("flip", { position, before: inRange ? codeword[position - 1] : null, received: [...received] }); return received; } /** Re-counts the three Hamming groups and the whole word: the failing groups add up to the syndrome, the whole word says odd or even. */ check(received: readonly number[]): { syndrome: number; odd: boolean } { const word = [0, ...received]; let syndrome = 0; let odd = false; for (const p of CHECK_POSITIONS) { for (const checkAt of CHECK_POSITIONS) { const covered = ExtendedHammingCode.covers(p); const covered = ExtendedHammingCode.covers(checkAt); 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 && p === OVERALL_POSITION) odd = true; if (!ok && checkAt === OVERALL_POSITION) odd = true; else if (!ok) syndrome += p; else if (!ok) syndrome += checkAt; // ▸ re-count the group, or for 8 the whole word: still even, or not this.tracer?.emit("check", { position: p, covers: covered, ones, ok }); this.tracer?.emit("check", { position: checkAt, covers: covered, onesCovered, ok }); } return { syndrome, odd }; } /** The decision table: syndrome and overall parity together say what happened and what to flip back. */ classify(syndrome: number, odd: boolean): { verdict: SecdedVerdict; position: number } { if (syndrome === 0 && !odd) return { verdict: "clean", position: 0 }; if (syndrome !== 0 && odd) return { verdict: "single", position: syndrome }; if (syndrome !== 0) return { verdict: "double", position: 0 }; return { verdict: "parity", position: OVERALL_POSITION }; } /** The word with the bit at a position flipped back; 0 leaves it alone. */ correct(received: readonly number[], position: number): number[] { const corrected = [...received]; if (position >= 1 && position <= EXTENDED_LENGTH) corrected[position - 1] = 1 - corrected[position - 1]!; // ▸ flip the named bit back; on two flips touch nothing and report them this.tracer?.emit("correct", { position, corrected: [...corrected] }); return corrected; } /** Check, classify, correct what the verdict allows, and read the data off positions 3, 5, 6, 7. */ decode(received: readonly number[]): { syndrome: number; odd: boolean; verdict: SecdedVerdict; position: number; corrected: number[]; decoded: number[] } { const { syndrome, odd } = this.check(received); const { verdict, position } = this.classify(syndrome, odd); // ▸ syndrome and overall parity together: clean, one flip, two flips, or the parity bit this.tracer?.emit("verdict", { syndrome, bits: syndrome.toString(2).padStart(3, "0"), odd, verdict, position }); const corrected = this.correct(received, position); const decoded = DATA_POSITIONS.map((p) => corrected[p - 1]!); const decoded = DATA_POSITIONS.map((checkAt) => corrected[checkAt - 1]!); return { syndrome, odd, verdict, position, corrected, decoded }; } /** Encode, flip the chosen positions (0 for none), decode. */ run(data: readonly number[], flipAt = 0, flipAlso = 0): ExtendedHammingResult { const codeword = this.encode(data); const flips: number[] = []; let received = this.flip(codeword, flipAt); if (flipAt >= 1 && flipAt <= EXTENDED_LENGTH) flips.push(flipAt); if (flipAlso >= 1 && flipAlso <= EXTENDED_LENGTH && flipAlso !== flipAt) { received = this.flip(received, flipAlso); flips.push(flipAlso); } const { syndrome, odd, verdict, position, corrected, decoded } = this.decode(received); const intact = decoded.every((bit, i) => bit === (data[i]! & 1)); return { data: data.map((bit) => bit & 1), codeword, received, flips, syndrome, odd, verdict, position, corrected, decoded, intact }; }}One flip, and the data came back right. Eight bits carry four, a rate of 0.500 against 0.571 for the (7, 4) code: one more bit buys the double-flip report.