Interleaving: Spreading a Burst
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.
block-interleaver.ts
import type { Tracer } from "../trace";import { EXTENDED_LENGTH, ExtendedHammingCode, type SecdedVerdict } from "./extended-hamming-code";/** A block of this many codewords is the most the stage draws legibly. */export const MAX_INTERLEAVE_ROWS = 8;/** Each row of the block is one SECDED word, eight bits wide. */export const INTERLEAVE_COLS = EXTENDED_LENGTH;/** The two sendings compared: the codewords one after another, or the block read out column by column. */export type InterleavePhase = "plain" | "interleaved";export interface CodewordOutcome { row: number; /** The eight bits this codeword's decoder was handed. */ received: number[]; verdict: SecdedVerdict; corrected: number[]; /** The decoded data matches what was sent. */ intact: boolean; /** How many of the row's bits the burst inverted. */ flips: number;}export interface PhaseReport { /** The bits in the order they were sent. */ stream: number[]; /** The same bits after the burst. */ received: number[]; outcomes: CodewordOutcome[]; /** Codewords whose data came through intact. */ survived: number;}export interface InterleaverResult { rows: number; cols: number; data: number[][]; codewords: number[][]; burst: { start: number; length: number }; plain: PhaseReport; interleaved: PhaseReport; /** Codewords in the block, the denominator of both survived counts. */ total: number;}/** * A block interleaver over SECDED codewords. Noise on a real channel * comes in bursts, a scratch or a fade that wrecks many bits in a row, * and a code built for one flip per word loses every word the burst * lands on. Write the codewords into a block one per row, read the * block out column by column, and the sent order holds one bit of each * row before it returns to the first: a burst of up to `rows` bits now * touches every word once instead of one word `rows` times, and each * word corrects its single flip. The receiver writes the stream back * in by column and reads the rows out, the transpose undone. The cost * is latency, since nothing can be decoded until the whole block has * arrived, and a burst longer than the depth still puts two flips in * some row. The same bits sent plainly, codeword after codeword, are * run first so the two sendings can be compared. * * Time: O(rows * cols) per phase. Space: O(rows * cols). */export class BlockInterleaver { /** The corrector of every row, run without a tracer: this file emits every event itself. */ private readonly code = new ExtendedHammingCode(); readonly cols = INTERLEAVE_COLS; constructor( readonly rows = 4, private readonly tracer?: Tracer, ) { if (!Number.isInteger(rows) || rows < 1 || rows > MAX_INTERLEAVE_ROWS) throw new RangeError(`rows must be 1 to ${MAX_INTERLEAVE_ROWS}, got ${rows}`); } /** One SECDED codeword per row of data, quietly. */ encode(data: readonly (readonly number[])[]): number[][] { if (data.length !== this.rows) throw new RangeError(`expected ${this.rows} data rows, got ${data.length}`); return data.map((nibble) => this.code.encode(nibble)); } /** The codewords written into the block, one per row. */ write(codewords: readonly (readonly number[])[]): number[][] { const grid: number[][] = []; codewords.forEach((codeword, row) => { grid.push([...codeword]); // ▸ one codeword into each row of the block this.tracer?.emit("write", { row, codeword: [...codeword] }); }); return grid; } /** The block read out column by column: stream index s holds grid[s mod rows][s div rows]. */ read(grid: readonly (readonly number[])[]): number[] { const stream: number[] = []; for (let col = 0; col < this.cols; col++) { const bits = grid.map((row) => row[col] ?? 0); stream.push(...bits); // ▸ send the block a column at a time: one bit from every row this.tracer?.emit("read", { col, bits, stream: [...stream] }); } return stream; } /** The stream with `length` consecutive bits from `start` inverted, clamped to the stream; 0 bits is no burst. */ burst(stream: readonly number[], start: number, length: number, phase: InterleavePhase): number[] { const received = [...stream]; const from = Math.max(0, start); const to = Math.min(stream.length, from + Math.max(0, length)); for (let s = from; s < to; s++) received[s] = 1 - received[s]!; for (let streamIndex = from; streamIndex < to; streamIndex++) received[streamIndex] = 1 - received[streamIndex]!; const hit = Math.max(0, to - from); const bitsHit = Math.max(0, to - from); // ▸ the noise inverts a run of consecutive bits of whatever was sent this.tracer?.emit("burst", { phase, start, length, hit, received: [...received] }); this.tracer?.emit("burst", { phase, start, length, bitsHit, received: [...received] }); return received; } /** The received stream written back in by column and read out by row: the transpose undone. */ deinterleave(received: readonly number[], codewords: readonly (readonly number[])[]): number[][] { const rows: number[][] = []; for (let row = 0; row < this.rows; row++) { const word = Array.from({ length: this.cols }, (_, col) => received[col * this.rows + row] ?? 0); const flips = BlockInterleaver.differences(word, codewords[row] ?? []); rows.push(word); // ▸ put the stream back into the block by column and take each row out this.tracer?.emit("deinterleave", { row, received: [...word], flips }); } return rows; } /** Every row through the SECDED decoder; each verdict and whether the data survived. */ correct(words: readonly (readonly number[])[], codewords: readonly (readonly number[])[], data: readonly (readonly number[])[], phase: InterleavePhase): CodewordOutcome[] { return words.map((word, row) => { const { verdict, corrected, decoded } = this.code.decode(word); const flips = BlockInterleaver.differences(word, codewords[row] ?? []); const intact = decoded.every((bit, i) => bit === ((data[row]?.[i] ?? 0) & 1)); // ▸ every word through the SECDED decoder: one flip fixed, two reported this.tracer?.emit("correct", { phase, row, verdict, flips, intact }); return { row, received: [...word], verdict, corrected, intact, flips }; }); } /** How many codewords each sending brought through, side by side. */ compare(plain: PhaseReport, interleaved: PhaseReport, length: number): void { // ▸ how many words came through plainly, and how many interleaved this.tracer?.emit("compare", { plain: plain.survived, interleaved: interleaved.survived, total: this.rows, length }); } /** * Encode the data, send it plainly under the burst, then send it * interleaved under the same burst, and compare what survived. */ run(data: readonly (readonly number[])[], start: number, length: number): InterleaverResult { const codewords = this.encode(data); const normalized = data.map((row) => row.map((bit) => bit & 1)); const plainStream = codewords.flat(); const plainReceived = this.burst(plainStream, start, length, "plain"); const plainWords = codewords.map((_, row) => plainReceived.slice(row * this.cols, (row + 1) * this.cols)); const plainOutcomes = this.correct(plainWords, codewords, normalized, "plain"); const plain: PhaseReport = { stream: plainStream, received: plainReceived, outcomes: plainOutcomes, survived: plainOutcomes.filter((o) => o.intact).length }; const grid = this.write(codewords); const stream = this.read(grid); const received = this.burst(stream, start, length, "interleaved"); const words = this.deinterleave(received, codewords); const outcomes = this.correct(words, codewords, normalized, "interleaved"); const interleaved: PhaseReport = { stream, received, outcomes, survived: outcomes.filter((o) => o.intact).length }; this.compare(plain, interleaved, length); return { rows: this.rows, cols: this.cols, data: normalized, codewords, burst: { start, length }, plain, interleaved, total: this.rows }; } /** Positions where two words differ. */ private static differences(a: readonly number[], b: readonly number[]): number { let count = 0; for (let i = 0; i < Math.max(a.length, b.length); i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) count += 1; return count; }}Done: 3 of 4 words survived plainly and 4 of 4 interleaved. A block 4 deep turns any burst of up to 4 bits into one flip per word; a longer burst lands two on some row.