Parity: One Bit That Notices
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.
parity-grid.ts
import type { Tracer } from "../trace";export const MAX_PARITY_ROWS = 8;export const MAX_PARITY_COLS = 8;export type ParityVerdict = "clean" | "corrected" | "detected";/** A cell of the grid, row 0 at the top; the parity column is column `cols`, the parity row is row `rows`. */export interface GridCell { row: number; col: number;}export interface ParityGridResult { rows: number; cols: number; /** The data bits, row-major. */ data: number[]; /** The encoded grid: data with a parity column, and (in 2D) a parity row whose corner guards the parity column. */ grid: number[][]; received: number[][]; flips: GridCell[]; failingRows: number[]; failingCols: number[]; verdict: ParityVerdict; /** The cell the correction flipped back, or null. */ position: GridCell | null; corrected: number[][]; /** The data region of the corrected grid matches what was sent. */ intact: boolean;}/** * Parity, in one and two dimensions. A parity bit makes its line hold * an even number of ones, so a single flip anywhere in the line is * noticed when the receiver counts again. One row of data and its bit * (the byte with a parity bit) can only notice. Lay the data out as a * grid, give every row a parity bit and every column one too (the * corner guards the parity column), and a single flip fails exactly one * row and one column, which cross at the flipped cell: noticed and * placed, so it can be put back. Two flips fail two lines on some axis, * or none when they share a line, and can be noticed but not placed; * four flips on the corners of a rectangle fail nothing at all. * * Time: O(rows * cols). Space: O(rows * cols). */export class ParityGrid { constructor( readonly rows = 4, readonly cols = 4, private readonly tracer?: Tracer, ) { if (!Number.isInteger(rows) || rows < 1 || rows > MAX_PARITY_ROWS) throw new RangeError(`rows must be 1 to ${MAX_PARITY_ROWS}, got ${rows}`); if (!Number.isInteger(cols) || cols < 1 || cols > MAX_PARITY_COLS) throw new RangeError(`cols must be 1 to ${MAX_PARITY_COLS}, got ${cols}`); } /** One row of data is the 1D case: a parity bit per row and no column pass. */ get twoDimensional(): boolean { return this.rows > 1; } /** Rows of the encoded grid: the data rows plus the parity row in 2D. */ get height(): number { return this.twoDimensional ? this.rows + 1 : 1; } /** Columns of the encoded grid: the data columns plus the parity column. */ get width(): number { return this.cols + 1; } /** The even-parity bit of some bits: 1 when they hold an odd number of ones. */ static parity(bits: readonly number[]): number { static parity(lineBits: readonly number[]): number { return bits.reduce((sum, bit) => sum ^ (bit & 1), 0); return lineBits.reduce((sum, bit) => sum ^ (bit & 1), 0); } /** Lays the data out in rows, appends a parity bit to each, and (in 2D) a parity row under every column, the parity column included. */ encode(data: readonly number[]): number[][] { if (data.length !== this.rows * this.cols) throw new RangeError(`expected ${this.rows * this.cols} data bits, got ${data.length}`); const grid: number[][] = []; for (let row = 0; row < this.rows; row++) { const bits = data.slice(row * this.cols, (row + 1) * this.cols).map((bit) => bit & 1); const lineBits = data.slice(row * this.cols, (row + 1) * this.cols).map((bit) => bit & 1); const ones = bits.reduce((sum, bit) => sum + bit, 0); const onesInLine = lineBits.reduce((sum, bit) => sum + bit, 0); const parity = ones % 2; const parity = onesInLine % 2; grid.push([...bits, parity]); grid.push([...lineBits, parity]); // ▸ append a bit that makes the row's count of ones even this.tracer?.emit("parity-row", { row, bits, ones, parity }); this.tracer?.emit("parity-row", { row, lineBits, onesInLine, parity }); } if (this.twoDimensional) { const parityRow: number[] = []; for (let col = 0; col < this.width; col++) { const bits = grid.map((line) => line[col]!); const lineBits = grid.map((line) => line[col]!); const ones = bits.reduce((sum, bit) => sum + bit, 0); const onesInLine = lineBits.reduce((sum, bit) => sum + bit, 0); const parity = ones % 2; const parity = onesInLine % 2; parityRow.push(parity); // ▸ a bit under each column, the parity column too this.tracer?.emit("parity-col", { col, bits, ones, parity }); this.tracer?.emit("parity-col", { col, lineBits, onesInLine, parity }); } grid.push(parityRow); } return grid; } /** The grid with one cell inverted; a null cell is the noise leaving the word alone. */ flip(grid: readonly (readonly number[])[], cell: GridCell | null): number[][] { const received = grid.map((line) => [...line]); const inside = cell !== null && cell.row >= 0 && cell.row < this.height && cell.col >= 0 && cell.col < this.width; const before = inside ? received[cell.row]![cell.col]! : null; if (inside) received[cell.row]![cell.col] = 1 - before!; // ▸ the noise inverts the cells it hits this.tracer?.emit("flip", { row: inside ? cell.row : null, col: inside ? cell.col : null, before, received: received.map((line) => [...line]) }); return received; } /** Counts every row and (in 2D) every column again: a line with an odd count fails. */ check(received: readonly (readonly number[])[]): { failingRows: number[]; failingCols: number[] } { const failingRows: number[] = []; const failingCols: number[] = []; for (let row = 0; row < this.height; row++) { if (!this.checkLine("row", row, received[row]!)) failingRows.push(row); } if (this.twoDimensional) { for (let col = 0; col < this.width; col++) { if (!this.checkLine("col", col, received.map((line) => line[col]!))) failingCols.push(col); } } return { failingRows, failingCols }; } /** One line counted again: still even, or not. The same call for a row and a column, so one tag traces both passes. */ private checkLine(axis: "row" | "col", index: number, bits: readonly number[]): boolean { private checkLine(axis: "row" | "col", index: number, lineBits: readonly number[]): boolean { const ones = bits.reduce((sum, bit) => sum + bit, 0); const onesInLine = lineBits.reduce((sum, bit) => sum + bit, 0); const ok = ones % 2 === 0; const ok = onesInLine % 2 === 0; // ▸ count the line again: still even, or not this.tracer?.emit("check", { axis, index, ones, ok }); this.tracer?.emit("check", { axis, index, onesInLine, ok }); return ok; } /** One failing row and one failing column cross at the flipped cell; nothing failing is clean; anything else is noticed but not placed. */ locate(failingRows: readonly number[], failingCols: readonly number[]): { verdict: ParityVerdict; position: GridCell | null } { let verdict: ParityVerdict = "detected"; let position: GridCell | null = null; if (failingRows.length === 0 && failingCols.length === 0) verdict = "clean"; else if (failingRows.length === 1 && failingCols.length === 1) { verdict = "corrected"; position = { row: failingRows[0]!, col: failingCols[0]! }; } // ▸ one failing row and one failing column cross at the flip this.tracer?.emit("locate", { rows: [...failingRows], cols: [...failingCols], verdict, position: position ? { ...position } : null }); return { verdict, position }; } /** Flips the located cell back; with no position the received grid is handed on as it is. */ correct(received: readonly (readonly number[])[], position: GridCell | null): number[][] { const corrected = received.map((line) => [...line]); if (position) corrected[position.row]![position.col] = 1 - corrected[position.row]![position.col]!; // ▸ flip that cell back; nothing to fix, or only noticed this.tracer?.emit("correct", { position: position ? { ...position } : null, corrected: corrected.map((line) => [...line]) }); return corrected; } /** Encode, flip the chosen cells (none for a clean run), check every line, locate, correct. */ run(data: readonly number[], flips: readonly GridCell[] = []): ParityGridResult { const grid = this.encode(data); let received: number[][] = grid.map((line) => [...line]); if (flips.length === 0) received = this.flip(received, null); for (const cell of flips) received = this.flip(received, cell); const { failingRows, failingCols } = this.check(received); const { verdict, position } = this.locate(failingRows, failingCols); const corrected = this.correct(received, position); const intact = data.every((bit, i) => (bit & 1) === corrected[Math.floor(i / this.cols)]![i % this.cols]); return { rows: this.rows, cols: this.cols, data: data.map((bit) => bit & 1), grid, received, flips: flips.map((cell) => ({ ...cell })), failingRows, failingCols, verdict, position, corrected, intact, }; }}One flip, placed and put back: the data came through at a rate of 0.64 data bits per bit sent.