diff --git a/.github/ISSUE_TEMPLATE/tweak-book.yml b/.github/ISSUE_TEMPLATE/tweak-book.yml index fdce43bc..d9baabe6 100644 --- a/.github/ISSUE_TEMPLATE/tweak-book.yml +++ b/.github/ISSUE_TEMPLATE/tweak-book.yml @@ -16,6 +16,7 @@ body: description: The dropdown stays in sync with the shelf automatically. options: # BOOKS:START (auto-generated by generator/sync-issue-books.mjs — do not edit by hand) + - "Bitonic Sorter · Ken Batcher's fixed wiring of comparators, and what a circuit of sums and relu pays to run it. — handwired-bitonic-sorter" - "User Effort · A tiny circuit learns how long a click takes, then picks the faster layout — handwired-user-effort" - "JSON Search · Can a GPU search a hundred thousand JSON documents faster, one thread each, with the lab's own weighted-sum program? — handwired-json-search" - "Runtime Cost · What a CPU pays to run a handwired circuit: allocation, copying, and layers — handwired-runtime-cost" diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-1.stories.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-1.stories.tsx new file mode 100644 index 00000000..a084ec71 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-1.stories.tsx @@ -0,0 +1,28 @@ +// Bitonic Sorter — chapter 1 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5). +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-1'; + +/** + * Two Wires and a Fold — one comparator on two flat wires, Batcher's 24-comparator network as a ghost, then the + * wires stand up into the mountain [1, 2, 6, 7, 8, 5, 3, 0] and the half-cleaner folds it into a low and a high + * half; distance 2 and 1 finish the merge. + */ +function Chapter1() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Bitonic Sorter/Chapter 1', + component: Chapter1, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-1.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-1.tsx new file mode 100644 index 00000000..5601230b --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-1.tsx @@ -0,0 +1,951 @@ +// Two Wires and a Fold +// +// Bitonic Sorter (the Handwired labs) — chapter 1. Sources: +// wiki = Wikipedia "Bitonic sorter" rev 1373262602 (.orly-work/hw-books/inputs/bitonic/wikipedia-bitonic-sorter.wikitext) +// :14 (Batcher, 1968), :16 (bitonic: non-decreasing then non-increasing), :20 (length a power of two), +// :26 (the comparator; n = 2 is simply a comparator), :33-36 (the merge recursion), :41-42 (half-cleaner pairs), +// :45-46 (every element of one half above every element of the other) +// report = .orly-work/hw-books/bitonic-ref/report.txt +// §1 (n = 8: 24 comparators), §2 (stage list n = 8; the merge is stages 3-5 at distance 4, 2, 1), +// §4 (worked half-cleaner on [1, 2, 6, 7, 8, 5, 3, 0]), §4b (2,083 inputs, 415 rotated high halves, 0 failures), +// §6b (merge output [0, 1, 2, 3, 5, 6, 7, 8]) +// chenglou/handwired@4ea7fcb lab/ui-exploration/guide/sections/loops.md:185 (a fixed list of compare-exchange steps) +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan; captions are the fixed narration +// contract (verbatim). Every comparator, bar and swap on screen comes from the local port of bitonic-ref/bitonic.ts +// below, run at module scope; the §4b counter is recomputed here by enumerating all 4^8 sequences. +// +// The visual machine: eight wires. They begin lying flat (the two-wire comparator, then Batcher's 24-comparator +// network as a ghost), stand up into eight columns holding the mountain, and the half-cleaner's arcs fold it: +// distance 4 splits it into a low and a high half, distance 2 and 1 finish the merge. +import type { ReactNode } from 'react'; +import { curveCatmullRom, line } from 'd3'; +import { CAMERA_HOME, Camera, Timeline, cameraInterp, colors, ease } from '../../core'; +import type { CameraState, ChannelRef, SceneState } from '../../core'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +const bump = (u: number): number => Math.sin(Math.PI * clamp01(u)); +const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace'; +const commas = (n: number): string => String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ','); +const list = (a: number[]): string => `[${a.join(', ')}]`; + +const BAR = '#cbd5e1'; +const COOL = '#38bdf8'; +const PAIR_HUES = [colors.SECONDARY, colors.TEAL, colors.POSITIVE, colors.NEGATIVE]; + +// --------------------------------------------------------------------------- +// Batcher's network: a local port of bitonic-ref/bitonic.ts (exclusive loop bound) +// --------------------------------------------------------------------------- + +type Comparator = [i: number, j: number, ascending: boolean]; + +function bitonicNetwork(n: number): Comparator[][] { + const stages: Comparator[][] = []; + const place = (stage: number, c: Comparator) => { + (stages[stage] ??= []).push(c); + }; + const emitMerge = (low: number, count: number, up: boolean, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + for (let i = low; i < low + k; i++) place(start, [i, i + k, up]); + emitMerge(low, k, up, start + 1); + return 1 + emitMerge(low + k, k, up, start + 1); + }; + const emitSort = (low: number, count: number, up: boolean, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + const half = emitSort(low, k, true, start); + emitSort(low + k, k, false, start); + return half + emitMerge(low, count, up, start + half); + }; + emitSort(0, n, true, 0); + return stages; +} + +function applyStage(a: number[], stage: Comparator[]): number[] { + const out = a.slice(); + for (const [i, j, up] of stage) { + if (up ? out[i] > out[j] : out[i] < out[j]) [out[i], out[j]] = [out[j], out[i]]; + } + return out; +} + +const N = 8; +const NET = bitonicNetwork(N); // report §2: six stages +const COMPARATORS = NET.reduce((s, st) => s + st.length, 0); // 24 (report §1) +const MERGE = NET.slice(3); // the final merge: distance 4, 2, 1, all ascending (report §2 stages 3-5) +const DIST = MERGE.map((st) => st[0][1] - st[0][0]); +if (COMPARATORS !== 24 || DIST.join() !== '4,2,1' || MERGE.some((st) => st.some((c) => !c[2]))) { + throw new Error('network does not match report §1-2'); +} + +/** report §4: the worked mountain, and the array after each merge stage */ +const MOUNTAIN = [1, 2, 6, 7, 8, 5, 3, 0]; +const TRACE = MERGE.reduce((acc, st) => [...acc, applyStage(acc[acc.length - 1], st)], [MOUNTAIN]); +if (list(TRACE[1]) !== '[1, 2, 3, 0, 8, 5, 6, 7]' || list(TRACE[3]) !== '[0, 1, 2, 3, 5, 6, 7, 8]') { + throw new Error('half-cleaner trace does not match report §4 / §6b'); +} +/** slot of each value after k merge stages (the eight values are distinct, so a bar is its value) */ +const SLOT = TRACE.map((arr) => MOUNTAIN.map((v) => arr.indexOf(v))); +const LOW = TRACE[1].slice(0, 4); +const HIGH = TRACE[1].slice(4); + +const isStraight = (a: number[]): boolean => { + let i = 0; + while (i + 1 < a.length && a[i] <= a[i + 1]) i++; + while (i + 1 < a.length && a[i] >= a[i + 1]) i++; + return i === a.length - 1; +}; +const isRotated = (a: number[]): boolean => a.some((_, r) => isStraight([...a.slice(r), ...a.slice(0, r)])); +const HIGH_STRAIGHT = isStraight(HIGH); // false +const HIGH_ROTATED = isRotated(HIGH); // true + +/** report §4b: every article-bitonic 8-sequence over 0..3 through the half-cleaner */ +const SWEEP = (() => { + let inputs = 0, rotated = 0, failures = 0; + for (let code = 0; code < 4 ** N; code++) { + const a = Array.from({ length: N }, (_, i) => Math.floor(code / 4 ** i) % 4); + if (!isStraight(a)) continue; + inputs++; + const out = applyStage(a, MERGE[0]); + const lo = out.slice(0, 4), hi = out.slice(4); + if (!isStraight(hi) && isRotated(hi)) rotated++; + if (Math.max(...lo) > Math.min(...hi) || !isRotated(lo) || !isRotated(hi)) failures++; + } + return { inputs, rotated, failures }; +})(); + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; caption band y ≥ 630 stays empty) +// --------------------------------------------------------------------------- + +// lying flat: wire i is a horizontal line, bars lie along it from HX0 +const HX0 = 140; +const HY = (i: number) => 165 + 52 * i; +const H_UNIT = 26; +const H_END = 1000; +// the ghost network's rods: stage columns, overlapping rods take separate sub-columns +const NET_X0 = 440; +const SUB = 22; +const GAP = 46; +const levelsOf = (stage: Comparator[]): number[] => { + const lv: number[] = []; + stage.forEach(([i, j], k) => { + let l = 0; + while (stage.some(([a, b], m) => m < k && lv[m] === l && a <= j && i <= b)) l++; + lv.push(l); + }); + return lv; +}; +const RODS = (() => { + const rods: Array<{ i: number; j: number; x: number }> = []; + let x = NET_X0; + for (const stage of NET) { + const lv = levelsOf(stage); + stage.forEach(([i, j], k) => rods.push({ i, j, x: x + GAP / 2 + lv[k] * SUB })); + x += GAP + (Math.max(...lv) + 1) * SUB; + } + return rods; +})(); +const ROD0 = RODS[0]; // stage 0, wires 0 and 1: the chapter's first comparator + +// standing up: wire i is a column at VX(i), bars rise from VB +const VC = 570; +const PITCH = 84; +const VB = 500; +const V_UNIT = 30; +const V_TH = 44; +const V_TOP = VB - 252; +const SPL1 = 24; +const SPL2 = 10; +const vx = (i: number, s1: number, s2: number) => + VC + (i - 3.5) * PITCH + (i < 4 ? -1 : 1) * s1 * SPL1 + ((i >> 1) & 1 ? 1 : -1) * s2 * SPL2; +const VAL_Y = 524; +const IDX_Y = 547; +const HALF_Y = 576; +const MERGE_LV = MERGE.map(levelsOf); + +// the rotated-mountain inset above the high half +const IB = 226; +const I_UNIT = 11; + +// right-hand panel +const PX = 990; +const MY = (i: number) => 212 + 28 * i; +const MINI_X = [1022, 1118, 1205]; +const MINI_LBL = [1043, 1125, 1205]; + +const CAM_PAIR: CameraState = { x: 320, y: 200, k: 2.1 }; +const HOME: CameraState = CAMERA_HOME; +const CAM_ARCS: CameraState = { x: 570, y: 340, k: 1.2 }; +const CAM_RIGHT: CameraState = { x: 700, y: 352, k: 1.15 }; +const CAM_MACH: CameraState = { x: 570, y: 360, k: 1.15 }; +const CAM_END: CameraState = { x: 570, y: 372, k: 1.06 }; + +/** per-item progress from one group channel: item i starts `each` seconds after item i-1 */ +const staggerU = (u: number, i: number, n: number, each: number, dur: number): number => { + const total = dur + each * (n - 1); + return clamp01((u * total - i * each) / dur); +}; + +/** speaking time at ~150 words per minute, plus a breath */ +const speak = (text: string): number => Math.round((text.split(/\s+/).length / 2.5 + 0.6) * 10) / 10; +const HOLD = 0.8; + +// --------------------------------------------------------------------------- +// Timeline — twelve fixed captions +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const cam = tl.channel('cam', CAM_PAIR, cameraInterp); + const ch = (key: string, init = 0) => tl.channel(key, init); + const tw = (c: ChannelRef, to: number, at: number, dur: number, e = ease.enter) => tl.tween(c, to, { at, dur, ease: e }); + + // B0–B2 lying flat + const ifU = ch('decisionChip'); + const strikeU = ch('decisionStrike'); + const w01U = ch('wires01'); + const rodU = ch('rod0'); + const fixedU = ch('fixedTag'); + const compLbl = ch('comparatorLabel'); + const pairA = ch('pairA'); + const outLbl = ch('minMaxLabels'); + const flashA = ch('flashA'); + const tagA = ch('inOrderTag'); + const pairB = ch('pairB'); + const flashB = ch('flashB'); + const swapU = ch('swap'); + const tagB = ch('swapTag'); + const wiresU = ch('wires2to7'); + const rodsU = ch('ghostRods'); + const rodsOp = ch('ghostRodsOp', 1); + const plaqueU = ch('plaque'); + const sameU = ch('sameTag'); + // B3 stand up, the mountain + const rotU = ch('standUp'); + const growU = ch('barsGrow'); + const readU = ch('readAlong', -1); + const outlineU = ch('mountainOutline'); + const mtnLbl = ch('mountainLabel'); + const peakU = ch('peak'); + // B4–B6 the half-cleaner + const arcsU = MERGE.map((_, k) => ch(`arcs${DIST[k]}`)); + const arcsOp = MERGE.map((_, k) => ch(`arcs${DIST[k]}Op`, 1)); + const fireU = MERGE.map((_, k) => ch(`fire${DIST[k]}`)); + const stageTag = MERGE.map((_, k) => ch(`tag${DIST[k]}`)); + const pulseT = ch('pulse'); + const split1 = ch('splitHalves'); + const split2 = ch('splitQuarters'); + const tintU = ch('tint'); + const tableU = ch('table'); + const tableOp = ch('tableOp', 1); + const lowLbl = ch('lowLabel'); + const highLbl = ch('highLabel'); + const divU = ch('divider'); + const divOp = ch('dividerOp', 1); + const ceilU = ch('ceiling'); + const floorU = ch('floor'); + const bandU = ch('gapBand'); + const borderOp = ch('borderOp', 1); + const ineqU = ch('inequality'); + const crossU = ch('noCrossTag'); + // B7–B8 the rotated mountain + const lowDim = ch('lowDim', 1); + const valleyU = ch('valley'); + const valleyLbl = ch('valleyLabel'); + const valleyOp = ch('valleyOp', 1); + const liftU = ch('ghostLift'); + const ringU = ch('ring'); + const climbU = ch('climb'); + const climbLbl = ch('climbLabel'); + const insetOp = ch('insetOp', 1); + const statusU = ch('status'); + const wikiU = ch('wikiTag'); + const needU = ch('needTag'); + const countU = ch('count'); + const rotCountU = ch('rotatedCount'); + const failU = ch('failures'); + const panelOp = ch('panelOp', 1); + // B9–B11 the merge, as wiring + const finalLbl = ch('finalArray'); + const miniWires = ch('miniWires'); + const miniCol = MERGE.map((_, k) => ch(`mini${DIST[k]}`)); + const pairBracket = ch('twoWireBracket'); + const miniOp = ch('miniOp', 1); + const endU = ch('endLabel'); + const quietU = ch('quiet'); + + let t = 0.4; + const beat = (text: string): number => { + const t0 = t; + const d = speak(text); + tl.caption({ at: t0, dur: d, text }); + tl.hold(t0 + d, HOLD); + t = t0 + d + HOLD; + return t0; + }; + + // — B0 · a sorting network never decides — + let a = beat('Most sorting looks at the data and decides what to do next. A sorting network never decides. It is a fixed list of compare-and-swap steps, wired the same way for every input.'); + tw(ifU, 1, a + 0.6, 0.7); + tw(strikeU, 1, a + 4.4, 0.6, ease.draw); + tw(w01U, 1, a + 5.6, 1.3, ease.draw); + tw(rodU, 1, a + 6.8, 0.8, ease.draw); + tw(fixedU, 1, a + 10.2, 0.7); + + // — B1 · the comparator — + a = beat('The one part is the comparator. Two numbers go in. The smaller comes out on the top wire, the larger on the bottom. Already in order, nothing happens. Out of order, they trade places.'); + tw(fixedU, 0, a, 0.5, ease.move); + tw(ifU, 0, a, 0.5, ease.move); + tw(compLbl, 1, a + 0.4, 0.6); + tw(pairA, 1, a + 2.4, 0.7); + tw(outLbl, 1, a + 4.2, 0.6); + tw(flashA, 1, a + 7.6, 0.8, ease.linear); + tw(tagA, 1, a + 7.9, 0.6); + tw(pairA, 0, a + 9.7, 0.5, ease.move); + tw(tagA, 0, a + 9.7, 0.5, ease.move); + tw(pairB, 1, a + 10.0, 0.6); + tw(flashB, 1, a + 10.8, 1.2, ease.linear); + tw(swapU, 1, a + 10.9, 1.1, ease.move); + tw(tagB, 1, a + 11.6, 0.6); + + // — B2 · Batcher's network: 24 comparators — + a = beat('In nineteen sixty-eight, Ken Batcher showed how to wire comparators into a bitonic sorter. For eight numbers it takes twenty-four comparators, and the same twenty-four sort any list of eight.'); + tl.tween(cam, HOME, { at: a, dur: 1.5, ease: ease.move }); + for (const c of [pairB, tagB, outLbl, compLbl]) tw(c, 0, a, 0.6, ease.move); + tw(wiresU, 1, a + 0.5, 1.2, ease.draw); + tw(plaqueU, 1, a + 1.4, 0.7); + tw(rodsU, 1, a + 5.2, 2.6, ease.linear); + tw(sameU, 1, a + 9.6, 0.7); + + // — B3 · stand the wires up: the mountain — + a = beat('It rests on one shape: a sequence that climbs and then falls, which the name bitonic describes. Picture a mountain: one, two, six, seven, eight, five, three, zero.'); + for (const c of [rodsOp, plaqueU, sameU, rodU]) tw(c, 0, a, 0.7, ease.move); + tw(rotU, 1, a + 0.5, 1.5, ease.move); + tw(outlineU, 1, a + 2.4, 1.4, ease.draw); + tw(growU, 1, a + 2.2, 1.6, ease.enter); + tw(mtnLbl, 1, a + 4.0, 0.6); + tw(peakU, 1, a + 5.0, 0.5, ease.pop); + tw(readU, 8, a + 7.6, 4.0, ease.linear); + + // — B4 · the half-cleaner pairs wire i with wire i + 4 — + a = beat('Here is the half-cleaner. Pair each wire in the first half with its partner in the second half: wire zero with wire four, one with five, two with six, three with seven.'); + tl.tween(cam, CAM_ARCS, { at: a, dur: 1.4, ease: ease.move }); + tw(mtnLbl, 0, a, 0.6, ease.move); + tw(peakU, 0, a, 0.6, ease.move); + tw(outlineU, 0, a + 0.3, 0.8, ease.move); + tw(stageTag[0], 1, a + 0.8, 0.6); + tw(arcsU[0], 1, a + 6.6, 5.0, ease.linear); + tw(pulseT, 6, a + 6.6, 7.0, ease.linear); + + // — B5 · four comparators fire at once — + a = beat("Four comparators fire at once. Each pair's smaller number drops into the left half, the larger into the right. The left half is now one, two, three, zero. The right is eight, five, six, seven."); + tl.tween(cam, HOME, { at: a, dur: 1.3, ease: ease.move }); + tw(fireU[0], 1, a + 0.9, 1.2, ease.move); + tw(tableU, 1, a + 2.4, 2.4, ease.linear); + tw(tintU, 1, a + 3.4, 0.8); + tw(split1, 1, a + 3.6, 1.0, ease.move); + tw(arcsOp[0], 0.12, a + 4.4, 0.8, ease.move); + tw(stageTag[0], 0, a + 4.4, 0.6, ease.move); + tw(lowLbl, 1, a + 7.6, 0.6); + tw(highLbl, 1, a + 10.2, 0.6); + + // — B6 · the border: at most three, at least five — + a = beat('Check the border. Everything on the left is at most three, and everything on the right is at least five. Nothing needs to cross, so the two halves can be sorted separately.'); + tw(tableOp, 0, a, 0.6, ease.move); + tw(arcsOp[0], 0, a, 0.6, ease.move); + tw(divU, 1, a + 0.4, 1.0, ease.draw); + tw(ceilU, 1, a + 2.4, 0.9, ease.draw); + tw(floorU, 1, a + 4.6, 0.9, ease.draw); + tw(bandU, 1, a + 5.8, 0.6); + tw(ineqU, 1, a + 6.2, 0.6); + tw(crossU, 1, a + 8.6, 0.6); + + // — B7 · a rotated mountain — + a = beat('Each half is still a mountain, in a wider sense. The right half, eight, five, six, seven, dips like a valley, but start reading at the five and it climbs. A rotated mountain counts.'); + tl.tween(cam, CAM_RIGHT, { at: a, dur: 1.4, ease: ease.move }); + for (const c of [borderOp, ineqU, crossU]) tw(c, 0, a, 0.6, ease.move); + tw(lowDim, 0.12, a + 0.4, 0.8, ease.move); + tw(valleyU, 1, a + 3.4, 1.4, ease.draw); + tw(valleyLbl, 1, a + 4.4, 0.6); + tw(valleyLbl, 0, a + 7.0, 0.5, ease.move); + tw(liftU, 1, a + 7.0, 1.0, ease.move); + tw(valleyOp, 0, a + 7.0, 0.8, ease.move); + tw(ringU, 1, a + 8.2, 1.4, ease.move); + tw(climbU, 1, a + 9.6, 1.0, ease.draw); + tw(climbLbl, 1, a + 9.8, 0.6); + tw(statusU, 1, a + 11.2, 0.6); + + // — B8 · the definition gap, and the sweep that checks it — + a = beat("Wikipedia's definition allows only straight mountains, but the argument needs rotated ones too. Across all two thousand eighty-three eight-long mountains with values zero to three, the split never failed."); + tl.tween(cam, HOME, { at: a, dur: 1.3, ease: ease.move }); + tw(wikiU, 1, a + 0.6, 0.6); + tw(needU, 1, a + 3.2, 0.6); + tw(countU, 1, a + 5.6, 2.6, ease.move); + tw(rotCountU, 1, a + 8.4, 0.6); + tw(failU, 1, a + 11.4, 0.6, ease.pop); + + // — B9 · split again: distance 2, then 1 — + a = beat('So split again. Half-clean each half into quarters, then each quarter into pairs. Run on this input, the pieces come out as zero, one, two, three, five, six, seven, eight.'); + tl.tween(cam, CAM_MACH, { at: a, dur: 1.4, ease: ease.move }); + for (const c of [panelOp, insetOp, valleyU, divOp]) tw(c, 0, a, 0.6, ease.move); + tw(lowDim, 1, a + 0.2, 0.6, ease.move); + tw(stageTag[1], 1, a + 0.6, 0.5); + tw(arcsU[1], 1, a + 0.8, 0.9, ease.draw); + tw(fireU[1], 1, a + 2.2, 1.0, ease.move); + tw(split2, 1, a + 2.6, 0.9, ease.move); + tw(arcsOp[1], 0, a + 3.5, 0.5, ease.move); + tw(stageTag[1], 0, a + 3.5, 0.4, ease.move); + tw(stageTag[2], 1, a + 3.9, 0.5); + tw(arcsU[2], 1, a + 4.0, 0.8, ease.draw); + tw(fireU[2], 1, a + 5.0, 1.0, ease.move); + tw(arcsOp[2], 0, a + 6.3, 0.5, ease.move); + tw(stageTag[2], 0, a + 6.3, 0.4, ease.move); + tw(finalLbl, 1, a + 7.6, 0.7); + + // — B10 · three rounds: distance 4, 2, 1 — + a = beat('Three rounds of this do it for eight numbers: pairs at distance four, then two, then one. Two wires is the smallest case, and a bitonic sorter of that size is simply a comparator.'); + tl.tween(cam, HOME, { at: a, dur: 1.4, ease: ease.move }); + tw(finalLbl, 0, a, 0.6, ease.move); + tw(miniWires, 1, a + 0.5, 1.0, ease.draw); + tw(miniCol[0], 1, a + 3.4, 0.7); + tw(miniCol[1], 1, a + 4.6, 0.7); + tw(miniCol[2], 1, a + 5.6, 0.7); + tw(pairBracket, 1, a + 9.0, 0.7); + + // — B11 · the merging machine — + a = beat('That is the merging machine. Hand it any mountain whose length is a power of two, and it hands back a sorted line. What it cannot do yet is make mountains. That comes next.'); + tl.tween(cam, CAM_END, { at: a, dur: 1.6, ease: ease.move }); + tw(miniOp, 0, a, 0.8, ease.move); + tw(quietU, 1, a + 0.2, 1.0, ease.move); + tw(endU, 1, a + 1.4, 0.8); + tl.hold(t, 1.7); + + return { + tl, cam, ifU, strikeU, w01U, rodU, fixedU, compLbl, pairA, outLbl, flashA, tagA, pairB, flashB, swapU, tagB, + wiresU, rodsU, rodsOp, plaqueU, sameU, rotU, growU, readU, outlineU, mtnLbl, peakU, + arcsU, arcsOp, fireU, stageTag, pulseT, split1, split2, tintU, tableU, tableOp, lowLbl, highLbl, + divU, divOp, ceilU, floorU, bandU, borderOp, ineqU, crossU, + lowDim, valleyU, valleyOp, valleyLbl, liftU, ringU, climbU, climbLbl, insetOp, statusU, wikiU, needU, countU, rotCountU, + failU, panelOp, finalLbl, miniWires, miniCol, pairBracket, miniOp, endU, quietU, + }; +} + +const scene = buildScene(); + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +const smooth = line<[number, number]>().curve(curveCatmullRom.alpha(0.5)); + +function Txt({ x, y, children, size = 14, fill = colors.TEXT, anchor = 'middle', mono = false, weight, opacity = 1 }: { + x: number; y: number; children: ReactNode; size?: number; fill?: string; anchor?: 'start' | 'middle' | 'end'; + mono?: boolean; weight?: number; opacity?: number; +}) { + if (opacity <= 0.005) return null; + return ( + + {children} + + ); +} + +/** a horizontal bar lying on wire w (the flat two-wire demo) */ +function FlatBar({ v, y, dx = 0, opacity, labelOp = 1 }: { v: number; y: number; dx?: number; opacity: number; labelOp?: number }) { + if (opacity <= 0.005) return null; + return ( + + + + {v} + + + ); +} + +export function Render({ s }: { s: SceneState }) { + const v = (c: ChannelRef): number => s.get(c); + const cam = s.get(scene.cam); + + const rot = v(scene.rotU); + const s1 = v(scene.split1); + const s2 = v(scene.split2); + const X = (i: number) => vx(i, s1, s2); + const quiet = v(scene.quietU); + const fade = (op: number) => op * lerp(1, 0.12, quiet); + + // ---- bars: which merge stage is moving, and where each value stands + const fire = scene.fireU.map((c) => v(c)); + let k = -1; + for (let m = 0; m < fire.length; m++) if (fire[m] > 0) k = m; + const barX = (val: number): { x: number; u: number; moving: boolean } => { + const vi = MOUNTAIN.indexOf(val); + if (k < 0) return { x: X(SLOT[0][vi]), u: 0, moving: false }; + const from = SLOT[k][vi], to = SLOT[k + 1][vi]; + return { x: lerp(X(from), X(to), fire[k]), u: fire[k], moving: from !== to }; + }; + const done = fire.filter((f) => f >= 1).length; + const arrNow = TRACE[done]; + const grow = v(scene.growU); + const tint = v(scene.tintU); + const lowDim = v(scene.lowDim); + const readAt = v(scene.readU); + const pulse = 0.5 + 0.5 * Math.sin(2 * Math.PI * v(scene.pulseT)); + + // ---- the flat demo + const swap = v(scene.swapU); + const flashA = bump(v(scene.flashA)); + const flashB = bump(v(scene.flashB)); + const rodGlow = Math.max(flashA, flashB); + + // ---- panel + const panelOp = v(scene.panelOp); + const count = v(scene.countU); + + return ( + <> + + + {/* ================= B0 · the decision a network never makes ================= */} + {v(scene.ifU) > 0.005 && ( + + + + {'if (a > b) { … } else { … }'} + + 0 ? 1 : 0} /> + + never decides + + + )} + + {/* ================= the eight wires: flat, then standing ================= */} + {Array.from({ length: N }, (_, i) => { + const op = i < 2 ? v(scene.w01U) : staggerU(v(scene.wiresU), i - 2, 6, 0.08, 0.6); + if (op <= 0.005) return null; + const ax = lerp(HX0, X(i), rot); + const ay = lerp(HY(i), VB, rot); + const th = (-Math.PI / 2) * rot; + const dx = Math.cos(th), dy = Math.sin(th); + const s0 = lerp(-28, 0, rot); + const len = lerp(H_END - HX0, VB - V_TOP, rot); + const drawn = i < 2 ? clamp01(v(scene.w01U)) : 1; + const e = s0 + (len - s0) * drawn; + const lit = rot > 0.99 && i < 4 ? lowDim : 1; + return ( + + + + {rot > 0.5 ? `w${i}` : i} + + + ); + })} + + {/* ================= B1 · the comparator on wires 0 and 1 ================= */} + {v(scene.rodU) > 0.005 && ( + 0 ? 1 : 0}> + {rodGlow > 0.01 && } + 0.01 ? colors.WARM : colors.TEXT} strokeWidth={2.5} /> + + + + )} + + comparator + + + min + + + max + + + + {/* out of order: 7 above 3, the pair crosses over */} + + + + a fixed list of compare-and-swap steps + + + already in order: nothing happens + + + out of order: they trade places + + + {/* ================= B2 · the ghost of Batcher's 8-wire network ================= */} + {v(scene.rodsOp) * v(scene.rodsU) > 0.005 && ( + + {RODS.map((r, n) => { + if (n === 0) return null; + const u = staggerU(v(scene.rodsU), n - 1, RODS.length - 1, 0.1, 0.3); + if (u <= 0) return null; + return ( + + + + + + ); + })} + + )} + {v(scene.plaqueU) > 0.005 && ( + + + + Ken Batcher, 1968 + + + the bitonic sorter + + + + {Math.max(1, Math.round(1 + v(scene.rodsU) * (COMPARATORS - 1)))} + + + comparators + + + n = 8 + + + + the same 24 for any list of 8 + + + )} + + {/* ================= B4–B9 · comparator arcs (one hue per pair) ================= */} + {MERGE.map((stage, si) => { + const u = v(scene.arcsU[si]); + const op = v(scene.arcsOp[si]); + if (u <= 0.005 || op <= 0.005) return null; + const f = bump(fire[si]); + const fired = fire[si] > 0; + return ( + + {stage.map(([i, j], n) => { + const du = si === 0 ? staggerU(u, n, 4, 0.31, 0.2) : u; + if (du <= 0) return null; + const peak = V_TOP - 34 - MERGE_LV[si][n] * 24; + const xi = X(i), xj = X(j); + const hue = PAIR_HUES[n]; + const glow = fired ? f : si === 0 ? 0.35 * pulse : 0; + return ( + + {glow > 0.02 && } + + + = 1 ? 1 : 0} /> + {si === 0 && ( + <> + + = 1 ? 1 : 0) * (1 - clamp01(fire[0] * 2))} /> + + )} + + ); + })} + + ); + })} + {MERGE.map((_, si) => ( + + {si === 0 ? 'half-cleaner: x[i] vs x[i + 4]' : `half-clean again: distance ${DIST[si]}`} + + ))} + + {/* ================= B6 · the border ================= */} + {v(scene.divU) > 0.005 && ( + + )} + {s2 > 0.005 && + [(X(1) + X(2)) / 2, (X(5) + X(6)) / 2].map((x, n) => ( + + ))} + {v(scene.borderOp) > 0.005 && ( + + {v(scene.bandU) > 0.005 && ( + + )} + 0 ? 1 : 0} /> + + ceiling 3 + + 0 ? 1 : 0} /> + + floor 5 + + + )} + + {/* ================= the bars (one per value) ================= */} + {grow > 0.005 && + MOUNTAIN.map((val, n) => { + const g = staggerU(grow, n, N, 0.12, 0.6); + if (g <= 0) return null; + const { x, u, moving } = barX(val); + const h = Math.max(4, val * V_UNIT) * g; + const dim = SLOT[1][n] < 4 ? lowDim : 1; + const labelOp = moving ? clamp01(1 - 2.5 * bump(u)) : 1; + const reading = Math.floor(readAt) === n && readAt < 8; + const tinted = val <= 3 ? COOL : colors.WARM; + // a straight swap crosses its partner mid-flight: go glassy and outlined so both stay readable + const glass = moving ? 1 - 0.65 * bump(u) : 1; + const edge = moving && bump(u) > 0.02 ? (val <= 3 ? COOL : colors.WARM) : 'none'; + return ( + + + {tint > 0.005 && } + + {val} + + + ); + })} + {/* the mountain's outline and its peak */} + {v(scene.outlineU) > 0.005 && ( + [X(i), VB - val * V_UNIT - 6] as [number, number])) ?? ''} + fill="none" + stroke={colors.POSITIVE} + strokeWidth={2.5} + pathLength={1} + strokeDasharray="1 1" + strokeDashoffset={1 - clamp01(v(scene.outlineU) * 1.2)} + opacity={Math.min(1, v(scene.outlineU) * 3)} + /> + )} + + bitonic: up, then down + + {v(scene.peakU) > 0.005 && ( + + + + peak + + + )} + + {/* half labels: the live contents of each half */} + + {`low half ${list(arrNow.slice(0, 4))}`} + + + {`high half ${list(arrNow.slice(4))}`} + + + {/* ================= B7 · the valley, and its rotation ================= */} + {v(scene.valleyU) > 0.005 && ( + [X(i), VB - TRACE[1][i] * V_UNIT - 6] as [number, number])) ?? ''} + fill="none" + stroke={colors.NEGATIVE} + strokeWidth={2.5} + pathLength={1} + strokeDasharray="1 1" + strokeDashoffset={1 - v(scene.valleyU)} + opacity={v(scene.valleyOp)} + /> + )} + + dips like a valley + + {v(scene.liftU) * v(scene.insetOp) > 0.005 && ( + + {HIGH.map((val, n) => { + const lift = v(scene.liftU); + const ring = v(scene.ringU); + const s = 4 + n; + const rs = 4 + ((n + 3) % 4); + const x = lerp(X(s), X(rs), ring); + const wrapY = n === 0 ? -48 * bump(ring) : 0; + const base = lerp(VB, IB, lift) + wrapY; + const h = Math.max(4, val * lerp(V_UNIT, I_UNIT, lift)); + const w = lerp(V_TH, 30, lift); + return ( + + + + {val} + + + ); + })} + {v(scene.ringU) > 0.005 && v(scene.ringU) < 0.999 && ( + + )} + {v(scene.climbU) > 0.005 && ( + [X(i), IB - [5, 6, 7, 8][n] * I_UNIT - 6] as [number, number])) ?? ''} + fill="none" + stroke={colors.POSITIVE} + strokeWidth={2.5} + pathLength={1} + strokeDasharray="1 1" + strokeDashoffset={1 - v(scene.climbU)} + /> + )} + + start at the 5: it climbs + + + [5, 6, 7, 8] + + + )} + + {/* ================= right-hand panel ================= */} + {/* B5 · the half-cleaner table, from the real values */} + {v(scene.tableU) * v(scene.tableOp) > 0.005 && ( + + + half-cleaner, n = 8 + + {[['i', 1000], ['x[i]', 1040], ['x[i+4]', 1100], ['min', 1170], ['max', 1225]].map(([h, x]) => ( + + {h} + + ))} + + {[0, 1, 2, 3].map((i) => ( + + {[i, MOUNTAIN[i], MOUNTAIN[i + 4], TRACE[1][i], TRACE[1][i + 4]].map((c, n) => ( + = 3 ? 700 : 400} fill={n === 3 ? COOL : n === 4 ? colors.WARM : colors.TEXT}> + {c} + + ))} + + ))} + + )} + {/* B6 · the inequality */} + + {`max(low) = ${Math.max(...LOW)} ≤ min(high) = ${Math.min(...HIGH)}`} + + + nothing needs to cross + + + sort each half on its own + + {/* B7–B8 · status of the high half, then the sweep */} + {panelOp > 0.005 && ( + + + {`high half ${list(HIGH)}`} + + + {`straight mountain: ${HIGH_STRAIGHT ? 'yes' : 'no'}`} + + + {`rotated mountain: ${HIGH_ROTATED ? 'yes' : 'no'}`} + + + Wikipedia's definition: straight only + + + the split needs rotated ones too + + + + {commas(SWEEP.inputs * count)} + + + bitonic inputs + + + + {commas(SWEEP.rotated)} + + + rotated high halves + + + {commas(SWEEP.failures)} + + + failures + + + values 0..3, n = 8, every one checked + + + )} + + {list(TRACE[3])} + + + {/* B10 · the merge, drawn as wiring: distance 4, 2, 1 */} + {v(scene.miniWires) * v(scene.miniOp) > 0.005 && ( + + + the merge, as wiring + + {Array.from({ length: N }, (_, i) => ( + + ))} + {MERGE.map((stage, si) => { + const u = v(scene.miniCol[si]); + if (u <= 0.005) return null; + return ( + + + {`distance ${DIST[si]}`} + + {stage.map(([i, j], n) => { + const x = MINI_X[si] + MERGE_LV[si][n] * 14; + const hot = si === 2 && n === 0 && v(scene.pairBracket) > 0.01; + const col = hot ? colors.WARM : colors.TEXT; + return ( + + + + + + ); + })} + + ); + })} + {v(scene.pairBracket) > 0.005 && ( + + + + 2 wires = one comparator + + + 4 comparators per round, 3 rounds + + + )} + + )} + + {/* B11 · the payoff */} + + bitonic in, sorted out + + + any mountain whose length is a power of two + + + + {/* ================= screen-space header ================= */} + + + 1 · Two Wires and a Fold + + + ); +} + +export const vizScene = () => scene; diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-2.stories.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-2.stories.tsx new file mode 100644 index 00000000..b61f322c --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-2.stories.tsx @@ -0,0 +1,28 @@ +// Bitonic Sorter — chapter 2 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5). +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-2'; + +/** + * Building the Mountains — eight discs ride an 8-wire bitonic network through its six stages: pairs set to climb + * or fall, two mountains of four, one mountain of eight, the half-cleaner, sorted; then the work count, the + * distance ribbon, a second input on the same wiring, and the zero-one and random checks. + */ +function Chapter2() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Bitonic Sorter/Chapter 2', + component: Chapter2, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-2.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-2.tsx new file mode 100644 index 00000000..a7246729 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-2.tsx @@ -0,0 +1,1013 @@ +// Building the Mountains +// +// Bitonic Sorter (Handwired labs) — chapter 2. Sources: +// Wikipedia "Bitonic sorter" rev 1373262602 (wiki:LINE): 80-82 (sorters build the monotonic halves bottom-up), +// 138 (the stage count) +// .orly-work/hw-books/bitonic-ref/bitonic.ts:59-89 (bitonicNetwork: the stage list depends only on n), +// 91-104 (applyStage, applyNetwork) +// .orly-work/hw-books/bitonic-ref/report.txt §1 (n = 8: 6 stages, 24 comparators), §2 (the n = 8 stage list: +// pairs, asc/desc), §2b (the n = 16 stage list, for the ghost), §3 (the worked trace from +// [3, 7, 4, 8, 6, 2, 1, 5]), §5 (256 and 65,536 zero-one inputs, 0 not sorted), §5b (2,000 random arrays per +// size, values 0..999, n = 2 to 64, 0 mismatches), §6 (the second input, [750, 653, 936, 503, 164, 498, 670, +// 702], the first random n = 8 array the report prints) +// .orly-work/hw-books/inputs/bitonic-sorter.md "What the checks cover" (n = 32 and 64 only sampled) +// chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46 lab/ui-exploration/guide/sections/loops.md:185 +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan; captions are the fixed narration +// contract (verbatim), fact-checked by Haiku 5.5 (docs/book-plans/handwired-bitonic-sorter/review.md). +// +// Every disc, bar, table cell and swap on screen comes from a line-for-line local port of bitonic.ts run at module +// scope; the module throws if the trace ever disagrees with report.txt §3 or the stage list with §2. The zero-one +// counters are recomputed here too (all 256 lists at 8 wires, all 65,536 at 16). The random-array strip (§5b) is +// quoted from the report, not recomputed (its seeded generator is not ported). One change to the plan: beat 9's +// second input is the report's own first random n = 8 array instead of an invented one. +import 'katex/dist/katex.min.css'; +import { interpolateRgb } from 'd3'; +import { CAMERA_HOME, Camera, MathLabel, STAGE_W, Timeline, cameraInterp, colors, ease } from '../../core'; +import type { CameraState, SceneState } from '../../core'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +/** 0 → 1 → 0 over [a, b] of u */ +const bump = (u: number, a: number, b: number): number => { + const v = (u - a) / Math.max(1e-6, b - a); + return v <= 0 || v >= 1 ? 0 : Math.sin(Math.PI * v); +}; +const smooth = (u: number): number => ease.move(clamp01(u)); +const MONO = colors.font.mono; +const UI = colors.font.ui; +const CH13 = 7.83; // mono advance at 13 px + +// --------------------------------------------------------------------------- +// The network and the traces (a local port of bitonic.ts, exclusive bound) +// --------------------------------------------------------------------------- + +type Comparator = [i: number, j: number, ascending: boolean]; + +function bitonicNetwork(n: number): Comparator[][] { + const stages: Comparator[][] = []; + const place = (stage: number, c: Comparator) => { + (stages[stage] ??= []).push(c); + }; + const emitMerge = (low: number, count: number, direction: 0 | 1, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + for (let i = low; i < low + k; i++) place(start, [i, i + k, direction === 1]); + emitMerge(low, k, direction, start + 1); + return 1 + emitMerge(low + k, k, direction, start + 1); + }; + const emitSort = (low: number, count: number, direction: 0 | 1, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + const half = emitSort(low, k, 1, start); + emitSort(low + k, k, 0, start); + return half + emitMerge(low, count, direction, start + half); + }; + const depth = emitSort(0, n, 1, 0); + const k = Math.log2(n); + if (depth !== (k * (k + 1)) / 2 || stages.length !== depth) throw new Error(`bitonic ch2: stage count for n=${n}`); + return stages; +} + +/** applyStage, tracking which disc (by its input index) sits on each wire. */ +function applyStage(a: number[], ids: number[], stage: Comparator[], swaps: boolean[]): void { + for (const [i, j, asc] of stage) { + const sw = asc ? a[i] > a[j] : a[i] < a[j]; + swaps.push(sw); + if (sw) { + [a[i], a[j]] = [a[j], a[i]]; + [ids[i], ids[j]] = [ids[j], ids[i]]; + } + } +} + +type Run = { + input: number[]; + rows: number[][]; // rows[0] = input, rows[s + 1] = after stage s + wireOf: number[][]; // wireOf[s][disc] = the wire disc sits on after s stages + swaps: boolean[][]; // swaps[s][c] = comparator c of stage s traded its pair + rank: number[]; // rank[disc] = 0..7 by value (the disc's colour) + vmax: number; +}; + +const NET = bitonicNetwork(8); +const N_STAGES = NET.length; // 6 +const N_COMPARATORS = NET.reduce((n, s) => n + s.length, 0); // 24 + +function trace(input: number[]): Run { + const a = [...input]; + const ids = input.map((_, i) => i); + const rows = [[...a]]; + const wireOf = [ids.map((_, d) => ids.indexOf(d))]; + const swaps: boolean[][] = []; + for (const stage of NET) { + const sw: boolean[] = []; + applyStage(a, ids, stage, sw); + swaps.push(sw); + rows.push([...a]); + wireOf.push(input.map((_, d) => ids.indexOf(d))); + } + const sorted = [...input].sort((x, y) => x - y); + return { input, rows, wireOf, swaps, rank: input.map((v) => sorted.indexOf(v)), vmax: sorted[sorted.length - 1] }; +} + +const RUN_A = trace([3, 7, 4, 8, 6, 2, 1, 5]); +const RUN_B = trace([750, 653, 936, 503, 164, 498, 670, 702]); +const RUNS = [RUN_A, RUN_B]; + +// report.txt §3, verbatim; §2, verbatim. +const REPORT_TRACE = [ + [3, 7, 4, 8, 6, 2, 1, 5], + [3, 7, 8, 4, 2, 6, 5, 1], + [3, 4, 8, 7, 5, 6, 2, 1], + [3, 4, 7, 8, 6, 5, 2, 1], + [3, 4, 2, 1, 6, 5, 7, 8], + [2, 1, 3, 4, 6, 5, 7, 8], + [1, 2, 3, 4, 5, 6, 7, 8], +]; +const STAGE_LINES = NET.map((st, s) => `stage ${s}: ${st.map(([i, j, asc]) => `(${i},${j} ${asc ? 'asc' : 'desc'})`).join(' ')}`); +const REPORT_STAGES = [ + 'stage 0: (0,1 asc) (2,3 desc) (4,5 asc) (6,7 desc)', + 'stage 1: (0,2 asc) (1,3 asc) (4,6 desc) (5,7 desc)', + 'stage 2: (0,1 asc) (2,3 asc) (4,5 desc) (6,7 desc)', + 'stage 3: (0,4 asc) (1,5 asc) (2,6 asc) (3,7 asc)', + 'stage 4: (0,2 asc) (1,3 asc) (4,6 asc) (5,7 asc)', + 'stage 5: (0,1 asc) (2,3 asc) (4,5 asc) (6,7 asc)', +]; +if (REPORT_TRACE.some((row, r) => row.join() !== RUN_A.rows[r].join())) throw new Error('bitonic ch2: trace disagrees with report §3'); +if (REPORT_STAGES.some((l, s) => l !== STAGE_LINES[s])) throw new Error('bitonic ch2: stage list disagrees with report §2'); +if (N_STAGES !== 6 || N_COMPARATORS !== 24) throw new Error('bitonic ch2: expected 6 stages and 24 comparators'); +if (RUN_B.rows[6].join() !== [...RUN_B.input].sort((x, y) => x - y).join()) throw new Error('bitonic ch2: second input not sorted'); +const SWAP_TOTAL = RUNS.map((r) => r.swaps.flat().filter(Boolean).length); + +// The zero-one principle, exhaustively (report.txt §5): every 0/1 list through 8 and 16 wires. +function zeroOne(n: number): boolean[] { + const net = bitonicNetwork(n); + const out: boolean[] = []; + const a = new Array(n); + for (let m = 0; m < 1 << n; m++) { + for (let i = 0; i < n; i++) a[i] = (m >> i) & 1; + for (const st of net) for (const [i, j, asc] of st) if (asc ? a[i] > a[j] : a[i] < a[j]) [a[i], a[j]] = [a[j], a[i]]; + let ok = true; + for (let i = 1; i < n; i++) if (a[i - 1] > a[i]) ok = false; + out.push(ok); + } + return out; +} +const ZERO8 = zeroOne(8); +const ZERO16_SORTED = zeroOne(16).filter(Boolean).length; +const ZERO8_SORTED = ZERO8.filter(Boolean).length; +if (ZERO8.length !== 256 || ZERO8_SORTED !== 256 || ZERO16_SORTED !== 65536) throw new Error('bitonic ch2: zero-one check failed'); + +// The n = 16 network for the ghost (report.txt §2b). +const NET16 = bitonicNetwork(16); + +const fmtInt = (v: number): string => v.toLocaleString('en-US'); + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; y ≥ 630 is the caption band; 0..76 the title band) +// --------------------------------------------------------------------------- + +const ASC = '#38bdf8'; // smaller value to the upper wire +const DESC = '#fb923c'; // smaller value to the lower wire +const valueColor = interpolateRgb('#64748b', colors.TEAL); +const rankColor = (rank: number): string => valueColor(rank / 7); + +const WY = (i: number): number => 140 + i * 50; // wire i +const WX0 = 96; +const WX1 = 790; +const COL = [190, 300, 380, 510, 600, 680]; // stage columns, grouped by merge +const REST = [128, 245, 340, 445, 555, 640, 740]; // where the discs wait between stages +const DIST = NET.map((st) => st[0][1] - st[0][0]); // 1 2 1 4 2 1 +const rodOff = (i: number, d: number): number => ((i % d) - (d - 1) / 2) * 12; +const DISC_R = 15; +const STAGE_LABEL_Y = 112; +const CHIP_Y = 524; + +// right panel: the trace table, and the same list as bars +const CELL0 = 904; +const CELL_P = 42; +const CELL_W = 38; +const cellX = (j: number): number => CELL0 + j * CELL_P; +const ROW_Y = (r: number): number => 136 + r * 27; +const BAR_BASE = 566; +const BAR_H = 168; +const BAR_W = 28; +const barCx = (j: number): number => cellX(j) + CELL_W / 2; +const BRACKET_Y = 578; + +// ghost n = 16 (beat 8): columns grouped 1 | 2 | 3 | 4 +const G16_COL = (() => { + const xs: number[] = []; + let x = 884; + [1, 2, 3, 4].forEach((len, m) => { + if (m > 0) x += 40; + for (let c = 0; c < len; c++) { + xs.push(x); + if (c < len - 1) x += 30; + } + }); + return xs; +})(); +const G16_Y = (i: number): number => 128 + i * 22; + +// verification board (beats 10 and 11) +const GRID = { x: 880, y: 120, pitch: 15, cell: 12 }; +const N16_BAR = { x: 880, y: 404, w: 350, h: 14 }; +const SIZES = [2, 4, 8, 16, 32, 64]; + +const CAM_START: CameraState = { x: 640, y: 350, k: 1.04 }; +const CAM_S0: CameraState = { x: 380, y: 330, k: 1.22 }; +const CAM_PROFILE: CameraState = { x: 1070, y: 470, k: 1.45 }; +const CAM_S3: CameraState = { x: 470, y: 330, k: 1.22 }; +const CAM_WIDE: CameraState = { x: 640, y: 352, k: 0.97 }; + +const SOURCES = [ + 'wiki:80-82 · report.txt §2', + 'report.txt §2 stage 0, §3', + 'report.txt §2 stages 1-2', + 'report.txt §3 after stage 2 · wiki:80-82', + 'report.txt §2 stage 3, §3', + 'report.txt §3 after stages 4-5', + 'report.txt §1 n = 8', + 'report.txt §2 · wiki:138', + 'bitonic.ts:59 · report.txt §2, §6', + 'report.txt §5', + 'report.txt §5b · research "What the checks cover"', +]; + +const CAPTIONS = [ + 'A shuffled list is not a mountain, so build them from the bottom up. Any two numbers form a mountain already, and one comparator can set them to climb or to fall.', + 'Take three, seven, four, eight, six, two, one, five. Stage zero compares neighbors, and the second and fourth pairs run downhill. Out comes three, seven, eight, four, two, six, five, one.', + 'Now there are two mountains of four. Merge each with a half-cleaner, across a distance of two and then one. The first block sorts upward, the second downward.', + 'That gives three, four, seven, eight, then six, five, two, one. Sorted up beside sorted down is exactly a mountain, which is why the directions alternate. Two sorted runs became one bigger mountain.', + 'Stage three is the half-cleaner from before, across a distance of four. The four smallest values land on the left, three, four, two, one, and the four largest on the right.', + 'Two more stages finish it, at distance two and then one. After the sixth stage: one, two, three, four, five, six, seven, eight.', + 'Count the work. Six stages, each with four comparators on separate wires, so all four fire at the same moment. Twenty-four comparators, but only six steps in time.', + 'The distances tell the story: one, then two and one, then four, two and one. Each merge is one stage longer than the last, so eight numbers take one plus two plus three, six stages.', + 'Notice what the comparators never do: choose where to look. The wiring is the same for every input, and only the swaps depend on the values. That is what lets a fixed machine run it.', + 'Does it sort everything? We ran all two hundred fifty-six lists of zeros and ones through eight wires, and all sixty-five thousand five hundred thirty-six through sixteen. Every one came out sorted.', + 'Two thousand random lists per size, from two up to sixty-four wires, also matched the built-in sort. Thirty-two and sixty-four wires were sampled this way, not checked exhaustively.', +]; +/** ~150 words per minute; a hyphenated number is two spoken words */ +const speak = (text: string): number => { + const words = text.split(/\s+/).length + (text.match(/-/g)?.length ?? 0); + return Math.round((words / 2.5 + 0.4) * 10) / 10; +}; + +// --------------------------------------------------------------------------- +// Timeline — eleven fixed captions +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const cam = tl.channel('cam', CAM_START, cameraInterp); + const src = tl.channel('src', 0); + + const netOp = tl.channel('netOp', 1); // the whole network whispers when the panel has the spotlight + const panelOp = tl.channel('panelOp', 1); // trace table + bars + const wiresU = tl.channel('wiresU', 0); + const discsU = tl.channel('discsU', 0); + const discOp = tl.channel('discOp', 1); + const which = tl.channel('which', 0); // 0: the worked input · 1: the second input + const run = tl.channel('run', 0); // 0..6 stages applied (worked input) + const runB = tl.channel('runB', 0); // 0..6 stages applied (second input) + const rodsU = tl.channel('rodsU', 0); // stage columns drawn, 0..6 + const legendU = tl.channel('legendU', 0); + const chips = Array.from({ length: N_STAGES }, (_, s) => tl.channel(`chip${s}`, 0)); + const tableU = tl.channel('tableU', 0); + const barsU = tl.channel('barsU', 0); + const block2U = tl.channel('block2U', 0); + const block4U = tl.channel('block4U', 0); + const block8U = tl.channel('block8U', 0); + const outlineU = tl.channel('outlineU', 0); + const upU = tl.channel('upU', 0); + const downU = tl.channel('downU', 0); + const m8U = tl.channel('m8U', 0); + const bracesU = tl.channel('bracesU', 0); + const sortedU = tl.channel('sortedU', 0); + const sweepU = tl.channel('sweepU', -1); + const countU = tl.channel('countU', 0); + const totalU = tl.channel('totalU', 0); + const stepsU = tl.channel('stepsU', 0); + const distU = tl.channel('distU', 0); + const mergeU = [1, 2, 3].map((m) => tl.channel(`merge${m}U`, 0)); + const sumU = tl.channel('sumU', 0); + const ghostU = tl.channel('ghostU', 0); + const ghostSumU = tl.channel('ghostSumU', 0); + const fixedU = tl.channel('fixedU', 0); + const swapsU = tl.channel('swapsU', 0); + const boardU = tl.channel('boardU', 0); + const zeroU = tl.channel('zeroU', 0); // 0..256 lists checked at 8 wires + const n16U = tl.channel('n16U', 0); // 0..1 of the 65,536 at 16 wires + const stripU = tl.channel('stripU', 0); + const sizesU = tl.channel('sizesU', 0); // 0..6 chips + const sampledU = tl.channel('sampledU', 0); + + let t = 0.4; + const beat = (i: number): [number, number] => { + const at = t; + const dur = speak(CAPTIONS[i]); + tl.caption({ at, dur, text: CAPTIONS[i] }); + tl.set(src, i, Math.max(0, at - 0.1)); + t = tl.hold(at + dur, 0.7); + return [at, dur]; + }; + + // — B0 · bare wires, eight discs; any pair is already a mountain — + { + const [b] = beat(0); + tl.tween(wiresU, 1, { at: b + 0.1, dur: 1.2, ease: ease.draw }); + tl.tween(discsU, 1, { at: b + 0.8, dur: 1.2, ease: ease.linear }); + tl.tween(tableU, 1, { at: b + 1.6, dur: 0.7, ease: ease.enter }); + tl.tween(barsU, 1, { at: b + 2.2, dur: 0.9, ease: ease.enter }); + tl.tween(block2U, 1, { at: b + 5.0, dur: 0.7, ease: ease.enter }); + tl.tween(legendU, 1, { at: b + 8.6, dur: 0.7, ease: ease.enter }); + } + // — B1 · stage 0: neighbours, the second and fourth pairs downhill — + { + const [b, d] = beat(1); + tl.tween(cam, CAM_S0, { at: b, dur: 1.4, ease: ease.move }); + tl.tween(panelOp, 0, { at: b, dur: 0.6, ease: ease.move }); + tl.tween(rodsU, 1, { at: b + 0.8, dur: 1.0, ease: ease.draw }); + tl.tween(chips[0], 1, { at: b + 1.6, dur: 0.6, ease: ease.enter }); + tl.tween(run, 1, { at: b + 4.6, dur: 3.0, ease: ease.linear }); + tl.tween(cam, CAMERA_HOME, { at: b + d - 2.6, dur: 1.4, ease: ease.move }); + tl.tween(panelOp, 1, { at: b + d - 2.2, dur: 0.8, ease: ease.move }); + } + // — B2 · two mountains of four, merged at distance 2 then 1 — + { + const [b] = beat(2); + tl.tween(chips[0], 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(block2U, 0, { at: b + 0.2, dur: 0.5, ease: ease.move }); + tl.tween(block4U, 1, { at: b + 0.6, dur: 0.7, ease: ease.enter }); + tl.tween(rodsU, 3, { at: b + 1.6, dur: 1.6, ease: ease.draw }); + tl.tween(chips[1], 1, { at: b + 1.8, dur: 0.6, ease: ease.enter }); + tl.tween(chips[2], 1, { at: b + 2.6, dur: 0.6, ease: ease.enter }); + tl.tween(run, 3, { at: b + 4.4, dur: 5.0, ease: ease.linear }); + } + // — B3 · sorted up beside sorted down is a mountain — + { + const [b] = beat(3); + tl.tween(netOp, 0.12, { at: b, dur: 0.4, ease: ease.move }); + tl.tween(tableU, 0, { at: b, dur: 0.4, ease: ease.move }); // the push would cut it at the top edge + tl.tween(cam, CAM_PROFILE, { at: b + 0.3, dur: 1.4, ease: ease.move }); + chips.slice(1, 3).forEach((c) => tl.tween(c, 0, { at: b, dur: 0.5, ease: ease.move })); + tl.tween(upU, 1, { at: b + 1.6, dur: 0.7, ease: ease.enter }); + tl.tween(downU, 1, { at: b + 3.2, dur: 0.7, ease: ease.enter }); + tl.tween(outlineU, 1, { at: b + 5.2, dur: 1.5, ease: ease.draw }); + tl.tween(block4U, 0, { at: b + 8.6, dur: 0.6, ease: ease.move }); + tl.tween(block8U, 1, { at: b + 8.8, dur: 0.7, ease: ease.enter }); + tl.tween(m8U, 1, { at: b + 9.2, dur: 0.7, ease: ease.pop }); + } + // — B4 · stage 3, the half-cleaner across a distance of four — + { + const [b] = beat(4); + tl.tween(cam, CAM_S3, { at: b, dur: 1.4, ease: ease.move }); + tl.tween(panelOp, 0, { at: b, dur: 0.6, ease: ease.move }); + tl.tween(netOp, 1, { at: b + 1.2, dur: 0.6, ease: ease.move }); // once the camera has brought the wires back + tl.tween(outlineU, 0, { at: b + 0.6, dur: 0.4, ease: ease.move }); + tl.tween(upU, 0, { at: b + 0.6, dur: 0.4, ease: ease.move }); + tl.tween(downU, 0, { at: b + 0.6, dur: 0.4, ease: ease.move }); + tl.tween(m8U, 0, { at: b + 0.6, dur: 0.4, ease: ease.move }); + tl.tween(rodsU, 4, { at: b + 1.0, dur: 1.0, ease: ease.draw }); + tl.tween(chips[3], 1, { at: b + 1.6, dur: 0.6, ease: ease.enter }); + tl.tween(run, 4, { at: b + 3.6, dur: 3.0, ease: ease.linear }); + tl.tween(bracesU, 1, { at: b + 7.0, dur: 0.8, ease: ease.enter }); + } + // — B5 · distance 2, then 1: sorted — + { + const [b, d] = beat(5); + tl.tween(cam, CAMERA_HOME, { at: b, dur: 1.3, ease: ease.move }); + tl.tween(bracesU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(chips[3], 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(panelOp, 1, { at: b + 0.4, dur: 0.8, ease: ease.move }); + tl.tween(tableU, 1, { at: b + 0.4, dur: 0.8, ease: ease.move }); + tl.tween(rodsU, 6, { at: b + 0.6, dur: 1.2, ease: ease.draw }); + tl.tween(chips[4], 1, { at: b + 0.9, dur: 0.6, ease: ease.enter }); + tl.tween(chips[5], 1, { at: b + 1.5, dur: 0.6, ease: ease.enter }); + tl.tween(run, 6, { at: b + 2.2, dur: 4.6, ease: ease.linear }); + tl.tween(sortedU, 1, { at: b + d - 2.0, dur: 0.6, ease: ease.pop }); + } + // — B6 · count the work: 6 stages × 4 comparators — + { + const [b] = beat(6); + tl.tween(cam, CAM_WIDE, { at: b, dur: 1.4, ease: ease.move }); + chips.slice(4).forEach((c) => tl.tween(c, 0, { at: b, dur: 0.5, ease: ease.move })); + tl.tween(panelOp, 0.12, { at: b + 0.2, dur: 0.8, ease: ease.move }); + tl.tween(block8U, 0, { at: b + 0.2, dur: 0.6, ease: ease.move }); + tl.tween(countU, 1, { at: b + 1.0, dur: 0.7, ease: ease.enter }); + tl.tween(sweepU, N_STAGES, { at: b + 1.6, dur: 4.8, ease: ease.linear }); + tl.tween(totalU, 1, { at: b + 6.4, dur: 0.7, ease: ease.pop }); + tl.tween(stepsU, 1, { at: b + 8.4, dur: 0.7, ease: ease.enter }); + } + // — B7 · the distances: 1 | 2 1 | 4 2 1, and the n = 16 ghost — + { + const [b] = beat(7); + tl.tween(cam, CAMERA_HOME, { at: b, dur: 1.3, ease: ease.move }); + tl.tween(panelOp, 0, { at: b, dur: 0.6, ease: ease.move }); + tl.tween(countU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(totalU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(stepsU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(distU, 1, { at: b + 0.6, dur: 0.7, ease: ease.enter }); + tl.tween(mergeU[0], 1, { at: b + 1.2, dur: 0.6, ease: ease.enter }); + tl.tween(mergeU[1], 1, { at: b + 2.4, dur: 0.6, ease: ease.enter }); + tl.tween(mergeU[2], 1, { at: b + 3.8, dur: 0.6, ease: ease.enter }); + tl.tween(sumU, 1, { at: b + 9.6, dur: 0.7, ease: ease.enter }); + tl.tween(ghostU, 1, { at: b + 10.8, dur: 1.2, ease: ease.draw }); + tl.tween(ghostSumU, 1, { at: b + 11.8, dur: 0.7, ease: ease.enter }); + } + // — B8 · the same 24 rods, another input: only the swaps change — + { + const [b] = beat(8); + tl.tween(ghostU, 0, { at: b, dur: 0.6, ease: ease.move }); + tl.tween(ghostSumU, 0, { at: b, dur: 0.6, ease: ease.move }); + tl.tween(sumU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(distU, 0, { at: b, dur: 0.5, ease: ease.move }); + mergeU.forEach((m) => tl.tween(m, 0, { at: b, dur: 0.5, ease: ease.move })); + tl.tween(discOp, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(sortedU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.set(which, 1, b + 0.6); + tl.tween(discOp, 1, { at: b + 0.7, dur: 0.6, ease: ease.enter }); + tl.tween(panelOp, 1, { at: b + 0.7, dur: 0.8, ease: ease.move }); + tl.tween(fixedU, 1, { at: b + 1.6, dur: 0.7, ease: ease.enter }); + tl.tween(runB, N_STAGES, { at: b + 2.4, dur: 8.0, ease: ease.linear }); + tl.tween(swapsU, 1, { at: b + 10.8, dur: 0.7, ease: ease.enter }); + } + // — B9 · every zero-one list through 8 and 16 wires — + { + const [b] = beat(9); + tl.tween(panelOp, 0, { at: b, dur: 0.6, ease: ease.move }); + tl.tween(fixedU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(swapsU, 0, { at: b, dur: 0.5, ease: ease.move }); + tl.tween(netOp, 0.12, { at: b + 0.2, dur: 0.8, ease: ease.move }); + tl.tween(cam, CAM_WIDE, { at: b, dur: 1.4, ease: ease.move }); + tl.tween(boardU, 1, { at: b + 0.7, dur: 0.7, ease: ease.enter }); + tl.tween(zeroU, 256, { at: b + 1.8, dur: 4.2, ease: ease.linear }); + tl.tween(n16U, 1, { at: b + 6.8, dur: 3.6, ease: ease.linear }); + } + // — B10 · 2,000 random lists per size; 32 and 64 only sampled — + { + const [b] = beat(10); + tl.tween(stripU, 1, { at: b + 0.3, dur: 0.7, ease: ease.enter }); + tl.tween(sizesU, SIZES.length, { at: b + 1.4, dur: 2.4, ease: ease.linear }); + tl.tween(sampledU, 1, { at: b + 6.4, dur: 0.7, ease: ease.pop }); + } + tl.hold(t, 1.2); + + return { + tl, cam, src, netOp, panelOp, wiresU, discsU, discOp, which, run, runB, rodsU, legendU, chips, tableU, barsU, + block2U, block4U, block8U, outlineU, upU, downU, m8U, bracesU, sortedU, sweepU, countU, totalU, stepsU, + distU, mergeU, sumU, ghostU, ghostSumU, fixedU, swapsU, boardU, zeroU, n16U, stripU, sizesU, sampledU, + }; +} + +const scene = buildScene(); + +// --------------------------------------------------------------------------- +// Glyphs +// --------------------------------------------------------------------------- + +/** A comparator rod: dots on both wires, an arrowhead on the wire that receives the larger value. */ +function Rod({ x, i, j, asc, opacity, glow }: { x: number; i: number; j: number; asc: boolean; opacity: number; glow: number }) { + if (opacity <= 0.005) return null; + const c = asc ? ASC : DESC; + const y0 = WY(i); + const y1 = WY(j); + const tip = asc ? y1 : y0; + const dir = asc ? 1 : -1; + const head = tip - dir * (DISC_R + 2); + return ( + + {glow > 0.01 && } + + + + + + ); +} + +function Disc({ x, y, value, rank, scale = 1, opacity = 1 }: { x: number; y: number; value: number; rank: number; scale?: number; opacity?: number }) { + if (opacity <= 0.005 || scale <= 0.01) return null; + const big = value >= 100; + return ( + + + + {value} + + + ); +} + +function Bracket({ x0, x1, y, color, label, opacity }: { x0: number; x1: number; y: number; color: string; label?: string; opacity: number }) { + if (opacity <= 0.005) return null; + return ( + + + {label && ( + + {label} + + )} + + ); +} + +function Chip({ x, y, text, color, opacity = 1, fs = 13, anchor = 'start', mono = false }: { + x: number; y: number; text: string; color: string; opacity?: number; fs?: number; anchor?: 'start' | 'middle' | 'end'; mono?: boolean; +}) { + if (opacity <= 0.005) return null; + const w = text.length * (mono ? CH13 : fs * 0.56) + 20; + const x0 = anchor === 'start' ? x : anchor === 'middle' ? x - w / 2 : x - w; + return ( + + + + {text} + + + ); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +/** Where each disc is at progress p (0..6 stages): x along the wire, y between its two wires. */ +function discPositions(R: Run, p: number): { x: number; y: number; wire: number }[] { + const s = Math.min(N_STAGES - 1, Math.max(0, Math.floor(p))); + const f = p >= N_STAGES ? 1 : clamp01(p - s); + return R.input.map((_, d) => { + if (p <= 0) return { x: REST[0], y: WY(R.wireOf[0][d]), wire: R.wireOf[0][d] }; + if (p >= N_STAGES) return { x: REST[N_STAGES], y: WY(R.wireOf[N_STAGES][d]), wire: R.wireOf[N_STAGES][d] }; + const from = R.wireOf[s][d]; + const to = R.wireOf[s + 1][d]; + const dd = DIST[s]; + const lowWire = from % (2 * dd) < dd ? from : from - dd; // the upper wire of this disc's comparator + const cx = COL[s] + rodOff(lowWire, dd); + const sw = smooth((f - 0.3) / 0.4); + let x: number; + if (f < 0.3) x = lerp(REST[s], cx, smooth(f / 0.3)); + else if (f < 0.7) x = cx; + else x = lerp(cx, REST[s + 1], smooth((f - 0.7) / 0.3)); + // two trading discs pass beside each other instead of through each other + if (from !== to) x += (to > from ? -1 : 1) * 17 * Math.sin(Math.PI * sw); + return { x, y: lerp(WY(from), WY(to), sw), wire: sw < 0.5 ? from : to }; + }); +} + +/** The value on wire j at progress p, for the bars: each bar slides with its disc. */ +function barSlots(R: Run, p: number): number[] { + const s = Math.min(N_STAGES - 1, Math.max(0, Math.floor(p))); + const f = p >= N_STAGES ? 1 : clamp01(p - s); + return R.input.map((_, d) => { + if (p <= 0) return R.wireOf[0][d]; + if (p >= N_STAGES) return R.wireOf[N_STAGES][d]; + return lerp(R.wireOf[s][d], R.wireOf[s + 1][d], smooth((f - 0.3) / 0.4)); + }); +} + +export function Render({ s }: { s: SceneState }) { + const cam = s.get(scene.cam); + const srcIdx = Math.max(0, Math.min(SOURCES.length - 1, Math.round(s.get(scene.src)))); + const netOp = s.get(scene.netOp); + const panelOp = s.get(scene.panelOp); + const wiresU = s.get(scene.wiresU); + const discsU = s.get(scene.discsU); + const discOp = s.get(scene.discOp); + const w = Math.round(s.get(scene.which)); + const R = RUNS[w]; + const p = w === 0 ? s.get(scene.run) : s.get(scene.runB); + const rodsU = s.get(scene.rodsU); + const chips = scene.chips.map((c) => s.get(c)); + const legendU = s.get(scene.legendU); + const tableU = s.get(scene.tableU); + const barsU = s.get(scene.barsU); + const block2U = s.get(scene.block2U); + const block4U = s.get(scene.block4U); + const block8U = s.get(scene.block8U); + const outlineU = s.get(scene.outlineU); + const upU = s.get(scene.upU); + const downU = s.get(scene.downU); + const m8U = s.get(scene.m8U); + const bracesU = s.get(scene.bracesU); + const sortedU = s.get(scene.sortedU); + const sweepU = s.get(scene.sweepU); + const countU = s.get(scene.countU); + const totalU = s.get(scene.totalU); + const stepsU = s.get(scene.stepsU); + const distU = s.get(scene.distU); + const mergeU = scene.mergeU.map((m) => s.get(m)); + const sumU = s.get(scene.sumU); + const ghostU = s.get(scene.ghostU); + const ghostSumU = s.get(scene.ghostSumU); + const fixedU = s.get(scene.fixedU); + const swapsU = s.get(scene.swapsU); + const boardU = s.get(scene.boardU); + const zeroU = s.get(scene.zeroU); + const n16U = s.get(scene.n16U); + const stripU = s.get(scene.stripU); + const sizesU = s.get(scene.sizesU); + const sampledU = s.get(scene.sampledU); + + const pos = discPositions(R, p); + const slots = barSlots(R, p); + const stageNow = Math.min(N_STAGES - 1, Math.max(0, Math.floor(p))); + const fNow = p >= N_STAGES || p <= 0 ? -1 : p - stageNow; + const lastRow = Math.max(0, Math.min(N_STAGES, Math.floor(p + 0.3))); + + return ( + <> + + + {/* ======================== the network ======================== */} + + {/* wires */} + {Array.from({ length: 8 }, (_, i) => ( + + + + {i} + + + ))} + {wiresU > 0.5 && ( + + wire + + )} + + {/* comparator rods, one column per stage */} + {NET.map((st, si) => { + const u = clamp01(rodsU - si); + if (u <= 0.005) return null; + const firing = si === stageNow && fNow >= 0 ? bump(fNow, 0.15, 0.85) : 0; + const sweep = sweepU >= 0 ? bump(sweepU - si, 0, 1) : 0; + return ( + + 0.05 ? colors.TEXT : colors.MUTED} fontSize={13} fontFamily={MONO} opacity={u}> + stage {si} + + {st.map(([i, j, asc], c) => { + const traded = R.swaps[si][c]; + // while a stage fires, a rod that traded its pair glows; one that did not stays quiet + const glow = Math.max(firing * (traded ? 1 : 0.25), sweep); + const dim = w === 1 && firing > 0 && !traded ? 0.55 : 1; + return ; + })} + + ); + })} + + {/* discs */} + {R.input.map((v, d) => { + const k = R.wireOf[0][d]; + const enter = clamp01(discsU * 9 - k); + return ; + })} + + {/* the stage's pair list, verbatim from the report */} + {chips.map((u, si) => + u > 0.005 ? ( + + {STAGE_LINES[si]} + + ) : null, + )} + + {/* B4: the half-cleaner's two halves */} + {bracesU > 0.01 && ( + + {[ + { a: 0, b: 3, text: 'the four smallest: 3, 4, 2, 1', c: ASC }, + { a: 4, b: 7, text: 'the four largest: 6, 5, 7, 8', c: colors.TEAL }, + ].map((h) => ( + + + + {h.text} + + + ))} + + )} + + {/* B6: four comparators per column, all firing together */} + {countU > 0.01 && ( + + {COL.map((x, si) => ( + = si ? colors.TEXT : colors.MUTED} fontSize={14} fontFamily={MONO}> + ×4 + + ))} + + )} + {totalU > 0.01 && ( + + {`${N_STAGES} stages × 4 comparators = ${N_COMPARATORS}`} + + )} + {stepsU > 0.01 && ( + + {`but only ${N_STAGES} steps in time: each column fires at once`} + + )} + + {/* B7: the distance ribbon, grouped by merge */} + {distU > 0.01 && ( + + {COL.map((x, si) => ( + + {DIST[si]} + + ))} + + distance + + + )} + {[ + { a: 0, b: 0, label: 'merge 2 · 1 stage' }, + { a: 1, b: 2, label: 'merge 4 · 2 stages' }, + { a: 3, b: 5, label: 'merge 8 · 3 stages' }, + ].map((m, k) => ( + + ))} + + + {/* B8: the wiring is fixed */} + {fixedU > 0.01 && ( + + same 24 comparators in the same places · only the swaps change + + )} + {swapsU > 0.01 && ( + + + {`[${RUN_A.input.join(', ')}]: ${SWAP_TOTAL[0]} of ${N_COMPARATORS} swapped`} + + + {`[${RUN_B.input.join(', ')}]: ${SWAP_TOTAL[1]} of ${N_COMPARATORS} swapped`} + + + )} + + + {/* ======================== right panel: the trace table and the bars ======================== */} + {panelOp > 0.005 && ( + + {tableU > 0.01 && ( + + + wire + + {Array.from({ length: 8 }, (_, j) => ( + + {j} + + ))} + {R.rows.map((row, r) => { + const u = r === 0 ? 1 : clamp01((p - (r - 1) - 0.7) / 0.3); + if (u <= 0.005) return null; + const done = r === N_STAGES ? (w === 0 ? sortedU : clamp01(p - N_STAGES + 1)) : 0; + const live = r === lastRow; + return ( + + + {r === 0 ? 'input' : `stage ${r - 1}`} + + {row.map((v, j) => { + const changed = r > 0 && R.rows[r - 1][j] !== v; + return ( + + 0.01 ? colors.POSITIVE : live ? colors.GRID : 'none'} strokeWidth={1 + done} /> + = 100 ? 12 : 13} fontFamily={MONO} fontWeight={changed ? 700 : 400}> + {v} + + + ); + })} + + ); + })} + {done8(w, sortedU) > 0.01 && ( + + sorted ascending + + )} + + )} + + {/* the same list, as bars: each bar rides with its disc */} + {barsU > 0.01 && ( + + + the same list as bars + + + {R.input.map((v, d) => { + const h = (v / R.vmax) * BAR_H * clamp01(barsU * 1.4); + const cx = lerp(barCx(0), barCx(7), slots[d] / 7); + return ( + 0.01 && w === 0 ? colors.POSITIVE : 'none'} strokeWidth={1.5 * sortedU} /> + ); + })} + {/* B3: the mountain outline over the bar tops */} + {outlineU > 0.01 && (() => { + const row = RUN_A.rows[3]; + const pts = row.map((v, j) => [barCx(j), BAR_BASE - (v / RUN_A.vmax) * BAR_H - 8] as const); + const n = outlineU * (pts.length - 1); + const k = Math.floor(n); + const drawn = pts.slice(0, k + 1).map(([x, y]) => `${x},${y}`); + if (k < pts.length - 1) { + const [ax, ay] = pts[k]; + const [bx, by] = pts[k + 1]; + drawn.push(`${lerp(ax, bx, n - k)},${lerp(ay, by, n - k)}`); + } + return ; + })()} + {upU > 0.01 && ( + + sorted up ↗ + + )} + {downU > 0.01 && ( + + sorted down ↘ + + )} + {m8U > 0.01 && ( + + together: one mountain of 8 + + )} + {/* block brackets: sizes 2, 4, 8 */} + {[ + { m: 2, u: block2U, doneAt: 1 }, + { m: 4, u: block4U, doneAt: 3 }, + { m: 8, u: block8U, doneAt: 6 }, + ].map(({ m, u, doneAt }) => + Array.from({ length: 8 / m }, (_, b) => { + const dirAsc = m === 8 || b % 2 === 0; + const sorted = w === 0 && p >= doneAt - 0.05; + return ( + + ); + }), + )} + {Math.max(block2U, block4U, block8U) > 0.01 && ( + + blocks + + )} + + )} + + )} + + {/* ======================== B7: the n = 16 ghost (report §2b) ======================== */} + {ghostU > 0.01 && ( + + + 16 wires: 10 stages + + {Array.from({ length: 16 }, (_, i) => ( + + ))} + {NET16.map((st, si) => { + const u = clamp01(ghostU * NET16.length - si); + const d = st[0][1] - st[0][0]; + return ( + + {st.map(([i, j, asc], c) => { + const x = G16_COL[si] + ((i % d) - (d - 1) / 2) * 3; + return ( + + + + + + ); + })} + + {d} + + + ); + })} + + 1 + 2 + 3 + 4 = 10 stages + + + )} + + {/* ======================== B9–B10: the checks ======================== */} + {boardU > 0.01 && ( + + + 8 wires · every list of zeros and ones + + {ZERO8.map((ok, k) => { + const lit = clamp01(zeroU - k); + const x = GRID.x + (k % 16) * GRID.pitch; + const y = GRID.y + Math.floor(k / 16) * GRID.pitch; + return 0 ? (ok ? colors.POSITIVE : colors.NEGATIVE) : colors.PANEL} + fillOpacity={lit > 0 ? 0.35 + 0.55 * lit : 1} stroke={colors.GRID} />; + })} + + {`${fmtInt(Math.min(ZERO8_SORTED, Math.floor(zeroU)))} / 256`} + + + sorted + + + + 16 wires · every list of zeros and ones + + + + + {`${fmtInt(Math.round(ZERO16_SORTED * n16U))} / ${fmtInt(65536)} sorted`} + + + + )} + {stripU > 0.01 && ( + + + 2,000 random lists per size, values 0 to 999 + + + against Array.prototype.sort · 0 mismatches + + {SIZES.map((n, k) => { + const u = clamp01(sizesU - k); + if (u <= 0.005) return null; + const x = N16_BAR.x + k * 60; + const tag = n >= 32 ? 'sampled' : n >= 8 ? '+ all 0/1' : ''; + return ( + + = 32 ? colors.WARM : colors.POSITIVE} strokeWidth={1.3} /> + + {`n=${n}`} + + + 0 ✗ + + {tag && ( + = 32 ? colors.WARM : colors.POSITIVE} fontSize={13} fontFamily={UI} + opacity={n >= 32 ? sampledU : 1}> + {tag} + + )} + + ); + })} + + )} + + + {/* ======================== screen-space: title, source chip, legend ======================== */} + + + 2 · Building the Mountains + + + + + {SOURCES[srcIdx]} + + + {legendU > 0.01 && ( + + {[ + { y: 30, c: ASC, text: 'asc · smaller value to the upper wire' }, + { y: 56, c: DESC, text: 'desc · smaller value to the lower wire' }, + ].map((l) => ( + + + + {l.text} + + + ))} + + )} + + ); +} + +/** the "sorted ascending" note under the last row, for whichever input is showing */ +function done8(w: number, sortedU: number): number { + return w === 0 ? sortedU : 0; +} + +export const vizScene = () => scene; diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-3.stories.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-3.stories.tsx new file mode 100644 index 00000000..dd295ab3 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-3.stories.tsx @@ -0,0 +1,28 @@ +// Bitonic Sorter — chapter 3 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan. +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-3'; + +/** + * The Staircase — one bitonic network grows from 8 to 64 wires beside a staircase of its stage counts + * (1, 3, 6, 10, 15, 21); a stage is n/2 comparators, the log axis shows the gap, a 64-wire sweep fires 32 at once, + * then +∞ padding, the article's off-by-one loop, and odd-even mergesort's 543 against 672. + */ +function Chapter3() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Bitonic Sorter/Chapter 3', + component: Chapter3, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-3.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-3.tsx new file mode 100644 index 00000000..0740cd0b --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-3.tsx @@ -0,0 +1,1098 @@ +// The Staircase +// +// Bitonic Sorter (Handwired labs) — chapter 3. Sources: +// Wikipedia "Bitonic sorter", rev 1373262602 (.orly-work/hw-books/inputs/bitonic/wikipedia-bitonic-sorter.wikitext) +// wiki:14 (O(n (log n)^2) comparators and O((log n)^2) "delay", called stages here per the book's vocabulary; +// parallel units in lockstep, "such as a typical GPU"), wiki:20 (power-of-two length, k = log2 n), +// wiki:99 ("using sentinel values"), wiki:109 (the loop as printed, `for i ← low to low + k do`), +// wiki:138 (k(k+1)/2 levels), wiki:140 (locality of reference, citing Batcher; quoted on the card, unmeasured) +// .orly-work/hw-books/bitonic-ref/report.txt §1 (stages 1, 3, 6, 10, 15, 21 and comparators 1, 6, 24, 80, 240, +// 672 for n = 2..64; odd-even mergesort 543 at n = 64), §6 (inclusive bound: 8 of 256 zero-one inputs not +// sorted at n = 8, first [1, 1, 1, 1, 0, 0, 0, 0]), §7 (+Infinity padding, m = 1..64, 3200 arrays, 0 mismatches) +// .orly-work/hw-books/bitonic-ref/bitonic.ts (bitonicNetwork, bitonicMerge/bitonicSort with `bound`), ported below +// docs/book-plans/handwired-bitonic-sorter/review.md, items 1 and 4 (on screen: "stages", never "delay"; the +// extra comparison runs from wire low + k to low + 2k, the first wire of the next block) +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan; captions are the fixed narration +// contract (verbatim). Every rod on screen is a comparator of a network built at module scope by a line-for-line +// port of bitonicNetwork (and, in beat 9, Batcher's odd-even mergesort); the module throws if the counts ever +// disagree with report.txt. The padding run, the failing output of the inclusive loop ([0, 0, 0, 1, 0, 1, 1, 1]) +// and the extra rod's wires (2 → 4, in stage 1) are computed by running the ported code, not written by hand. +// The 32 lockstep units in beat 5 are schematic (labelled on screen). +import 'katex/dist/katex.min.css'; +import { CAMERA_HOME, Camera, MathLabel, STAGE_W, Timeline, cameraInterp, colors, ease } from '../../core'; +import type { CameraState, SceneState } from '../../core'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +const smooth = (u: number): number => { + const v = clamp01(u); + return v * v * (3 - 2 * v); +}; +const MONO = colors.font.mono; +const UI = colors.font.ui; +const CH13 = 7.83; // mono advance at 13 px +const CH14 = 8.43; // mono advance at 14 px +const WIRE = '#39415a'; + +// --------------------------------------------------------------------------- +// The networks (module scope, exact): a port of bitonic-ref/bitonic.ts +// --------------------------------------------------------------------------- + +type Cmp = [lo: number, hi: number, ascending: boolean]; + +/** bitonic.ts:59-87, verbatim in behaviour. */ +function bitonicNetwork(n: number): Cmp[][] { + const stages: Cmp[][] = []; + const place = (stage: number, c: Cmp) => { + (stages[stage] ??= []).push(c); + }; + const emitMerge = (low: number, count: number, direction: 0 | 1, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + for (let i = low; i < low + k; i++) place(start, [i, i + k, direction === 1]); + emitMerge(low, k, direction, start + 1); + return 1 + emitMerge(low + k, k, direction, start + 1); + }; + const emitSort = (low: number, count: number, direction: 0 | 1, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + const half = emitSort(low, k, 1, start); + emitSort(low + k, k, 0, start); + return half + emitMerge(low, count, direction, start + half); + }; + emitSort(0, n, 1, 0); + return stages; +} + +/** Batcher's odd-even mergesort, grouped one stage per (p, k) pass. */ +function oddEvenNetwork(n: number): Cmp[][] { + const stages: Cmp[][] = []; + for (let p = 1; p < n; p *= 2) { + for (let k = p; k >= 1; k /= 2) { + const st: Cmp[] = []; + for (let j = k % p; j <= n - 1 - k; j += 2 * k) { + for (let i = 0; i <= Math.min(k - 1, n - j - k - 1); i++) { + if (Math.floor((i + j) / (p * 2)) === Math.floor((i + j + k) / (p * 2))) st.push([i + j, i + j + k, true]); + } + } + stages.push(st); + } + } + return stages; +} + +const KS = [1, 2, 3, 4, 5, 6]; +const NET = KS.map((k) => bitonicNetwork(2 ** k)); // NET[k - 1] +const STAGES = NET.map((st) => st.length); +const COMPS = NET.map((st) => st.reduce((a, s) => a + s.length, 0)); +// report.txt §1, the stages and comparators columns +const REPORT_STAGES = [1, 3, 6, 10, 15, 21]; +const REPORT_COMPS = [1, 6, 24, 80, 240, 672]; +KS.forEach((k, i) => { + const n = 2 ** k; + if (STAGES[i] !== REPORT_STAGES[i] || STAGES[i] !== (k * (k + 1)) / 2) throw new Error(`bitonic ch3: stages at n=${n}`); + if (COMPS[i] !== REPORT_COMPS[i] || COMPS[i] !== (n * k * (k + 1)) / 4) throw new Error(`bitonic ch3: comparators at n=${n}`); + if (NET[i].some((s) => s.length !== n / 2)) throw new Error(`bitonic ch3: a stage at n=${n} is not n/2 wide`); +}); +const net = (k: number): Cmp[][] => NET[k - 1]; +const S = (k: number): number => STAGES[k - 1]; +const OE = oddEvenNetwork(64); +const OE_COUNT = OE.reduce((a, s) => a + s.length, 0); +if (OE_COUNT !== 543) throw new Error(`bitonic ch3: odd-even count ${OE_COUNT}, report says 543`); +if (OE.length !== 21) throw new Error(`bitonic ch3: odd-even depth ${OE.length}`); + +function applyStage(a: number[], stage: Cmp[]): void { + for (const [i, j, asc] of stage) { + if (asc ? a[i] > a[j] : a[i] < a[j]) { + const t = a[i]; + a[i] = a[j]; + a[j] = t; + } + } +} + +/** Token positions stage by stage: POS[s][token] = wire, for s = 0..S (before stage 0 … after the last). */ +function tokenTrace(values: number[], stages: Cmp[][]): number[][] { + const wireTok = values.map((_, i) => i); + const snap = () => { + const p: number[] = []; + wireTok.forEach((tok, w) => (p[tok] = w)); + return p; + }; + const out = [snap()]; + for (const st of stages) { + for (const [i, j, asc] of st) { + const a = values[wireTok[i]]; + const b = values[wireTok[j]]; + if (asc ? a > b : a < b) [wireTok[i], wireTok[j]] = [wireTok[j], wireTok[i]]; + } + out.push(snap()); + } + return out; +} + +// Beat 7 — seven values padded with a sentinel (the chapter 2 trace input, its last value dropped). +const PAD_IN = [3, 7, 4, 8, 6, 2, 1, Infinity]; +const PAD_POS = tokenTrace(PAD_IN, net(3)); +const PAD_OUT = (() => { + const a = [...PAD_IN]; + for (const st of net(3)) applyStage(a, st); + return a; +})(); +if (PAD_OUT.some((v, i) => i > 0 && PAD_OUT[i - 1] > v) || PAD_OUT[7] !== Infinity) throw new Error('bitonic ch3: padding run'); + +// Beat 8 — the article's loop read literally (bitonic.ts with bound = 'inclusive'), instrumented to find the extra +// comparison that first moves a value on the failing input. +type Extra = { lo: number; hi: number; stage: number; swapped: boolean }; +function runRecursive(input: number[], bound: 'exclusive' | 'inclusive'): { out: number[]; extras: Extra[] } { + const a = [...input]; + const extras: Extra[] = []; + const merge = (low: number, count: number, dir: 0 | 1, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + const end = bound === 'exclusive' ? low + k : low + k + 1; + for (let i = low; i < end; i++) { + const swap = (dir === 1 && a[i] > a[i + k]) || (dir === 0 && a[i] < a[i + k]); + if (swap) [a[i], a[i + k]] = [a[i + k], a[i]]; + if (i === low + k && i + k < a.length) extras.push({ lo: i, hi: i + k, stage: start, swapped: swap }); + } + merge(low, k, dir, start + 1); + return 1 + merge(low + k, k, dir, start + 1); + }; + const sort = (low: number, count: number, dir: 0 | 1, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + const half = sort(low, k, 1, start); + sort(low + k, k, 0, start); + return half + merge(low, count, dir, start + half); + }; + sort(0, a.length, 1, 0); + // drop any undefined read past the end (the last block's extra compares a[n], which never swaps) + return { out: a.slice(0, input.length), extras }; +} +const isSorted = (a: number[]): boolean => a.every((v, i) => i === 0 || a[i - 1] <= v); +const ZO_FAIL = [1, 1, 1, 1, 0, 0, 0, 0]; +const INCL = runRecursive(ZO_FAIL, 'inclusive'); +const EXCL = runRecursive(ZO_FAIL, 'exclusive'); +const GHOST = INCL.extras.find((e) => e.swapped)!; +const ZO_FAILS = Array.from({ length: 256 }, (_, m) => Array.from({ length: 8 }, (_, i) => (m >> (7 - i)) & 1)).filter( + (x) => !isSorted(runRecursive(x, 'inclusive').out), +).length; +if (ZO_FAILS !== 8) throw new Error(`bitonic ch3: inclusive bound fails ${ZO_FAILS} of 256, report says 8`); +if (!isSorted(EXCL.out) || isSorted(INCL.out)) throw new Error('bitonic ch3: the failing input'); +if (!GHOST || GHOST.lo !== 2 || GHOST.hi !== 4 || GHOST.stage !== 1) throw new Error('bitonic ch3: ghost comparator moved'); +const BAD_WIRES = INCL.out.map((v, i) => (i < 7 && v > INCL.out[i + 1]) || (i > 0 && INCL.out[i - 1] > v)); + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; y ≥ 630 is the caption band) +// --------------------------------------------------------------------------- + +const NX0 = 96; // wire start +const P = 30; // fixed column pitch: the network's length IS its stage count +const COL0 = NX0 + 30; +const colX = (s: number): number => COL0 + s * P; +const wireEnd = (k: number): number => colX(S(k) - 1) + 26; +const NY0 = 118; +const NY1 = 548; +const wy = (i: number, n: number): number => NY0 + ((i + 0.5) * (NY1 - NY0)) / n; +const SPREAD = 20; // overlapping rods of one stage fan out across this width +const rodOff = (lo: number, d: number): number => (((lo % d) + 0.5) / d) * SPREAD - SPREAD / 2; +const DIST = ['#7dd3fc', '#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9']; // distance 1, 2, 4 … 32 +const DIST_WORD = ['1', '2', '4', '8', '16', '32']; + +// chart (right panel) +const BASE_Y = 500; +const bx = (k: number): number => 900 + (k - 1) * 58; +const BAR_W = 34; +const linH = (v: number): number => (v / 21) * 270; +const logH = (v: number): number => (Math.log10(v) / 3) * 300; +const AXIS_X = 872; + +// tokens (8 wires) +const IN_X = 62; +const OUT_X = colX(5) + P / 2 + 50; +const OUT1_X = 352; +const OUT2_X = 432; + +// lanes +const LANE = { x0: 878, y0: 252, pitch: 42, size: 32 }; + +const CAM_NET8: CameraState = { x: 380, y: 372, k: 1.15 }; +const CAM_COL: CameraState = { x: 690, y: 340, k: 1.45 }; +const CAM_CHART: CameraState = { x: 1000, y: 352, k: 1.12 }; +const CAM_LOG: CameraState = { x: 1036, y: 350, k: 1.12 }; +const CAM_PAD: CameraState = { x: 420, y: 342, k: 1.15 }; +const CAM_GHOST: CameraState = { x: 470, y: 340, k: 1.15 }; +const CAM_FINAL: CameraState = { x: 640, y: 356, k: 0.96 }; + +const merges8 = (s: number): number => (s === 0 ? 1 : s <= 2 ? 2 : 3); // which merge of the 8-wire sort owns column s + +// --------------------------------------------------------------------------- +// Timeline — ten fixed captions at ~150 words a minute +// --------------------------------------------------------------------------- + +const CAPTIONS = [ + 'Let the list have two to the k wires. The last merge takes k stages, the merge before it k minus one, and so on down to one. Add them up: k times k plus one, over two.', + 'For two, four, eight, sixteen, thirty-two and sixty-four wires, that is one, three, six, ten, fifteen and twenty-one stages. Each doubling adds only a few more stages.', + 'Every stage has half as many comparators as wires. So the total is the number of wires times k times k plus one, over four: one, six, twenty-four, eighty, two hundred forty, six hundred seventy-two.', + 'In the usual shorthand, comparators grow like n times log n squared, and stages like log n squared. From thirty-two to sixty-four wires, comparators nearly triple, while stages go only from fifteen to twenty-one.', + 'That gap is why hardware likes it. A graphics processor has many units running in lockstep, and each stage gives all of them, thirty-two comparators for sixty-four numbers, work in the same instant.', + "The article also says many consecutive operations keep locality of reference, which makes them cache-friendly in practice. That is the article's claim, citing Batcher. We did not measure it.", + 'The original design needs a length that is a power of two. For other lengths, pad with a sentinel bigger than everything, then drop the extras. That worked for every length from one to sixty-four in our check.', + "It is easy to get a stage wrong. The article's loop, read literally, runs one comparison too far, reaching into the neighboring block. At eight wires that fails eight of the two hundred fifty-six zeros-and-ones lists.", + 'Bitonic is not the only Batcher network. His odd-even merge sort needs fewer comparators: five hundred forty-three at sixty-four wires, against six hundred seventy-two.', + 'So the picture so far: half as many comparators as wires per stage, k times k plus one over two stages, and wiring that never looks at the data. Now a stranger machine, one with no swaps at all.', +]; +const CAPS = (() => { + let t = 0.4; + return CAPTIONS.map((text) => { + const dur = Math.round(text.split(/\s+/).length * 0.4 * 10) / 10; + const c = { at: +t.toFixed(1), dur, text }; + t = c.at + dur + 0.5; + return c; + }); +})(); + +export function buildScene() { + const tl = new Timeline(); + const A = CAPS.map((c) => c.at); + const cam = tl.channel('cam', CAMERA_HOME, cameraInterp); + + // the machine + const g = tl.channel('g', 3); // log2 of the wire count; fractional = mid-morph + const drawU = tl.channel('drawU', 0); // first draw-on of the 8-wire network + const netOp = tl.channel('netOp', 1); + const hudU = tl.channel('hudU', 0); + const labelsOp = tl.channel('labelsOp', 1); + // B0 + const mergeSel = tl.channel('mergeSel', 3); + const mergeU = tl.channel('mergeU', 0); + const bracketsU = tl.channel('bracketsU', 0); // 0..3: brackets 3, 2, 1 + const sumU = tl.channel('sumU', 0); + // chart + const chartOp = tl.channel('chartOp', 0); + const stairU = tl.channel('stairU', 0); // 0..6 bars + const incU = tl.channel('incU', 0); + const compRowU = tl.channel('compRowU', 0); // 0..6 entries + const compTexU = tl.channel('compTexU', 0); + const logU = tl.channel('logU', 0); + const lineU = tl.channel('lineU', 0); + const bigOU = tl.channel('bigOU', 0); + const markU = tl.channel('markU', 0); + // column focus / sweep + const focusCol = tl.channel('focusCol', 20); + const focusU = tl.channel('focusU', 0); + const colNoteU = tl.channel('colNoteU', 0); + const lanesU = tl.channel('lanesU', 0); + // B5 + const quoteU = tl.channel('quoteU', 0); + // B6 + const tokU = tl.channel('tokU', 0); + const tokP = tl.channel('tokP', -1); + const trimU = tl.channel('trimU', 0); + const padNoteU = tl.channel('padNoteU', 0); + const stripU = tl.channel('stripU', 0); + // B7 + const zoU = tl.channel('zoU', 0); + const blockU = tl.channel('blockU', 0); + const ghostU = tl.channel('ghostU', 0); + const codeU = tl.channel('codeU', 0); + const outU = tl.channel('outU', 0); + const failU = tl.channel('failU', 0); + // B8 + const oeU = tl.channel('oeU', 0); + const oeCountU = tl.channel('oeCountU', 0); + // B9 + const finalU = tl.channel('finalU', 0); + + CAPS.forEach((c) => tl.caption(c)); + + // — B0 · the sum of the merges: 1 + 2 + 3 = k(k+1)/2 — + tl.tween(drawU, 1, { at: A[0] + 0.2, dur: 1.5, ease: ease.draw }); + tl.tween(cam, CAM_NET8, { at: A[0] + 1.4, dur: 1.4, ease: ease.move }); + tl.tween(mergeU, 1, { at: A[0] + 3.4, dur: 0.6, ease: ease.move }); + tl.tween(bracketsU, 1, { at: A[0] + 3.6, dur: 0.6, ease: ease.enter }); + tl.set(mergeSel, 2, A[0] + 7.0); + tl.tween(bracketsU, 2, { at: A[0] + 7.0, dur: 0.6, ease: ease.enter }); + tl.set(mergeSel, 1, A[0] + 9.4); + tl.tween(bracketsU, 3, { at: A[0] + 9.4, dur: 0.6, ease: ease.enter }); + tl.tween(mergeU, 0, { at: A[0] + 11.4, dur: 0.6, ease: ease.move }); + tl.tween(sumU, 1, { at: A[0] + 11.6, dur: 0.7, ease: ease.enter }); + tl.hold(A[0] + CAPS[0].dur, 0.5); + + // — B1 · the network grows; the staircase climbs 1, 3, 6, 10, 15, 21 — + tl.tween(cam, CAMERA_HOME, { at: A[1], dur: 1.3, ease: ease.move }); + tl.tween(sumU, 0, { at: A[1], dur: 0.5, ease: ease.move }); + tl.tween(bracketsU, 0, { at: A[1], dur: 0.5, ease: ease.move }); + tl.tween(hudU, 1, { at: A[1] + 0.6, dur: 0.6, ease: ease.enter }); + tl.tween(chartOp, 1, { at: A[1] + 0.4, dur: 0.6, ease: ease.enter }); + tl.tween(stairU, 3, { at: A[1] + 0.8, dur: 1.3, ease: ease.linear }); + tl.tween(g, 4, { at: A[1] + 2.3, dur: 1.8, ease: ease.move }); + tl.tween(stairU, 4, { at: A[1] + 3.7, dur: 0.5, ease: ease.pop }); + tl.tween(g, 5, { at: A[1] + 4.5, dur: 1.8, ease: ease.move }); + tl.tween(stairU, 5, { at: A[1] + 5.9, dur: 0.5, ease: ease.pop }); + tl.tween(g, 6, { at: A[1] + 6.7, dur: 1.8, ease: ease.move }); + tl.tween(stairU, 6, { at: A[1] + 8.1, dur: 0.5, ease: ease.pop }); + tl.tween(incU, 1, { at: A[1] + 8.8, dur: 0.7, ease: ease.enter }); + tl.hold(A[1] + CAPS[1].dur, 0.5); + + // — B2 · one stage is n/2 rods; the total n·k(k+1)/4 — + tl.tween(hudU, 0, { at: A[2], dur: 0.5, ease: ease.move }); + tl.tween(cam, CAM_COL, { at: A[2], dur: 1.4, ease: ease.move }); + tl.tween(chartOp, 0.12, { at: A[2], dur: 0.6, ease: ease.move }); + tl.tween(focusU, 1, { at: A[2] + 0.6, dur: 0.7, ease: ease.move }); + tl.tween(colNoteU, 1, { at: A[2] + 1.6, dur: 0.7, ease: ease.enter }); + tl.tween(cam, CAM_CHART, { at: A[2] + 6.2, dur: 1.4, ease: ease.move }); + tl.tween(focusU, 0, { at: A[2] + 6.2, dur: 0.6, ease: ease.move }); + tl.tween(colNoteU, 0, { at: A[2] + 6.2, dur: 0.5, ease: ease.move }); + tl.tween(netOp, 0.15, { at: A[2] + 6.2, dur: 0.6, ease: ease.move }); + tl.tween(chartOp, 1, { at: A[2] + 6.4, dur: 0.6, ease: ease.move }); + tl.tween(compTexU, 1, { at: A[2] + 7.0, dur: 0.7, ease: ease.enter }); + tl.tween(compRowU, 6, { at: A[2] + 7.8, dur: 4.6, ease: ease.linear }); + tl.hold(A[2] + CAPS[2].dur, 0.5); + + // — B3 · log axis: the gentle curve and the steep one — + tl.tween(cam, CAM_LOG, { at: A[3], dur: 1.0, ease: ease.move }); + tl.tween(incU, 0, { at: A[3], dur: 0.5, ease: ease.move }); + tl.tween(compTexU, 0, { at: A[3], dur: 0.5, ease: ease.move }); + tl.tween(logU, 1, { at: A[3] + 0.6, dur: 1.4, ease: ease.move }); + tl.tween(lineU, 1, { at: A[3] + 2.2, dur: 1.4, ease: ease.draw }); + tl.tween(bigOU, 1, { at: A[3] + 3.6, dur: 0.7, ease: ease.enter }); + tl.tween(markU, 1, { at: A[3] + 7.6, dur: 0.8, ease: ease.enter }); + tl.hold(A[3] + CAPS[3].dur, 0.5); + + // — B4 · the 64-wire sweep: 32 comparators at once, 21 times — + tl.tween(cam, CAMERA_HOME, { at: A[4], dur: 1.3, ease: ease.move }); + tl.tween(chartOp, 0, { at: A[4], dur: 0.6, ease: ease.move }); + tl.tween(netOp, 1, { at: A[4], dur: 0.6, ease: ease.move }); + tl.tween(hudU, 1, { at: A[4] + 0.6, dur: 0.6, ease: ease.enter }); + tl.tween(logU, 0, { at: A[4] + 0.7, dur: 0.1, ease: ease.move }); // reset while hidden + tl.tween(lineU, 0, { at: A[4] + 0.7, dur: 0.1, ease: ease.move }); + tl.tween(bigOU, 0, { at: A[4], dur: 0.5, ease: ease.move }); + tl.tween(markU, 0, { at: A[4], dur: 0.5, ease: ease.move }); + tl.tween(lanesU, 1, { at: A[4] + 0.9, dur: 0.8, ease: ease.enter }); + tl.set(focusCol, 0, A[4] + 1.0); + tl.tween(focusU, 1, { at: A[4] + 1.6, dur: 0.5, ease: ease.move }); + tl.tween(focusCol, 20.999, { at: A[4] + 2.0, dur: 10.0, ease: ease.linear }); + tl.hold(A[4] + CAPS[4].dur, 0.5); + + // — B5 · the article's claim, unmeasured — + tl.tween(focusU, 0, { at: A[5], dur: 0.6, ease: ease.move }); + tl.tween(lanesU, 0, { at: A[5], dur: 0.6, ease: ease.move }); + tl.tween(netOp, 0.12, { at: A[5], dur: 0.6, ease: ease.move }); + tl.tween(hudU, 0.1, { at: A[5], dur: 0.6, ease: ease.move }); + tl.tween(quoteU, 1, { at: A[5] + 0.5, dur: 0.8, ease: ease.enter }); + tl.hold(A[5] + CAPS[5].dur, 0.5); + + // — B6 · pad seven values with +∞, sort, drop it — + tl.tween(quoteU, 0, { at: A[6], dur: 0.5, ease: ease.move }); + tl.tween(hudU, 0, { at: A[6], dur: 0.4, ease: ease.move }); + tl.tween(netOp, 0.6, { at: A[6], dur: 0.6, ease: ease.move }); + tl.tween(g, 3, { at: A[6] + 0.3, dur: 1.6, ease: ease.move }); + tl.tween(cam, CAM_PAD, { at: A[6] + 0.8, dur: 1.4, ease: ease.move }); + tl.tween(labelsOp, 0, { at: A[6] + 1.6, dur: 0.5, ease: ease.move }); + tl.tween(tokU, 1, { at: A[6] + 2.2, dur: 0.7, ease: ease.enter }); + tl.tween(padNoteU, 1, { at: A[6] + 2.8, dur: 0.7, ease: ease.enter }); + tl.tween(tokP, S(3) + 1, { at: A[6] + 4.2, dur: 6.2, ease: ease.linear }); + tl.tween(trimU, 1, { at: A[6] + 10.8, dur: 0.9, ease: ease.move }); + tl.tween(stripU, 1, { at: A[6] + 11.6, dur: 0.7, ease: ease.enter }); + tl.hold(A[6] + CAPS[6].dur, 0.5); + + // — B7 · the loop as printed: one comparison too far — + tl.tween(tokU, 0, { at: A[7], dur: 0.5, ease: ease.move }); + tl.tween(padNoteU, 0, { at: A[7], dur: 0.5, ease: ease.move }); + tl.tween(stripU, 0, { at: A[7], dur: 0.5, ease: ease.move }); + tl.tween(cam, CAM_GHOST, { at: A[7], dur: 1.0, ease: ease.move }); + tl.tween(zoU, 1, { at: A[7] + 0.6, dur: 0.7, ease: ease.enter }); + tl.tween(netOp, 0.15, { at: A[7] + 1.4, dur: 0.6, ease: ease.move }); + tl.tween(blockU, 1, { at: A[7] + 1.6, dur: 0.7, ease: ease.enter }); + tl.tween(codeU, 1, { at: A[7] + 2.2, dur: 0.7, ease: ease.enter }); + tl.tween(ghostU, 1, { at: A[7] + 3.4, dur: 1.0, ease: ease.draw }); + tl.tween(outU, 1, { at: A[7] + 7.4, dur: 0.8, ease: ease.enter }); + tl.tween(failU, 1, { at: A[7] + 10.0, dur: 0.6, ease: ease.pop }); + tl.hold(A[7] + CAPS[7].dur, 0.5); + + // — B8 · odd-even mergesort over the same 64 wires: 543 against 672 — + tl.tween(zoU, 0, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(blockU, 0, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(codeU, 0, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(ghostU, 0, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(outU, 0, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(failU, 0, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(cam, CAMERA_HOME, { at: A[8], dur: 1.3, ease: ease.move }); + tl.tween(netOp, 1, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(labelsOp, 1, { at: A[8], dur: 0.5, ease: ease.move }); + tl.tween(g, 6, { at: A[8] + 0.2, dur: 1.6, ease: ease.move }); + tl.tween(hudU, 1, { at: A[8] + 1.0, dur: 0.6, ease: ease.enter }); + tl.tween(netOp, 0.15, { at: A[8] + 2.0, dur: 0.6, ease: ease.move }); + tl.tween(oeU, 1, { at: A[8] + 2.2, dur: 2.6, ease: ease.linear }); + tl.tween(oeCountU, 1, { at: A[8] + 3.2, dur: 0.8, ease: ease.enter }); + tl.hold(A[8] + CAPS[8].dur, 0.5); + + // — B9 · pull back: the staircase and two formulas — + tl.tween(oeU, 0, { at: A[9], dur: 0.8, ease: ease.move }); + tl.tween(oeCountU, 0, { at: A[9], dur: 0.6, ease: ease.move }); + tl.tween(hudU, 0.12, { at: A[9], dur: 0.6, ease: ease.move }); + tl.tween(netOp, 0.13, { at: A[9], dur: 0.6, ease: ease.move }); + tl.tween(cam, CAM_FINAL, { at: A[9] + 0.2, dur: 1.5, ease: ease.move }); + tl.tween(chartOp, 1, { at: A[9] + 0.6, dur: 0.8, ease: ease.move }); + tl.tween(finalU, 1, { at: A[9] + 1.2, dur: 0.9, ease: ease.enter }); + tl.hold(A[9] + CAPS[9].dur, 2.5); + + return { + tl, cam, g, drawU, netOp, hudU, labelsOp, + mergeSel, mergeU, bracketsU, sumU, + chartOp, stairU, incU, compRowU, compTexU, logU, lineU, bigOU, markU, + focusCol, focusU, colNoteU, lanesU, + quoteU, + tokU, tokP, trimU, padNoteU, stripU, + zoU, blockU, ghostU, codeU, outU, failU, + oeU, oeCountU, + finalU, + }; +} + +const scene = buildScene(); + +// --------------------------------------------------------------------------- +// Network geometry at a continuous size g (a pure function of g) +// --------------------------------------------------------------------------- + +type Rod = { x: number; y1: number; y2: number; s: number; d: number; op: number }; +type Geo = { n: number; k: number; wires: { y: number; op: number }[]; rods: Rod[]; end: number; pitch: number }; + +function geometry(g: number): Geo { + const kLo = Math.max(1, Math.min(6, Math.floor(g + 1e-6))); + const u = g - kLo; + if (u < 1e-4 || kLo === 6) { + const k = kLo; + const n = 2 ** k; + const wires = Array.from({ length: n }, (_, i) => ({ y: wy(i, n), op: 1 })); + const rods: Rod[] = []; + net(k).forEach((st, s) => + st.forEach(([lo, hi]) => rods.push({ x: colX(s) + rodOff(lo, hi - lo), y1: wy(lo, n), y2: wy(hi, n), s, d: hi - lo, op: 1 })), + ); + return { n, k, wires, rods, end: wireEnd(k), pitch: (NY1 - NY0) / n }; + } + const kHi = kLo + 1; + const nLo = 2 ** kLo; + const n = 2 ** kHi; + const a = smooth(u / 0.4); + const b = clamp01((u - 0.3) / 0.3); + const y = (i: number): number => (i < nLo ? lerp(wy(i, nLo), wy(i, n), a) : wy(i, n) + (1 - b) * 24); + const wires = Array.from({ length: n }, (_, i) => ({ y: y(i), op: i < nLo ? 1 : b })); + const rods: Rod[] = []; + const sLo = S(kLo); + net(kHi).forEach((st, s) => + st.forEach(([lo, hi]) => { + let op = 1; + if (s < sLo) op = lo >= nLo ? b : 1; + else op = clamp01((u - (0.58 + (0.32 * (s - sLo)) / kHi)) / 0.1); + rods.push({ x: colX(s) + rodOff(lo, hi - lo), y1: y(lo), y2: y(hi), s, d: hi - lo, op }); + }), + ); + return { + n, k: kHi, wires, rods, + end: lerp(wireEnd(kLo), wireEnd(kHi), clamp01((u - 0.55) / 0.45)), + pitch: lerp((NY1 - NY0) / nLo, (NY1 - NY0) / n, a), + }; +} + +// --------------------------------------------------------------------------- +// Glyph kit (local) +// --------------------------------------------------------------------------- + +function Chip({ x, y, text, color, opacity = 1, fs = 13, anchor = 'start', mono = false }: { + x: number; y: number; text: string; color: string; opacity?: number; fs?: number; anchor?: 'start' | 'middle' | 'end'; mono?: boolean; +}) { + if (opacity <= 0.005) return null; + const w = text.length * (mono ? (fs === 14 ? CH14 : CH13) : fs * 0.56) + 20; + const x0 = anchor === 'start' ? x : anchor === 'middle' ? x - w / 2 : x - w; + return ( + + + + {text} + + + ); +} + +function CodeChip({ x, y, lines, stripe, opacity = 1, note }: { x: number; y: number; lines: string[]; stripe: string; opacity?: number; note?: string }) { + if (opacity <= 0.005) return null; + const w = Math.max(...lines.map((l) => l.length)) * CH14 + 26; + const h = lines.length * 18 + 12; + return ( + + + + {lines.map((l, i) => ( + + {l} + + ))} + {note && ( + + {note} + + )} + + ); +} + +function Token({ x, y, v, color = colors.TEXT, ring, opacity = 1 }: { x: number; y: number; v: number; color?: string; ring?: string; opacity?: number }) { + if (opacity <= 0.005) return null; + return ( + + + + {v === Infinity ? '∞' : v} + + + ); +} + +function Bracket({ x0, x1, y, label, color, opacity }: { x0: number; x1: number; y: number; label: string; color: string; opacity: number }) { + if (opacity <= 0.005) return null; + return ( + + + + {label} + + + ); +} + +// token x for progress p through the 8-wire network (-1 = parked at the input, S + 1 = parked at the output) +function tokenX(p: number, s8: number): number { + const x0 = colX(0) - P / 2; + const x1 = colX(s8 - 1) + P / 2; + if (p < 0) return lerp(IN_X, x0, p + 1); + if (p > s8) return lerp(x1, OUT_X, p - s8); + return x0 + p * P; +} +/** sideways nudge while a token crosses another: moving up bows left, moving down bows right */ +function tokenDX(tok: number, p: number, pos: number[][], s8: number): number { + if (p <= 0 || p >= s8) return 0; + const s = Math.floor(p); + const d = pos[s + 1][tok] - pos[s][tok]; + if (d === 0) return 0; + const f = clamp01((p - s - 0.25) / 0.5); + return Math.sign(d) * 15 * Math.sin(Math.PI * f); +} +function tokenY(tok: number, p: number, pos: number[][], s8: number): number { + if (p <= 0) return wy(pos[0][tok], 8); + if (p >= s8) return wy(pos[s8][tok], 8); + const s = Math.floor(p); + const f = smooth((p - s - 0.25) / 0.5); + return lerp(wy(pos[s][tok], 8), wy(pos[s + 1][tok], 8), f); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +export function Render({ s }: { s: SceneState }) { + const cam = s.get(scene.cam); + const g = s.get(scene.g); + const geo = geometry(g); + const drawU = s.get(scene.drawU); + const netOp = s.get(scene.netOp); + const hudU = s.get(scene.hudU); + const labelsOp = s.get(scene.labelsOp); + const mergeSel = Math.round(s.get(scene.mergeSel)); + const mergeU = s.get(scene.mergeU); + const bracketsU = s.get(scene.bracketsU); + const sumU = s.get(scene.sumU); + const chartOp = s.get(scene.chartOp); + const stairU = s.get(scene.stairU); + const incU = s.get(scene.incU); + const compRowU = s.get(scene.compRowU); + const compTexU = s.get(scene.compTexU); + const logU = s.get(scene.logU); + const lineU = s.get(scene.lineU); + const bigOU = s.get(scene.bigOU); + const markU = s.get(scene.markU); + const focusColV = s.get(scene.focusCol); + const focusCol = Math.floor(focusColV); + const focusU = s.get(scene.focusU); + const colNoteU = s.get(scene.colNoteU); + const lanesU = s.get(scene.lanesU); + const quoteU = s.get(scene.quoteU); + const tokU = s.get(scene.tokU); + const tokP = s.get(scene.tokP); + const trimU = s.get(scene.trimU); + const padNoteU = s.get(scene.padNoteU); + const stripU = s.get(scene.stripU); + const zoU = s.get(scene.zoU); + const blockU = s.get(scene.blockU); + const ghostU = s.get(scene.ghostU); + const codeU = s.get(scene.codeU); + const outU = s.get(scene.outU); + const failU = s.get(scene.failU); + const oeU = s.get(scene.oeU); + const oeCountU = s.get(scene.oeCountU); + const finalU = s.get(scene.finalU); + + const nNow = 2 ** Math.round(g); + const kNow = Math.round(g); + const flash = 1 - (focusColV - focusCol); // a lit column fires, then decays until the next one + const sweeping = lanesU > 0.01; + const rodOp = (r: Rod): number => { + let o = r.op * netOp; + if (drawU < 1) o *= clamp01(drawU * 7 - r.s); + if (mergeU > 0.001 && geo.n === 8) o *= lerp(1, merges8(r.s) === mergeSel ? 1 : 0.13, mergeU); + if (focusU > 0.001) o *= lerp(1, r.s === focusCol ? 1 : 0.13, focusU); + return o; + }; + const dotR = Math.max(1.4, Math.min(4.2, geo.pitch * 0.085)); + const rodW = Math.max(1, Math.min(2.6, geo.pitch * 0.045)); + const wireOp = Math.max(0.15, netOp) * clamp01(drawU * 1.5); + const wireLabelOp = labelsOp * clamp01((geo.pitch - 15) / 10) * clamp01(drawU * 2) * Math.max(0.12, netOp); + + // chart helpers + const barH = (v: number): number => lerp(linH(v), logH(v), logU); + const logY = (v: number): number => BASE_Y - logH(v); + + // the comparator curve, drawn on point by point + const compLine = (u: number): string => { + const f = u * 5; + const i = Math.min(5, Math.floor(f)); + const pts = KS.slice(0, i + 1).map((k) => [bx(k), logY(COMPS[k - 1])]); + if (i < 5) pts.push([lerp(bx(i + 1), bx(i + 2), f - i), lerp(logY(COMPS[i]), logY(COMPS[i + 1]), f - i)]); + return pts.map((p) => p.join(',')).join(' '); + }; + // odd-even overlay: columns wipe on left to right + const oeColOp = (st: number): number => clamp01(oeU * 22 - st); + + return ( + <> + + + {/* ======================== the network ======================== */} + + {sweeping && focusU > 0.01 && geo.n === 64 && ( + + )} + {geo.wires.map((w, i) => ( + = 64 ? 0.8 : 1.2} opacity={w.op * wireOp} /> + ))} + {geo.rods.map((r, i) => { + const o = rodOp(r); + if (o <= 0.005) return null; + const c = DIST[Math.log2(r.d)]; + const lit = sweeping && r.s === focusCol ? flash : 0; + return ( + + 0.02 ? '#ffffff' : c} strokeWidth={rodW + lit * 0.8} /> + + + + ); + })} + {wireLabelOp > 0.01 && + geo.wires.map((w, i) => ( + + {i} + + ))} + + + {/* HUD above the network: the current size, read off the ported network */} + {hudU > 0.01 && ( + + + {`n = ${nNow} wires · k = ${kNow} · ${S(kNow)} stages · ${COMPS[kNow - 1]} comparators`} + + + )} + {/* the distance key, under the network */} + {hudU > 0.01 && ( + + + comparator distance + + {DIST.map((c, i) => ( + + + + {DIST_WORD[i]} + + + ))} + + )} + + {/* B0: merge brackets under the 8-wire network, and the sum */} + {bracketsU > 0.01 && geo.n === 8 && ( + + + + + + + stages per merge + + + + {[ + { m: 3, t: 'the last merge: 8 wires, k = 3 stages', y: 250 }, + { m: 2, t: 'the merges before it: k − 1 = 2', y: 290 }, + { m: 1, t: 'and the first: 1', y: 330 }, + ].map((r) => ( + + {r.t} + + ))} + + + )} + {sumU > 0.01 && ( + + + + )} + + {/* B2: one column, n/2 rods */} + {colNoteU > 0.01 && ( + + + + one stage: 32 comparators + + + every wire used once + + + + )} + + {/* ======================== the chart (right panel) ======================== */} + {chartOp > 0.005 && ( + + {/* titles (linear mode) */} + + + + + {/* log mode legend */} + {bigOU > 0.01 && ( + + + + + )} + {/* log axis */} + {logU > 0.01 && ( + + + {[1, 10, 100, 1000].map((v) => ( + + + + + {v} + + + ))} + + log scale + + + )} + {/* baseline + n row */} + + + n + + {KS.map((k) => { + const u = clamp01(stairU - (k - 1)); + if (u <= 0.005) return null; + const v = STAGES[k - 1]; + const h = barH(v) * u; + const hl = markU > 0.01 && k >= 5; + return ( + + + + {v} + + {k > 1 && incU > 0.01 && ( + + {`+${v - STAGES[k - 2]}`} + + )} + + {2 ** k} + + + ); + })} + {/* comparator row */} + {compRowU > 0.01 && ( + + + comparators + + {KS.map((k) => ( + + {COMPS[k - 1]} + + ))} + + )} + {/* comparators on the log axis */} + {lineU > 0.01 && ( + + + {KS.map((k) => ( + + ))} + + )} + {/* 32 → 64 */} + {markU > 0.01 && ( + + + ×2.8 + + + +6 + + + 32 → 64 wires + + + comparators 240 → 672 + + + stages 15 → 21 + + + )} + + )} + + {/* B4: lockstep lanes (schematic) and the sweep counter */} + {lanesU > 0.01 && ( + + + 32 comparators per stage at n = 64 + + + parallel units in lockstep (schematic) + + {Array.from({ length: 32 }, (_, i) => { + const cx = LANE.x0 + (i % 8) * LANE.pitch; + const cy = LANE.y0 + Math.floor(i / 8) * LANE.pitch; + const on = focusU > 0.5 && focusColV >= 0 ? flash : 0; + return ( + + 0.05 ? DIST[Math.log2(net(6)[focusCol][0][1] - net(6)[focusCol][0][0])] : colors.GRID} strokeWidth={1.4} /> + + + + + + ); + })} + + {`stage ${focusCol + 1} of 21`} + + + {`distance ${net(6)[focusCol][0][1] - net(6)[focusCol][0][0]} · all 32 units busy every stage`} + + + )} + + {/* B5: the quote card */} + {quoteU > 0.01 && ( + + + + the article's claim (Wikipedia, Bitonic sorter, citing Batcher, 1968) + + {[ + '“… many of the consecutive operations in a bitonic sort', + 'retain a locality of reference, making implementations', + 'more cache-friendly …”', + ].map((l, i) => ( + + {l} + + ))} + + + )} + + {/* B6: padding with a sentinel */} + {tokU > 0.01 && + PAD_IN.map((v, tok) => { + const isInf = v === Infinity; + const tr = isInf ? trimU : 0; + return ( + + ); + })} + {padNoteU > 0.01 && ( + + + m = 7, padded to 8 + + + + + sorted + + + )} + {stripU > 0.01 && ( + + + + our check: the JavaScript reference against Array.prototype.sort + + + )} + + {/* B7: the loop as printed */} + {blockU > 0.01 && ( + + {[0, 4].map((w0, i) => ( + + ))} + + block 0–3 + + + block 4–7 + + + )} + {ghostU > 0.01 && ( + + + + + + )} + {zoU > 0.01 && ( + + + input + + {ZO_FAIL.map((v, w) => ( + + ))} + + )} + {codeU > 0.01 && ( + + + + + + {`the extra i = low + k: wire ${GHOST.lo} → wire ${GHOST.hi}`} + + + low + k to low + 2k, the first wire of the next block + + + + )} + {outU > 0.01 && ( + + + as printed + + + i < low + k + + {INCL.out.map((v, w) => ( + + ))} + {EXCL.out.map((v, w) => ( + + ))} + + )} + {failU > 0.01 && ( + + + + the first of them: 1 1 1 1 0 0 0 0 + + + )} + + {/* B8: odd-even mergesort, 64 wires, same columns */} + {oeU > 0.01 && geo.n === 64 && ( + + {OE.map((st, si) => { + const o = oeColOp(si); + if (o <= 0.005) return null; + return ( + + {st.map(([lo, hi], ci) => ( + + + + + + ))} + + ); + })} + + )} + {oeCountU > 0.01 && ( + + + two Batcher networks, 64 wires + + + bitonic + + + {COMPS[5]} + + + odd-even merge sort + + + {OE_COUNT} + + + {`comparators · ${OE.length} stages each, as drawn`} + + + )} + + {/* B9: the two formulas, backed by an opaque panel */} + {finalU > 0.01 && ( + + + + + + wiring set by n alone: it never looks at the data + + + )} + + + {/* ======================== screen-space title ======================== */} + + + 3 · The Staircase + + + ); +} + +export const vizScene = () => scene; diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-4.stories.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-4.stories.tsx new file mode 100644 index 00000000..5788d0db --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-4.stories.tsx @@ -0,0 +1,28 @@ +// Bitonic Sorter — chapter 4 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan. +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-4'; + +/** + * What Handwired Pays — a compare-exchange drawn as three layers of weighted sums and relu folds into a tower of + * layers; the tower and an operations bar then race ranks by counting at 16 and 64 items, the counting grid grows + * as n², fp32 drift gets its caveat, and the verdict closes it. + */ +function Chapter4() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Bitonic Sorter/Chapter 4', + component: Chapter4, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-4.tsx b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-4.tsx new file mode 100644 index 00000000..b7e75f69 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired-bitonic-sorter/chapter-4.tsx @@ -0,0 +1,1197 @@ +// What Handwired Pays +// +// Bitonic Sorter — chapter 4. Sources (none of these appear on screen): +// chenglou/handwired @ 4ea7fcb27ebe93acf49b9f29a93e162b975bad46, lab/ui-exploration: +// README.md:5, 18 (one fixed circuit of weighted sums and relu); docs/execution-model.md:3, 7 (FP32), 40 (layers: +// independent instructions at one dependency depth share a layer), 282 (fractional drift) +// docs/patterns.md:358 (a sort branches on comparisons); guide/sections/loops.md:185 (compare-exchange, 4 operations +// in 3 layers), 189 (16 items: 1,248 / 50 against 1,056 / 5), 190 (64 items: 10,624 / 105 against 16,512 / 5), +// 192, 194 (fractional keys: 20 to 24%, errors up to 6e-5); guide/patterns/ranks-instead-of-sorting.md:16 (ties +// break by index), 84; guide/review-and-decisions.md:269 (packaging a network past 64 items: open) +// Exploration notes @ d01b763: notes-outside:31-34 (cost table; 64 keys only, 2,752 operations, 64 layers), 109, 113 +// (counting is the better formulation at UI sizes; n² hurts around hundreds of items); notes-runtime:66 (min and +// max share the difference and relu, 4 / 3), 237 (odd-even network drift), 238 (rank sort) +// Reference: .orly-work/hw-books/bitonic-ref/bitonic.ts (bitonicNetwork, ported below), report.txt §1-2 +// Fact-check: docs/book-plans/handwired-bitonic-sorter/review.md (the tie-broken rank formula, ties shown by index) +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan. +// +// What is computed here and what is quoted. The stage and comparator counts (21 stages, 672 comparators at 64 wires; +// 80 at 16; the drawn 8-wire network's 6 stages and 24 comparators) come from a line-for-line port of bitonicNetwork; +// 63 = 21 × 3 and 2,688 = 672 × 4 are arithmetic and labelled so. The worked compare-exchange (a = 7, b = 3), the +// rank grid (8 example keys with one tie), the pair counts n² and the fp32 example (a = 2.7, b = 0.1, whose +// a − relu(a − b) lands 13 fp32 steps below b) are computed at module scope; the module throws if any disagrees with +// the caption. Every operation and layer count of Handwired itself is a measurement quoted from the notes. The plan's +// "counting grid unaffected" by drift and its closing recap strip are dropped: the first is not in the sources, the +// second would crowd the verdict. +import { CAMERA_HOME, Camera, MathLabel, STAGE_H, STAGE_W, Timeline, cameraInterp, colors, ease } from '../../core'; +import type { CameraState, SceneState } from '../../core'; +import type { ReactNode } from 'react'; +import 'katex/dist/katex.min.css'; + +// the network and the circuit wear the series' teal (the user's video accent is not this book's to choose) +const HUE = colors.TEAL; +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +const MONO = colors.font.mono; +const UI = colors.font.ui; +const fmtN = (v: number): string => Math.round(v).toLocaleString('en-US'); + +// --------------------------------------------------------------------------- +// The network (port of bitonic-ref/bitonic.ts: bitonicNetwork) — module scope, checked +// --------------------------------------------------------------------------- + +type Comparator = [i: number, j: number, ascending: boolean]; + +function bitonicNetwork(n: number): Comparator[][] { + const stages: Comparator[][] = []; + const place = (stage: number, c: Comparator) => { + (stages[stage] ??= []).push(c); + }; + const emitMerge = (low: number, count: number, up: boolean, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + for (let i = low; i < low + k; i++) place(start, [i, i + k, up]); + emitMerge(low, k, up, start + 1); + return 1 + emitMerge(low + k, k, up, start + 1); + }; + const emitSort = (low: number, count: number, up: boolean, start: number): number => { + if (count <= 1) return 0; + const k = count / 2; + const half = emitSort(low, k, true, start); + emitSort(low + k, k, false, start); + return half + emitMerge(low, count, up, start + half); + }; + emitSort(0, n, true, 0); + return stages; +} +const comparators = (net: Comparator[][]): number => net.reduce((s, st) => s + st.length, 0); + +const NET8 = bitonicNetwork(8); +const NET16 = bitonicNetwork(16); +const NET64 = bitonicNetwork(64); +const STAGES64 = NET64.length; // 21 +const COMPS64 = comparators(NET64); // 672 +const COMPS16 = comparators(NET16); // 80 +const PRED_LAYERS = STAGES64 * 3; // 63, arithmetic +const PRED_OPS = COMPS64 * 4; // 2,688, arithmetic +if (NET8.length !== 6 || comparators(NET8) !== 24 || NET16.length !== 10 || COMPS16 !== 80 || STAGES64 !== 21 || COMPS64 !== 672) { + throw new Error('bitonic ch4: network counts disagree with report.txt §1'); +} +if (PRED_LAYERS !== 63 || PRED_OPS !== 2688) throw new Error('bitonic ch4: predicted cost disagrees with caption 5/6'); + +// Handwired's measurements (loops.md:189-190, notes-outside:32): quoted, never derived. +const M = { + keys64: { ops: 2752, layers: 64 }, + bit16: { ops: 1248, layers: 50 }, + cnt16: { ops: 1056, layers: 5 }, + bit64: { ops: 10624, layers: 105 }, + cnt64: { ops: 16512, layers: 5 }, +}; + +// The drawn 8-wire network: rods packed into sub-columns so overlapping rods in one stage never collide. +const NET8_RODS = (() => { + const rods: { s: number; i: number; j: number; up: boolean; col: number }[] = []; + let col = 0; + NET8.forEach((stage, s) => { + const lanes: [number, number][][] = []; + stage.forEach(([i, j, up]) => { + let L = lanes.findIndex((l) => l.every(([a, b]) => j < a || i > b)); + if (L < 0) { + L = lanes.length; + lanes.push([]); + } + lanes[L].push([i, j]); + rods.push({ s, i, j, up, col: col + L }); + }); + col += lanes.length; + }); + return rods; +})(); + +// --------------------------------------------------------------------------- +// The worked compare-exchange (a = 7, b = 3): three weighted sums and one relu +// --------------------------------------------------------------------------- + +const relu = (x: number): number => (x > 0 ? x : 0); +const CA = 7; +const CB = 3; +const CD = CA - CB; // layer 1 +const CR = relu(CD); // layer 2 +const CMIN = CA - CR; // layer 3 +const CMAX = CB + CR; // layer 3 +if (CD !== 4 || CR !== 4 || CMIN !== Math.min(CA, CB) || CMAX !== Math.max(CA, CB)) throw new Error('bitonic ch4: compare-exchange'); + +// --------------------------------------------------------------------------- +// Ranks by counting: 8 example keys, one tie (items 1 and 3), ties broken by index (ranks-instead-of-sorting.md:16) +// --------------------------------------------------------------------------- + +const KEYS = [5, 2, 7, 2, 9, 4, 1, 6]; +const BITS = KEYS.map((xi, i) => KEYS.map((xj, j) => (xj < xi || (xj === xi && j < i) ? 1 : 0))); +const RANK = BITS.map((row) => row.reduce((s, v) => s + v, 0)); +const TIE = { i: 3, j: 1 }; +{ + const placed = KEYS.map(() => -1); + RANK.forEach((r, i) => (placed[r] = KEYS[i])); + const sorted = [...KEYS].sort((a, b) => a - b); + if (new Set(RANK).size !== 8 || placed.some((v, r) => v !== sorted[r]) || BITS[TIE.i][TIE.j] !== 1 || BITS[TIE.j][TIE.i] !== 0) { + throw new Error('bitonic ch4: rank example is not a stable permutation'); + } +} +const PAIRS = (n: number): number => n * n; + +// --------------------------------------------------------------------------- +// One fp32 example of drift (computed here; the measured rates are the notes') +// --------------------------------------------------------------------------- + +const f32 = Math.fround; +const DA = f32(2.7); +const DB = f32(0.1); +const DMIN = f32(DA - f32(relu(f32(DA - DB)))); +const ULP = 2 ** -27; // fp32 spacing in [1/16, 1/8) +const DSTEPS = Math.round((DB - DMIN) / ULP); +if (!(DB >= 1 / 16 && DB < 1 / 8 && DMIN >= 1 / 16) || DSTEPS !== 13 || Math.abs((DB - DMIN) / ULP - 13) > 1e-9) { + throw new Error('bitonic ch4: fp32 drift example'); +} +const DB_STR = DB.toPrecision(10); // 0.1000000015 +const DMIN_STR = DMIN.toPrecision(10); // 0.09999990463 + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; y ≥ 630 is the caption band) +// --------------------------------------------------------------------------- + +// the compare-exchange circuit: three layer slabs, nodes, inputs +const SLAB = { x: 110, w: 500, h: 80 }; +const slabY = (L: number): number => 460 - L * 110; +type NodeId = 'a' | 'b' | 'd' | 'r' | 'mn' | 'mx'; +const NODE: Record = { + a: { x: 250, y: 585 }, + b: { x: 470, y: 585 }, + d: { x: 360, y: 500 }, + r: { x: 360, y: 390 }, + mn: { x: 250, y: 280 }, + mx: { x: 470, y: 280 }, +}; +const NODE_ORDER: NodeId[] = ['a', 'b', 'd', 'r', 'mn', 'mx']; +const EDGES: { from: NodeId; to: NodeId; w?: string; wx: number; wy: number }[] = [ + { from: 'a', to: 'd', w: '+1', wx: 296, wy: 552 }, + { from: 'b', to: 'd', w: '−1', wx: 424, wy: 552 }, + { from: 'd', to: 'r', wx: 0, wy: 0 }, + { from: 'r', to: 'mn', w: '−1', wx: 296, wy: 342 }, + { from: 'r', to: 'mx', w: '+1', wx: 424, wy: 342 }, + { from: 'a', to: 'mn', w: '+1', wx: 236, wy: 446 }, + { from: 'b', to: 'mx', w: '+1', wx: 484, wy: 446 }, +]; +const NR = 24; + +// towers: one slab per layer, 4 px a layer +const PITCH = 4; +const BASE = 590; +const TW = { x: 150, w: 140 }; // bitonic +const CT = { x: 400, w: 140 }; // counting +const topOf = (n: number): number => BASE - PITCH * n; + +// right zone: scoreboard bars +const BAR = { x: 720, len: 400, ya: 340, yb: 440, h: 28, axisY: 500 }; +const COND_Y = 282; + +// counting grid +const GX = 880; +const GY = 190; +const GC = 30; +const SLOT_Y = 478; +const PHOTO_X = 812; + +// pair squares (side ∝ item count), anchored bottom-left +const SQ = { x: 760, y: 590, px: 1.6 }; + +// fp32 number line +const NL = { x0: 760, y: 320, step: 22, kMin: -16, kMax: 3 }; +const nlx = (k: number): number => NL.x0 + (k - NL.kMin) * NL.step; + +const CAM_PRIM: CameraState = { x: 950, y: 360, k: 1.25 }; +const CAM_CMP: CameraState = { x: 500, y: 400, k: 1.15 }; +const CAM_TOWER: CameraState = { x: 420, y: 420, k: 1.3 }; +const CAM_BARS: CameraState = { x: 900, y: 400, k: 1.15 }; +const CAM_GRID: CameraState = { x: 990, y: 350, k: 1.2 }; +const CAM_SQ: CameraState = { x: 900, y: 390, k: 1.08 }; +const CAM_DRIFT: CameraState = { x: 970, y: 370, k: 1.12 }; +const CAM_END: CameraState = { x: 640, y: 370, k: 0.94 }; + +// --------------------------------------------------------------------------- +// The narration (book.json chapters[3].captions, verbatim) +// --------------------------------------------------------------------------- + +const TEXTS = [ + 'Handwired writes a program as one fixed circuit of weighted sums and relu, over thirty-two-bit floats. Relu keeps a positive number and turns a negative one into zero. No loops, no branches.', + 'The circuit is cut into layers. Operations that do not depend on each other share a layer, and each layer reads only earlier ones. So cost has two numbers: operations in all, and layers deep.', + "A sort that branches on comparisons cannot be written that way, but a sorting network can, because its comparators never depend on the data. The numbers ahead are measurements from Handwired's own exploration notes.", + 'A comparator becomes arithmetic. Subtract the two keys, run the difference through relu, subtract that from the first key for the minimum, and add it to the second for the maximum. Four operations, three layers.', + 'Twenty-one stages at three layers each predicts sixty-three layers for sixty-four keys. Handwired measured the bitonic network, keys only, at sixty-four layers and two thousand seven hundred fifty-two operations.', + 'Four operations times six hundred seventy-two comparators would be two thousand six hundred eighty-eight. The notes do not explain the small gap, so read these as measurements, not a derivation.', + 'Real lists carry a payload, like a photo that travels with its key. With one payload value per item, the bitonic network measured one thousand two hundred forty-eight operations in fifty layers at sixteen items.', + 'The rival needs no network. A rank counts the items ahead of this one, so compare every pair and add. Equal keys break ties by index, and that count is the sorted position. A one-hot read fetches the payload.', + 'At sixteen items, counting wins on work: one thousand fifty-six operations against one thousand two hundred forty-eight, in five layers against fifty.', + 'At sixty-four, the network wins on work: ten thousand six hundred twenty-four operations against sixteen thousand five hundred twelve. But it is one hundred five layers deep, and counting stays at five.', + "Pairs grow with the square of the item count. Handwired's notes still call counting the better choice for the item counts interfaces usually have, with trouble starting around hundreds of items.", + 'One caveat. Minimum and maximum built from arithmetic drift on fractional keys. On the odd-even network, twenty to twenty-four percent of outputs matched no input bit for bit, with errors up to six in a hundred thousand.', + 'The verdict, from that historical record: at interface sizes, count ranks. Whether to package a sorting network for apps that sort well past sixty-four items is still an open question.', +]; +// ~150 words a minute, plus a breath; a 0.9 s hold between beats +const CAPS = (() => { + const out: { at: number; dur: number; text: string }[] = []; + let t = 0.5; + for (const text of TEXTS) { + const dur = Math.round((text.split(/\s+/).length / 2.5 + 0.5) * 10) / 10; + out.push({ at: +t.toFixed(1), dur, text }); + t += dur + 0.9; + } + return out; +})(); + +// condition chip text, by phase +const COND = [ + '64 keys, keys only · measured', + '16 items, one payload value each · measured', + '64 items, one payload value each, integer keys · measured', +]; + +// --------------------------------------------------------------------------- +// Timeline +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const cam = tl.channel('cam', CAMERA_HOME, cameraInterp); + + // B1 · the primitives + const sumU = tl.channel('sumU', 0); + const reluU = tl.channel('reluU', 0); + const fpU = tl.channel('fpU', 0); + const noU = tl.channel('noU', 0); + const cellsU = tl.channel('cellsU', 0); + const primOp = tl.channel('primOp', 1); + // B2 · layers + const slabU = tl.channel('slabU', 0); // 0..3 + const nodeU = tl.channel('nodeU', 0); // 0..6 + const edgeU = tl.channel('edgeU', 0); + const costU = tl.channel('costU', 0); + const indepU = tl.channel('indepU', 0); + const circOp = tl.channel('circOp', 1); + // B3 · branches vs network + const flowU = tl.channel('flowU', 0); + const flowX = tl.channel('flowX', 0); + const netU = tl.channel('netU', 0); + const stampU = tl.channel('stampU', 0); + const b3Op = tl.channel('b3Op', 1); + // B4 · the comparator as arithmetic + const valU = tl.channel('valU', 0); // 0..4 + const cardU = tl.channel('cardU', 0); + // B5–B6 · the keys-only tower and the gap + const foldU = tl.channel('foldU', 0); + const bitN = tl.channel('bitN', 3); + const bandU = tl.channel('bandU', 1); + const stageLblU = tl.channel('stageLblU', 0); + const predU = tl.channel('predU', 0); + const extraU = tl.channel('extraU', 0); + const measU = tl.channel('measU', 0); + const towerOp = tl.channel('towerOp', 1); + const barsOp = tl.channel('barsOp', 0); + const barA = tl.channel('barA', 0); + const barB = tl.channel('barB', 0); + const rowBOp = tl.channel('rowBOp', 0); + const rowPhase = tl.channel('rowPhase', 0); + const domain = tl.channel('domain', 2900); + const gapU = tl.channel('gapU', 0); + // B7 · payload + const payU = tl.channel('payU', 0); + const condOp = tl.channel('condOp', 0); + const condIdx = tl.channel('condIdx', 0); + const topLblU = tl.channel('topLblU', 0); + // B8 · the counting grid + const gridOp = tl.channel('gridOp', 0); + const gridU = tl.channel('gridU', 0); + const formU = tl.channel('formU', 0); + const bitsU = tl.channel('bitsU', 0); // 0..64 + const sumsU = tl.channel('sumsU', 0); + const tieU = tl.channel('tieU', 0); + const slideU = tl.channel('slideU', 0); + // B9–B10 · the races + const cntN = tl.channel('cntN', 0); + const laneBU = tl.channel('laneBU', 0); + const winA = tl.channel('winA', 0); + const winB = tl.channel('winB', 0); + const cntHi = tl.channel('cntHi', 0); + // B11 · pairs grow as n² + const sqMorph = tl.channel('sqMorph', 0); + const sqN = tl.channel('sqN', 8); + const sq16U = tl.channel('sq16U', 0); + const sq64U = tl.channel('sq64U', 0); + const sq256U = tl.channel('sq256U', 0); + const uiNoteU = tl.channel('uiNoteU', 0); + // B12 · fp32 drift + const driftOp = tl.channel('driftOp', 0); + const lineU = tl.channel('lineU', 0); + const bMarkU = tl.channel('bMarkU', 0); + const minMarkU = tl.channel('minMarkU', 0); + const gapNlU = tl.channel('gapNlU', 0); + const measCardU = tl.channel('measCardU', 0); + const intU = tl.channel('intU', 0); + // B13 · verdict + const finalU = tl.channel('finalU', 0); + const stamp1 = tl.channel('stamp1', 0); + const stamp2 = tl.channel('stamp2', 0); + + CAPS.forEach((c) => tl.caption(c)); + const B = CAPS.map((c) => c.at); + const E = CAPS.map((c) => c.at + c.dur); + const fade = (ch: ReturnType>, to: number, at: number, dur = 0.6) => + tl.tween(ch, to, { at, dur, ease: ease.move }); + + // — B1 · weighted sums and relu, no loops, no branches — + let b = B[0]; + tl.tween(cam, CAM_PRIM, { at: b + 0.2, dur: 1.4, ease: ease.move }); + tl.tween(sumU, 1, { at: b + 0.6, dur: 1.2, ease: ease.draw }); + tl.tween(cellsU, 1, { at: b + 1.6, dur: 1.4, ease: ease.linear }); + tl.tween(fpU, 1, { at: b + 4.2, dur: 0.6, ease: ease.pop }); + tl.tween(reluU, 1, { at: b + 6.0, dur: 1.4, ease: ease.draw }); + tl.tween(noU, 1, { at: b + 10.6, dur: 0.9, ease: ease.draw }); + tl.hold(E[0], 0.9); + + // — B2 · the circuit is cut into layers — + b = B[1]; + tl.tween(cam, CAMERA_HOME, { at: b, dur: 1.3, ease: ease.move }); + fade(primOp, 0, b, 0.7); + tl.tween(slabU, 3, { at: b + 0.6, dur: 1.8, ease: ease.linear }); + tl.tween(nodeU, 6, { at: b + 2.0, dur: 2.0, ease: ease.linear }); + tl.tween(edgeU, 1, { at: b + 3.6, dur: 1.4, ease: ease.draw }); + tl.tween(indepU, 1, { at: b + 4.6, dur: 0.6, ease: ease.pop }); + tl.tween(costU, 1, { at: b + 9.2, dur: 0.8, ease: ease.enter }); + tl.hold(E[1], 0.9); + + // — B3 · branches can't be wired; a network can — + b = B[2]; + fade(costU, 0, b, 0.5); + fade(indepU, 0, b, 0.5); + fade(circOp, 0.12, b, 0.7); + tl.tween(flowU, 1, { at: b + 0.5, dur: 0.8, ease: ease.enter }); + tl.tween(flowX, 1, { at: b + 3.0, dur: 0.8, ease: ease.draw }); + tl.tween(netU, 1, { at: b + 5.0, dur: 1.4, ease: ease.draw }); + tl.tween(stampU, 1, { at: b + 9.6, dur: 0.6, ease: ease.pop }); + tl.hold(E[2], 0.9); + + // — B4 · a comparator becomes arithmetic — + b = B[3]; + fade(b3Op, 0, b, 0.6); + fade(circOp, 1, b, 0.7); + tl.tween(cam, CAM_CMP, { at: b + 0.2, dur: 1.3, ease: ease.move }); + tl.tween(valU, 1, { at: b + 1.2, dur: 0.5, ease: ease.enter }); + tl.tween(valU, 2, { at: b + 2.4, dur: 0.6, ease: ease.move }); + tl.tween(valU, 3, { at: b + 4.4, dur: 0.6, ease: ease.move }); + tl.tween(valU, 4, { at: b + 6.4, dur: 0.6, ease: ease.move }); + tl.tween(cardU, 1, { at: b + 8.6, dur: 0.7, ease: ease.enter }); + tl.hold(E[3], 0.9); + + // — B5 · 21 stages × 3 layers predicts 63; measured 64 and 2,752 — + b = B[4]; + tl.tween(cam, CAMERA_HOME, { at: b, dur: 1.1, ease: ease.move }); + fade(cardU, 0, b, 0.5); + fade(valU, 0, b, 0.5); + tl.tween(foldU, 1, { at: b + 0.3, dur: 1.6, ease: ease.move }); + tl.tween(cam, CAM_TOWER, { at: b + 2.0, dur: 1.3, ease: ease.move }); + tl.tween(stageLblU, 1, { at: b + 2.0, dur: 0.4, ease: ease.enter }); + tl.tween(bitN, PRED_LAYERS, { at: b + 2.2, dur: 3.8, ease: ease.linear }); + fade(stageLblU, 0, b + 6.2, 0.4); + tl.tween(predU, 1, { at: b + 6.2, dur: 0.6, ease: ease.enter }); + tl.tween(bitN, M.keys64.layers, { at: b + 7.6, dur: 0.5, ease: ease.pop }); + tl.tween(extraU, 1, { at: b + 7.6, dur: 0.5, ease: ease.pop }); + tl.tween(measU, 1, { at: b + 7.9, dur: 0.6, ease: ease.enter }); + tl.hold(E[4], 0.9); + + // — B6 · 672 × 4 = 2,688 against the measured 2,752 — + b = B[5]; + tl.tween(cam, CAM_BARS, { at: b, dur: 1.3, ease: ease.move }); + fade(barsOp, 1, b + 0.4, 0.6); + tl.tween(barA, M.keys64.ops, { at: b + 0.6, dur: 1.0, ease: ease.move }); + fade(rowBOp, 1, b + 2.0, 0.6); + tl.tween(barB, PRED_OPS, { at: b + 2.2, dur: 1.0, ease: ease.move }); + tl.tween(gapU, 1, { at: b + 5.2, dur: 0.7, ease: ease.pop }); + tl.hold(E[5], 0.9); + + // — B7 · a payload rides along: 16 items, 1,248 operations, 50 layers — + b = B[6]; + tl.tween(cam, CAMERA_HOME, { at: b, dur: 1.3, ease: ease.move }); + fade(gapU, 0, b, 0.5); + fade(rowBOp, 0, b, 0.5); + fade(predU, 0, b, 0.5); + fade(measU, 0, b, 0.5); + fade(extraU, 0, b + 0.3, 0.6); + fade(bandU, 0, b + 0.3, 0.8); + tl.set(barB, 0, b + 0.6); + tl.set(condIdx, 1, b + 0.5); + tl.tween(payU, 1, { at: b + 1.0, dur: 1.2, ease: ease.linear }); + fade(condOp, 1, b + 6.0, 0.5); + tl.tween(bitN, M.bit16.layers, { at: b + 7.0, dur: 1.3, ease: ease.move }); + tl.tween(topLblU, 1, { at: b + 7.4, dur: 0.6, ease: ease.enter }); + tl.tween(domain, 1400, { at: b + 7.0, dur: 1.3, ease: ease.move }); + tl.tween(barA, M.bit16.ops, { at: b + 7.0, dur: 1.3, ease: ease.move }); + tl.hold(E[6], 0.9); + + // — B8 · ranks by counting: compare every pair, add, break ties by index, read one-hot — + b = B[7]; + tl.tween(cam, CAM_GRID, { at: b, dur: 1.3, ease: ease.move }); + fade(barsOp, 0, b, 0.5); + fade(condOp, 0, b, 0.5); + fade(payU, 0, b, 0.5); + fade(towerOp, 0.12, b, 0.6); + fade(gridOp, 1, b + 0.4, 0.5); + tl.tween(gridU, 1, { at: b + 0.5, dur: 0.9, ease: ease.enter }); + tl.tween(formU, 1, { at: b + 1.4, dur: 0.7, ease: ease.enter }); + tl.tween(bitsU, 64, { at: b + 3.0, dur: 3.0, ease: ease.linear }); + tl.tween(sumsU, 1, { at: b + 6.2, dur: 0.8, ease: ease.enter }); + tl.tween(tieU, 1, { at: b + 7.8, dur: 0.6, ease: ease.pop }); + tl.tween(slideU, 1, { at: b + 11.6, dur: 1.6, ease: ease.move }); + tl.hold(E[7], 0.9); + + // — B9 · 16 items: counting less work, 5 layers against 50 — + b = B[8]; + tl.tween(cam, CAMERA_HOME, { at: b, dur: 1.3, ease: ease.move }); + fade(gridOp, 0, b, 0.6); + fade(towerOp, 1, b, 0.6); + fade(barsOp, 1, b + 0.3, 0.6); + fade(condOp, 1, b + 0.3, 0.6); + tl.set(rowPhase, 1, b + 0.2); + fade(rowBOp, 1, b + 0.6, 0.5); + tl.tween(barB, M.cnt16.ops, { at: b + 0.8, dur: 1.2, ease: ease.move }); + tl.tween(laneBU, 1, { at: b + 0.8, dur: 0.6, ease: ease.enter }); + tl.tween(cntN, M.cnt16.layers, { at: b + 1.2, dur: 0.6, ease: ease.pop }); + tl.tween(winB, 1, { at: b + 2.8, dur: 0.5, ease: ease.pop }); + tl.hold(E[8], 0.9); + + // — B10 · 64 items: the network less work, 105 layers against 5 — + b = B[9]; + fade(winB, 0, b, 0.4); + tl.set(condIdx, 2, b + 0.2); + tl.tween(domain, 17000, { at: b + 0.6, dur: 1.6, ease: ease.move }); + tl.tween(barA, M.bit64.ops, { at: b + 0.6, dur: 1.6, ease: ease.move }); + tl.tween(barB, M.cnt64.ops, { at: b + 0.6, dur: 1.6, ease: ease.move }); + tl.tween(winA, 1, { at: b + 4.2, dur: 0.5, ease: ease.pop }); + tl.tween(bitN, M.bit64.layers, { at: b + 6.4, dur: 1.8, ease: ease.move }); + tl.tween(cntHi, 1, { at: b + 9.4, dur: 0.6, ease: ease.pop }); + tl.hold(E[9], 0.9); + + // — B11 · pairs grow as n² — + b = B[10]; + tl.tween(cam, CAM_SQ, { at: b, dur: 1.3, ease: ease.move }); + fade(winA, 0, b, 0.4); + fade(cntHi, 0, b, 0.4); + fade(barsOp, 0, b, 0.6); + fade(condOp, 0, b, 0.6); + fade(towerOp, 0.12, b, 0.6); + fade(gridOp, 1, b + 0.4, 0.6); + tl.tween(sqMorph, 1, { at: b + 0.6, dur: 1.3, ease: ease.move }); + tl.tween(sqN, 16, { at: b + 2.2, dur: 0.9, ease: ease.move }); + tl.tween(sq16U, 1, { at: b + 2.6, dur: 0.5, ease: ease.enter }); + tl.tween(sqN, 64, { at: b + 3.6, dur: 1.2, ease: ease.move }); + tl.tween(sq64U, 1, { at: b + 4.4, dur: 0.5, ease: ease.enter }); + tl.tween(uiNoteU, 1, { at: b + 6.0, dur: 0.6, ease: ease.enter }); + tl.tween(sq256U, 1, { at: b + 9.0, dur: 1.4, ease: ease.draw }); + tl.hold(E[10], 0.9); + + // — B12 · fractional keys drift — + b = B[11]; + tl.tween(cam, CAM_DRIFT, { at: b, dur: 1.3, ease: ease.move }); + fade(gridOp, 0, b, 0.6); + fade(driftOp, 1, b + 0.4, 0.6); + tl.tween(lineU, 1, { at: b + 0.6, dur: 1.2, ease: ease.draw }); + tl.tween(bMarkU, 1, { at: b + 1.8, dur: 0.5, ease: ease.pop }); + tl.tween(minMarkU, 1, { at: b + 2.8, dur: 0.5, ease: ease.pop }); + tl.tween(gapNlU, 1, { at: b + 3.8, dur: 0.6, ease: ease.enter }); + tl.tween(measCardU, 1, { at: b + 5.4, dur: 0.7, ease: ease.enter }); + tl.tween(intU, 1, { at: b + 11.5, dur: 0.6, ease: ease.enter }); + tl.hold(E[11], 0.9); + + // — B13 · the verdict — + b = B[12]; + tl.tween(cam, CAM_END, { at: b, dur: 1.6, ease: ease.move }); + fade(driftOp, 0, b, 0.6); + tl.set(rowBOp, 1, b); + fade(towerOp, 0.12, b, 0.3); + tl.tween(finalU, 1, { at: b + 0.6, dur: 0.8, ease: ease.enter }); + tl.tween(stamp1, 1, { at: b + 3.0, dur: 0.6, ease: ease.pop }); + tl.tween(stamp2, 1, { at: b + 5.6, dur: 0.6, ease: ease.pop }); + tl.hold(E[12], 2.5); + + return { + tl, cam, + sumU, reluU, fpU, noU, cellsU, primOp, + slabU, nodeU, edgeU, costU, indepU, circOp, + flowU, flowX, netU, stampU, b3Op, + valU, cardU, + foldU, bitN, bandU, stageLblU, predU, extraU, measU, towerOp, + barsOp, barA, barB, rowBOp, rowPhase, domain, gapU, + payU, condOp, condIdx, topLblU, + gridOp, gridU, formU, bitsU, sumsU, tieU, slideU, + cntN, laneBU, winA, winB, cntHi, + sqMorph, sqN, sq16U, sq64U, sq256U, uiNoteU, + driftOp, lineU, bMarkU, minMarkU, gapNlU, measCardU, intU, + finalU, stamp1, stamp2, + }; +} + +const scene = buildScene(); + +// --------------------------------------------------------------------------- +// Glyphs +// --------------------------------------------------------------------------- + +function Cross({ x0, y0, x1, y1, u, w = 3 }: { x0: number; y0: number; x1: number; y1: number; u: number; w?: number }) { + if (u <= 0.01) return null; + const a = clamp01(u * 2); + const c = clamp01(u * 2 - 1); + return ( + + + {c > 0 && } + + ); +} + +function Check({ x, y, s = 1, opacity = 1 }: { x: number; y: number; s?: number; opacity?: number }) { + return ( + + ); +} + +function Photo({ x, y, w = 26, h = 20, tint, opacity = 1 }: { x: number; y: number; w?: number; h?: number; tint: string; opacity?: number }) { + if (opacity <= 0.005) return null; + return ( + + + + + + ); +} + +const photoTint = (key: number): string => colors.heat(0.25 + (0.7 * key) / 9); + +function ReluIcon({ x, y, s = 1, color }: { x: number; y: number; s?: number; color: string }) { + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +export function Render({ s }: { s: SceneState }) { + const cam = s.get(scene.cam); + const sx = (x: number): number => (x - cam.x) * cam.k + STAGE_W / 2; + const sy = (y: number): number => (y - cam.y) * cam.k + STAGE_H / 2; + // labels a camera push would carry off the stage, under the title band, or into the caption band fade out whole + const fitBox = (x0: number, x1: number, y0: number, y1: number): number => { + const r = 6; + return clamp01((sx(x0) - 4) / r) * clamp01((STAGE_W - 4 - sx(x1)) / r) * clamp01((sy(y0) - 80) / r) * clamp01((626 - sy(y1)) / r); + }; + const T = (p: { + x: number; y: number; text: string; fs?: number; color?: string; anchor?: 'start' | 'middle' | 'end'; mono?: boolean; + op?: number; weight?: number; k?: string | number; + }): ReactNode => { + const { x, y, text, fs = 14, color = colors.TEXT, anchor = 'start', mono = false, op = 1, weight } = p; + if (op <= 0.01) return null; + const w = text.length * fs * (mono ? 0.6 : 0.54); + const x0 = anchor === 'start' ? x : anchor === 'middle' ? x - w / 2 : x - w; + const o = op * fitBox(x0, x0 + w, y - fs, y + fs * 0.3); + if (o <= 0.01) return null; + return ( + + {text} + + ); + }; + const Chip = (p: { x: number; y: number; text: string; color: string; op?: number; fs?: number; mono?: boolean; fill?: boolean; k?: string }): ReactNode => { + const { x, y, text, color, op = 1, fs = 14, mono = false, fill = false } = p; + if (op <= 0.01) return null; + const w = text.length * fs * (mono ? 0.6 : 0.54) + 20; + return ( + + + {T({ x: x + w / 2, y, text, fs, color, anchor: 'middle', mono })} + + ); + }; + + const v = (k: K) => s.get(scene[k] as never) as number; + + const sumU = v('sumU'); + const reluU = v('reluU'); + const fpU = v('fpU'); + const noU = v('noU'); + const cellsU = v('cellsU'); + const primOp = v('primOp'); + const slabU = v('slabU'); + const nodeU = v('nodeU'); + const edgeU = v('edgeU'); + const costU = v('costU'); + const indepU = v('indepU'); + const circOp = v('circOp'); + const flowU = v('flowU'); + const flowX = v('flowX'); + const netU = v('netU'); + const stampU = v('stampU'); + const b3Op = v('b3Op'); + const valU = v('valU'); + const cardU = v('cardU'); + const foldU = v('foldU'); + const bitN = v('bitN'); + const bandU = v('bandU'); + const stageLblU = v('stageLblU'); + const predU = v('predU'); + const extraU = v('extraU'); + const measU = v('measU'); + const towerOp = v('towerOp'); + const barsOp = v('barsOp'); + const barA = v('barA'); + const barB = v('barB'); + const rowBOp = v('rowBOp'); + const rowPhase = v('rowPhase'); + const domain = v('domain'); + const gapU = v('gapU'); + const payU = v('payU'); + const condOp = v('condOp'); + const condIdx = Math.round(v('condIdx')); + const topLblU = v('topLblU'); + const gridOp = v('gridOp'); + const gridU = v('gridU'); + const formU = v('formU'); + const bitsU = v('bitsU'); + const sumsU = v('sumsU'); + const tieU = v('tieU'); + const slideU = v('slideU'); + const cntN = v('cntN'); + const laneBU = v('laneBU'); + const winA = v('winA'); + const winB = v('winB'); + const cntHi = v('cntHi'); + const sqMorph = v('sqMorph'); + const sqN = v('sqN'); + const sq16U = v('sq16U'); + const sq64U = v('sq64U'); + const sq256U = v('sq256U'); + const uiNoteU = v('uiNoteU'); + const driftOp = v('driftOp'); + const lineU = v('lineU'); + const bMarkU = v('bMarkU'); + const minMarkU = v('minMarkU'); + const gapNlU = v('gapNlU'); + const measCardU = v('measCardU'); + const intU = v('intU'); + const finalU = v('finalU'); + const stamp1 = v('stamp1'); + const stamp2 = v('stamp2'); + + // ---- the circuit: slabs fold down into the tower's first three layers ---- + const slabRect = (L: number) => { + const big = { x: SLAB.x, y: slabY(L), w: SLAB.w, h: SLAB.h }; + const small = { x: TW.x, y: BASE - PITCH * (L + 1), w: TW.w, h: PITCH - 1 }; + return { x: lerp(big.x, small.x, foldU), y: lerp(big.y, small.y, foldU), w: lerp(big.w, small.w, foldU), h: lerp(big.h, small.h, foldU) }; + }; + const nodesOp = circOp * clamp01(1 - foldU * 2.5); + const nodeIn = (id: NodeId): number => clamp01(nodeU - NODE_ORDER.indexOf(id)); + const opsShown = Math.min(4, Math.max(0, Math.floor(nodeU + 1e-6) - 2)); + + // ---- towers ---- + const tower = (x: number, w: number, n: number, color: string, opts: { band?: number; from?: number; extra?: number; hi?: number } = {}) => { + const out: ReactNode[] = []; + const count = Math.ceil(n - 1e-6); + for (let k = opts.from ?? 0; k < count; k++) { + const frac = Math.min(1, n - k); + const y = BASE - PITCH * (k + 1); + const stage = Math.floor(k / 3); + let op = lerp(0.8, stage % 2 ? 0.45 : 0.9, opts.band ?? 0); + let fill = color; + if (k === M.keys64.layers - 1 && (opts.extra ?? 0) > 0.01) { + fill = colors.WARM; + op = lerp(op, 1, opts.extra ?? 0); + } + out.push(); + } + if (opts.hi && opts.hi > 0.01 && count > 0) { + const top = BASE - PITCH * n; + out.push(); + } + return out; + }; + + // ---- bars ---- + const bx = (val: number): number => BAR.x + (BAR.len * val) / domain; + const tickStep = [250, 500, 1000, 2000, 2500, 5000].find((st) => domain / st <= 5) ?? 5000; + const ticks: number[] = []; + for (let t = 0; t <= domain + 1e-6; t += tickStep) ticks.push(t); + + // ---- grid → squares ---- + const gridSide = lerp(8 * GC, 8 * SQ.px, sqMorph); + const sqSide = sqMorph > 0 ? (sqMorph < 1 ? gridSide : sqN * SQ.px) : 8 * GC; + const sqX = lerp(GX, SQ.x, sqMorph); + const sqY = lerp(GY, SQ.y - 8 * SQ.px, sqMorph) - (sqMorph >= 1 ? sqSide - 8 * SQ.px : 0); + const cellOp = clamp01(1 - sqMorph * 1.6); + + return ( + <> + + + {/* ======================== B1 · weighted sums, relu, no loops, no branches ======================== */} + {primOp > 0.01 && ( + + {/* weighted sum */} + + {[200, 240, 280].map((y, k) => ( + + + + {T({ x: 726, y: y - 8 + (240 - y) * 0.25, text: `w${k + 1}`, fs: 13, color: colors.MUTED, mono: true, op: clamp01(sumU * 2 - 1), k })} + + ))} + + {T({ x: 800, y: 250, text: 'Σ', fs: 26, color: HUE, anchor: 'middle' })} + + {T({ x: 800, y: 300, text: 'weighted sum', fs: 15, anchor: 'middle' })} + + {/* relu */} + + + + + {T({ x: 960, y: 180, text: 'relu', fs: 18, color: HUE })} + {T({ x: 1020, y: 296, text: 'negative → 0', fs: 13, color: colors.MUTED, anchor: 'middle', op: clamp01(reluU * 2) })} + {T({ x: 1150, y: 296, text: 'positive kept', fs: 13, color: colors.MUTED, anchor: 'middle', op: clamp01(reluU * 2 - 1) })} + + {Chip({ x: 720, y: 376, text: '32-bit floats', color: HUE, op: fpU, mono: true })} + {/* no loops, no branches */} + + {Chip({ x: 720, y: 444, text: 'loop', color: colors.MUTED, mono: true })} + + {Chip({ x: 830, y: 444, text: 'branch', color: colors.MUTED, mono: true })} + + + {/* one fixed circuit */} + {T({ x: 720, y: 492, text: 'one fixed circuit', fs: 14, color: colors.MUTED, op: clamp01(cellsU * 3) })} + {Array.from({ length: 20 }, (_, i) => { + const r = Math.floor(i / 10); + const c = i % 10; + const u = clamp01(cellsU * 20 - i); + if (u <= 0) return null; + const isRelu = (r + c) % 3 === 1; + const x = 720 + c * 48; + const y = 504 + r * 34; + return ( + + + {isRelu ? : T({ x: x + 21, y: y + 19, text: 'Σ', fs: 14, color: HUE, anchor: 'middle' })} + + ); + })} + + )} + + {/* ======================== the circuit (B2–B4), folding into the tower (B5) ======================== */} + {foldU < 0.999 && slabU > 0.01 && ( + + {[0, 1, 2].map((L) => { + const u = clamp01(slabU - L); + if (u <= 0) return null; + const r = slabRect(L); + return ( + + + {T({ x: SLAB.x + SLAB.w - 12, y: slabY(L) + 24, text: `layer ${L + 1}`, fs: 14, color: HUE, anchor: 'end', op: clamp01(1 - foldU * 3) })} + + ); + })} + + )} + {nodesOp > 0.01 && ( + + {/* edges: every one runs upward, into a later layer */} + {EDGES.map((e, i) => { + const A = NODE[e.from]; + const Bn = NODE[e.to]; + const len = Math.hypot(Bn.x - A.x, Bn.y - A.y); + const ux = (Bn.x - A.x) / len; + const uy = (Bn.y - A.y) / len; + const u = clamp01(edgeU * 1.6 - i * 0.1); + if (u <= 0) return null; + const x1 = A.x + ux * NR; + const y1 = A.y + uy * NR; + const x2 = lerp(x1, Bn.x - ux * (NR + 4), u); + const y2 = lerp(y1, Bn.y - uy * (NR + 4), u); + const skip = e.from === 'a' || e.from === 'b' ? e.to === 'mn' || e.to === 'mx' : false; + return ( + + + {u > 0.98 && } + {e.w && T({ x: e.wx, y: e.wy, text: e.w, fs: 13, color: colors.MUTED, anchor: 'middle', mono: true, op: cardU })} + + ); + })} + {/* min and max share layer 3: independent */} + {indepU > 0.01 && ( + + + + )} + {NODE_ORDER.map((id) => { + const u = nodeIn(id); + if (u <= 0) return null; + const n = NODE[id]; + const isInput = id === 'a' || id === 'b'; + const lit = valU >= (id === 'a' || id === 'b' ? 1 : id === 'd' ? 2 : id === 'r' ? 3 : 4) - 0.5; + return ( + + + {isInput + ? T({ x: n.x, y: n.y + 6, text: id, fs: 17, color: colors.WARM, anchor: 'middle', mono: true }) + : id === 'r' + ? + : T({ x: n.x, y: n.y + 8, text: 'Σ', fs: 21, color: HUE, anchor: 'middle' })} + + ); + })} + {/* values and formulas (B4) */} + {T({ x: NODE.a.x - 34, y: NODE.a.y + 6, text: `a = ${CA}`, fs: 15, color: colors.WARM, anchor: 'end', mono: true, op: clamp01(valU) })} + {T({ x: NODE.b.x + 34, y: NODE.b.y + 6, text: `b = ${CB}`, fs: 15, color: colors.WARM, mono: true, op: clamp01(valU) })} + {T({ x: NODE.d.x + 34, y: NODE.d.y + 6, text: valU >= 1.5 ? `d = a − b = ${CD}` : 'd = a − b', fs: 15, color: valU >= 1.5 ? colors.WARM : colors.TEXT, mono: true, op: clamp01(valU * 2 - 1) })} + {T({ x: NODE.r.x + 34, y: NODE.r.y + 6, text: valU >= 2.5 ? `relu(d) = ${CR}` : 'relu(d)', fs: 15, color: valU >= 2.5 ? colors.WARM : colors.TEXT, mono: true, op: clamp01(valU * 2 - 1) })} + {T({ x: NODE.mn.x, y: NODE.mn.y - 46, text: valU >= 3.5 ? `min = a − relu(d) = ${CMIN}` : 'min = a − relu(d)', fs: 15, color: valU >= 3.5 ? colors.WARM : colors.TEXT, anchor: 'middle', mono: true, op: clamp01(valU * 2 - 1) })} + {T({ x: NODE.mx.x + 20, y: NODE.mx.y - 46, text: valU >= 3.5 ? `max = b + relu(d) = ${CMAX}` : 'max = b + relu(d)', fs: 15, color: valU >= 3.5 ? colors.WARM : colors.TEXT, anchor: 'middle', mono: true, op: clamp01(valU * 2 - 1) })} + + )} + + {/* B2 · cost has two numbers */} + {costU > 0.01 && ( + + {T({ x: 720, y: 240, text: 'cost has two numbers', fs: 15, color: colors.MUTED })} + {T({ x: 720, y: 300, text: 'operations in all', fs: 18 })} + {T({ x: 1000, y: 304, text: `${opsShown}`, fs: 32, color: HUE, mono: true })} + {T({ x: 720, y: 360, text: 'layers deep', fs: 18 })} + {T({ x: 1000, y: 364, text: '3', fs: 32, color: HUE, mono: true })} + + )} + {T({ x: 720, y: 430, text: 'independent: min and max share layer 3', fs: 14, color: colors.POSITIVE, op: indepU })} + {T({ x: 720, y: 456, text: 'each layer reads only earlier ones', fs: 14, color: colors.MUTED, op: indepU })} + + {/* ======================== B3 · a branching sort vs a fixed network ======================== */} + {flowU > 0.01 && b3Op > 0.01 && ( + + {T({ x: 740, y: 124, text: 'a sort that branches on comparisons', fs: 15 })} + + {T({ x: 800, y: 205, text: 'a > b ?', fs: 14, anchor: 'middle', mono: true })} + {[{ y: 160, t: 'swap' }, { y: 240, t: 'keep' }].map((bx2, k) => ( + + + + {T({ x: 930, y: bx2.y + 5, text: bx2.t, fs: 14, anchor: 'middle', mono: true, k })} + + + ))} + + {T({ x: 1085, y: 205, text: 'which pair next?', fs: 14, anchor: 'middle' })} + + + )} + {netU > 0.01 && b3Op > 0.01 && ( + + {T({ x: 740, y: 318, text: 'a sorting network: wiring fixed before any data', fs: 15, op: clamp01(netU * 3) })} + {Array.from({ length: 8 }, (_, i) => ( + + ))} + {NET8_RODS.map((r, k) => { + const x = 800 + r.col * 18 + r.s * 34; + const u = clamp01(netU * 1.6 - 0.4 - (r.s / 6) * 0.6); + if (u <= 0) return null; + const c = r.up ? HUE : colors.WARM; + return ( + + + + + + ); + })} + + + {T({ x: 770, y: 538, text: `8 wires · ${NET8.length} stages · ${comparators(NET8)} comparators, the same for every input`, fs: 13, color: colors.MUTED })} + + {Chip({ x: 740, y: 590, text: "the numbers ahead: measurements from Handwired's exploration notes", color: colors.WARM, op: stampU, fs: 14 })} + + )} + + {/* ======================== B4 · the compare-exchange card ======================== */} + {cardU > 0.01 && ( + + {T({ x: 690, y: 250, text: 'compare-exchange, keys only', fs: 14, color: colors.MUTED })} + + + {Chip({ x: 690, y: 392, text: 'emitMinimum', color: HUE, mono: true })} + {Chip({ x: 830, y: 392, text: 'emitMaximum', color: HUE, mono: true })} + {T({ x: 690, y: 452, text: '4 operations · 3 layers', fs: 22, color: HUE })} + {T({ x: 690, y: 482, text: 'three weighted sums and one relu', fs: 14, color: colors.MUTED })} + + )} + + {/* ======================== towers ======================== */} + + {foldU > 0.01 && tower(TW.x, TW.w, bitN, HUE, { band: bandU, from: foldU < 0.999 ? 3 : 0, extra: extraU })} + {cntN > 0.01 && tower(CT.x, CT.w, cntN, colors.SECONDARY, { hi: cntHi })} + {/* B5 labels */} + {T({ x: TW.x + TW.w + 14, y: topOf(bitN) + 5, text: `stage ${Math.max(1, Math.min(STAGES64, Math.ceil(bitN / 3 - 1e-6)))} of ${STAGES64}`, fs: 14, color: HUE, mono: true, op: stageLblU })} + {predU > 0.01 && ( + + + + + )} + {T({ x: TW.x + TW.w + 18, y: topOf(PRED_LAYERS) + 28, text: `${STAGES64} stages × 3 layers = ${PRED_LAYERS} · predicted`, fs: 14, color: colors.MUTED, op: predU })} + {T({ x: TW.x + TW.w + 18, y: topOf(M.keys64.layers) - 10, text: `${M.keys64.layers} layers · measured`, fs: 16, color: colors.WARM, op: measU })} + {T({ x: TW.x + TW.w + 18, y: topOf(M.keys64.layers) - 36, text: `${fmtN(M.keys64.ops)} operations · measured`, fs: 16, color: colors.WARM, op: measU })} + {/* tops and lanes (B7 on) */} + {T({ x: TW.x + TW.w / 2, y: topOf(bitN) - 10, text: `${Math.round(bitN)} layers`, fs: 15, color: HUE, anchor: 'middle', mono: true, op: topLblU })} + {T({ x: CT.x + CT.w / 2, y: topOf(cntN) - 10, text: `${Math.round(cntN)} layers`, fs: 15, color: cntHi > 0.5 ? colors.POSITIVE : colors.SECONDARY, anchor: 'middle', mono: true, op: laneBU })} + {T({ x: TW.x + TW.w / 2, y: 614, text: 'bitonic network', fs: 14, color: HUE, anchor: 'middle', op: clamp01(foldU * 2 - 1) })} + {T({ x: CT.x + CT.w / 2, y: 614, text: 'ranks by counting', fs: 14, color: colors.SECONDARY, anchor: 'middle', op: laneBU })} + {T({ x: CT.x + CT.w / 2, y: topOf(cntN) - 34, text: 'stays at 5', fs: 14, color: colors.POSITIVE, anchor: 'middle', op: cntHi })} + + + {/* ======================== condition chip + payload items ======================== */} + {Chip({ x: BAR.x, y: COND_Y, text: COND[condIdx], color: colors.MUTED, op: condOp * (finalU > 0 ? 0.12 : 1), fs: 13 })} + {payU > 0.01 && ( + + {T({ x: 720, y: 134, text: 'each item: a key, plus a payload that travels with it', fs: 14, color: colors.MUTED, op: clamp01(payU * 2) })} + {[0, 1, 2, 3].map((k) => { + const u = clamp01(payU * 4 - k); + if (u <= 0) return null; + const x = 740 + k * 120; + return ( + + + {T({ x, y: 191, text: `${KEYS[k]}`, fs: 14, color: colors.WARM, anchor: 'middle', mono: true })} + + + + ); + })} + + )} + + {/* ======================== bars: operations in all ======================== */} + {barsOp > 0.01 && ( + 0 ? 0.12 : 1)}> + {T({ x: BAR.x, y: BAR.ya - 10, text: condIdx === 0 ? 'bitonic network, keys only' : 'bitonic network', fs: 14, color: HUE })} + + {T({ x: bx(barA) + 10, y: BAR.ya + 20, text: fmtN(barA), fs: 16, color: colors.TEXT, mono: true, op: clamp01(barA / 200) })} + {Chip({ x: bx(barA) + 18 + fmtN(barA).length * 9.6, y: BAR.ya + 20, text: 'less work', color: colors.POSITIVE, op: winA, fs: 13, fill: true })} + {rowBOp > 0.01 && ( + + {T({ x: BAR.x, y: BAR.yb - 10, text: rowPhase < 0.5 ? `4 operations × ${COMPS64} comparators · arithmetic` : 'ranks by counting', fs: 14, color: rowPhase < 0.5 ? colors.MUTED : colors.SECONDARY })} + + {T({ x: bx(barB) + 10, y: BAR.yb + 20, text: fmtN(barB), fs: 16, color: rowPhase < 0.5 ? colors.MUTED : colors.TEXT, mono: true, op: clamp01(barB / 200) })} + {Chip({ x: bx(barB) + 18 + fmtN(barB).length * 9.6, y: BAR.yb + 20, text: 'less work', color: colors.POSITIVE, op: winB, fs: 13, fill: true })} + + )} + {/* the unexplained gap */} + {gapU > 0.01 && ( + + + + + {T({ x: (bx(M.keys64.ops) + bx(PRED_OPS)) / 2, y: 409, text: '?', fs: 15, color: colors.WARM, anchor: 'middle', mono: true })} + {T({ x: bx(PRED_OPS) - 22, y: 409, text: 'the notes do not explain the gap', fs: 14, color: colors.WARM, anchor: 'end' })} + + )} + {/* axis */} + + {ticks.map((t) => ( + + + {T({ x: bx(t), y: BAR.axisY + 20, text: fmtN(t), fs: 13, color: colors.MUTED, anchor: 'middle', mono: true, k: t })} + + ))} + {T({ x: BAR.x + BAR.len, y: BAR.axisY + 44, text: 'operations in all', fs: 13, color: colors.MUTED, anchor: 'end' })} + + )} + + {/* ======================== B8 · the counting grid → B11 · pair squares ======================== */} + {gridOp > 0.01 && ( + + {/* formula, headers, keys (B8 only) */} + {formU > 0.01 && cellOp > 0.01 && ( + + + + )} + {cellOp > 0.01 && gridU > 0.01 && ( + + {KEYS.map((x, j) => T({ x: GX + j * GC + GC / 2, y: GY - 10, text: `${x}`, fs: 14, color: colors.WARM, anchor: 'middle', mono: true, k: `c${j}` }))} + {KEYS.map((x, i) => { + const cy = GY + i * GC + GC / 2; + return ( + + + {T({ x: 852, y: cy + 5, text: `${x}`, fs: 13, color: colors.WARM, anchor: 'middle', mono: true })} + + ); + })} + {T({ x: GX + 8 * GC + 22, y: GY - 10, text: 'rank', fs: 13, color: HUE })} + + )} + {/* the square: 8 × 8 compare bits, later a pair count */} + + {cellOp > 0.01 && ( + + {BITS.map((row, i) => + row.map((bit, j) => { + const u = clamp01(bitsU - (i * 8 + j)); + if (u <= 0) return null; + const x = GX + j * GC; + const y = GY + i * GC; + const isTie = i === TIE.i && j === TIE.j; + return ( + + 0.01 ? colors.WARM : HUE) : 'none'} + fillOpacity={bit ? 0.75 : 0} stroke={i === j ? 'none' : colors.GRID} /> + {i === j && } + + ); + }), + )} + {RANK.map((r, i) => T({ x: GX + 8 * GC + 22, y: GY + i * GC + GC / 2 + 5, text: `${r}`, fs: 15, color: HUE, mono: true, op: clamp01(sumsU * 8 - i), k: `s${i}` }))} + {tieU > 0.01 && ( + + + + + )} + {T({ x: GX, y: 458, text: 'items 1 and 3 tie at 2: the earlier index counts as ahead', fs: 13, color: colors.WARM, op: tieU })} + {/* rank slots, and the one-hot read */} + + {T({ x: GX - 12, y: SLOT_Y + 20, text: 'slot', fs: 13, color: colors.MUTED, anchor: 'end' })} + {KEYS.map((_, r) => ( + + + {T({ x: GX + r * GC + GC / 2, y: SLOT_Y + 46, text: `${r}`, fs: 13, color: colors.MUTED, anchor: 'middle', mono: true })} + + ))} + {T({ x: GX, y: SLOT_Y + 74, text: 'a one-hot read fetches each payload into its rank', fs: 13, color: colors.MUTED, op: clamp01(slideU * 2 - 1) })} + + {KEYS.map((x, i) => { + const u = clamp01(slideU * 1.4 - i * 0.05); + const from = { x: PHOTO_X, y: GY + i * GC + GC / 2 }; + const to = { x: GX + RANK[i] * GC + GC / 2, y: SLOT_Y + GC / 2 }; + return ; + })} + + )} + {/* B11: the pair squares */} + {sqMorph > 0.01 && ( + + {sq256U > 0.01 && ( + + )} + {T({ x: SQ.x + 14, y: SQ.y - 256 * SQ.px + 28, text: `256 items: ${fmtN(PAIRS(256))} pairs (arithmetic)`, fs: 15, color: colors.NEGATIVE, op: sq256U })} + {T({ x: SQ.x + 14, y: SQ.y - 256 * SQ.px + 52, text: 'the notes: n² starts to hurt around hundreds of items', fs: 13, color: colors.MUTED, op: sq256U })} + + {T({ x: SQ.x + 64 * SQ.px + 20, y: SQ.y - 4, text: `16 items: ${fmtN(PAIRS(16))} pairs`, fs: 15, op: sq16U })} + {T({ x: SQ.x + 64 * SQ.px + 20, y: SQ.y - 64 * SQ.px + 14, text: `64 items: ${fmtN(PAIRS(64))} pairs`, fs: 15, op: sq64U })} + {T({ x: SQ.x + 64 * SQ.px + 20, y: SQ.y - 26, text: `network: ${fmtN(COMPS16)} comparators`, fs: 13, color: colors.MUTED, op: sq16U })} + {T({ x: SQ.x + 64 * SQ.px + 20, y: SQ.y - 64 * SQ.px + 34, text: `network: ${fmtN(COMPS64)} comparators`, fs: 13, color: colors.MUTED, op: sq64U })} + {T({ x: SQ.x + 64 * SQ.px + 20, y: SQ.y - 64 * SQ.px - 58, text: 'item counts interfaces usually have:', fs: 14, color: colors.POSITIVE, op: uiNoteU })} + {T({ x: SQ.x + 64 * SQ.px + 20, y: SQ.y - 64 * SQ.px - 36, text: 'counting is the better choice (notes)', fs: 14, color: colors.POSITIVE, op: uiNoteU })} + + )} + + )} + + {/* ======================== B12 · fp32 drift ======================== */} + {driftOp > 0.01 && ( + + {T({ x: 720, y: 160, text: 'fractional keys, in 32-bit floats', fs: 19 })} + {T({ x: 720, y: 192, text: `computed here: a = 2.7, b = 0.1, min = a − relu(a − b)`, fs: 14, color: colors.MUTED, mono: false })} + + {Array.from({ length: NL.kMax - NL.kMin + 1 }, (_, i) => { + const k = NL.kMin + i; + const u = clamp01(lineU * (NL.kMax - NL.kMin + 1) - i); + if (u <= 0) return null; + return ; + })} + {T({ x: nlx(NL.kMin) - 10, y: NL.y + 76, text: 'one tick = one fp32 step near 0.1 (2⁻²⁷)', fs: 13, color: colors.MUTED, op: lineU })} + {bMarkU > 0.01 && ( + + + {T({ x: nlx(0), y: NL.y - 22, text: `b = ${DB_STR}`, fs: 14, color: colors.POSITIVE, anchor: 'middle', mono: true })} + + )} + {minMarkU > 0.01 && ( + + + {T({ x: nlx(-DSTEPS), y: NL.y - 22, text: `min = ${DMIN_STR}`, fs: 14, color: colors.NEGATIVE, anchor: 'middle', mono: true })} + + )} + {gapNlU > 0.01 && ( + + + {T({ x: (nlx(-DSTEPS) + nlx(0)) / 2, y: NL.y + 46, text: `${DSTEPS} fp32 steps apart: not bit-equal to b`, fs: 14, color: colors.WARM, anchor: 'middle' })} + + )} + {measCardU > 0.01 && ( + + + {T({ x: 738, y: 458, text: 'measured by Handwired · odd-even network · fractional keys', fs: 13, color: colors.MUTED })} + {T({ x: 738, y: 490, text: '20–24% of outputs matched no input bit for bit', fs: 17 })} + {T({ x: 738, y: 520, text: 'errors up to 6e-5', fs: 17, color: colors.WARM, mono: true })} + + )} + {T({ x: 720, y: 572, text: 'integer keys: exact', fs: 15, color: colors.POSITIVE, op: intU })} + + )} + + {/* ======================== B13 · the verdict ======================== */} + {finalU > 0.01 && ( + + + {T({ x: 350, y: 236, text: 'the verdict, from the historical record', fs: 16, color: colors.MUTED })} + {T({ x: 350, y: 258, text: "Handwired's exploration notes, measured", fs: 13, color: colors.MUTED })} + + + {T({ x: 387, y: 305, text: 'holds', fs: 15, color: colors.POSITIVE, anchor: 'middle', weight: 600 })} + {T({ x: 440, y: 306, text: 'at interface sizes, count ranks', fs: 21 })} + + + + {T({ x: 387, y: 373, text: 'open', fs: 15, color: colors.WARM, anchor: 'middle', weight: 600 })} + {T({ x: 440, y: 374, text: 'package a sorting network for apps', fs: 21 })} + {T({ x: 440, y: 404, text: 'that sort well past 64 items?', fs: 21 })} + + + )} + + + {/* ======================== screen-space title ======================== */} + + + 4 · What Handwired Pays + + + ); +} + +export const vizScene = () => scene; diff --git a/apps/bookshelf/src/viz/books/handwired-cascade-argmax/chapter-2.tsx b/apps/bookshelf/src/viz/books/handwired-cascade-argmax/chapter-2.tsx index 546179aa..75a756d9 100644 --- a/apps/bookshelf/src/viz/books/handwired-cascade-argmax/chapter-2.tsx +++ b/apps/bookshelf/src/viz/books/handwired-cascade-argmax/chapter-2.tsx @@ -781,6 +781,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ======================== the bracket(s) ======================== */} + {/* ======================== the comparator ======================== */} {plotOp > 0.005 && ( diff --git a/apps/bookshelf/src/viz/books/handwired-generative-fields/chapter-2.tsx b/apps/bookshelf/src/viz/books/handwired-generative-fields/chapter-2.tsx index da977f63..f9ae6253 100644 --- a/apps/bookshelf/src/viz/books/handwired-generative-fields/chapter-2.tsx +++ b/apps/bookshelf/src/viz/books/handwired-generative-fields/chapter-2.tsx @@ -697,6 +697,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ======================== the opening card (sample 4 → sample 7) ======================== */} + {/* ======================== hero chart (B1–B2) ======================== */} {chartU > 0.01 && ( diff --git a/apps/bookshelf/src/viz/books/handwired-learned-interpreter/chapter-3.tsx b/apps/bookshelf/src/viz/books/handwired-learned-interpreter/chapter-3.tsx index e94935f4..13ce6c0a 100644 --- a/apps/bookshelf/src/viz/books/handwired-learned-interpreter/chapter-3.tsx +++ b/apps/bookshelf/src/viz/books/handwired-learned-interpreter/chapter-3.tsx @@ -589,6 +589,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ======================== the twelve chips (they later become table cells) ======================== */} {chipLayer > 0.005 && chipsU > 0.001 && ( diff --git a/apps/bookshelf/src/viz/books/handwired-reusable-layout/chapter-3.tsx b/apps/bookshelf/src/viz/books/handwired-reusable-layout/chapter-3.tsx index 3c864763..7973706a 100644 --- a/apps/bookshelf/src/viz/books/handwired-reusable-layout/chapter-3.tsx +++ b/apps/bookshelf/src/viz/books/handwired-reusable-layout/chapter-3.tsx @@ -667,6 +667,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ======================== the held-out deck ======================== */} {deckU > 0.01 && opDeck > 0.005 && ( diff --git a/apps/bookshelf/src/viz/books/handwired-trace-compiler/chapter-4.tsx b/apps/bookshelf/src/viz/books/handwired-trace-compiler/chapter-4.tsx index 6dd09ae2..be907313 100644 --- a/apps/bookshelf/src/viz/books/handwired-trace-compiler/chapter-4.tsx +++ b/apps/bookshelf/src/viz/books/handwired-trace-compiler/chapter-4.tsx @@ -397,6 +397,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ======================== the coverage wall ======================== */} diff --git a/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-2.tsx b/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-2.tsx index 7ad7e67c..2f825c80 100644 --- a/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-2.tsx +++ b/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-2.tsx @@ -577,6 +577,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ============ the gallery (persistent) ============ */} diff --git a/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-3.tsx b/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-3.tsx index 60d75092..945d33ce 100644 --- a/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-3.tsx +++ b/apps/bookshelf/src/viz/books/handwired-ui-exploration/chapter-3.tsx @@ -581,6 +581,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ======================== top-left: the app shell and its five knobs ======================== */} {topOp > 0.005 && shellU > 0.005 && ( diff --git a/apps/bookshelf/src/viz/books/handwired-user-effort/chapter-2.tsx b/apps/bookshelf/src/viz/books/handwired-user-effort/chapter-2.tsx index 64f35848..a4836b3f 100644 --- a/apps/bookshelf/src/viz/books/handwired-user-effort/chapter-2.tsx +++ b/apps/bookshelf/src/viz/books/handwired-user-effort/chapter-2.tsx @@ -559,6 +559,7 @@ export function Render({ s }: { s: SceneState }) { return ( <> + {/* ======================== scatter (the persistent machine) ======================== */} diff --git a/docs/book-plans/handwired-bitonic-sorter/book.json b/docs/book-plans/handwired-bitonic-sorter/book.json new file mode 100644 index 00000000..285606bc --- /dev/null +++ b/docs/book-plans/handwired-bitonic-sorter/book.json @@ -0,0 +1,79 @@ +{ + "slug": "handwired-bitonic-sorter", + "title": "Bitonic Sorter", + "subtitle": "Ken Batcher's fixed wiring of comparators, and what a circuit of sums and relu pays to run it.", + "animal": "pangolin", + "accent": "#3e9b73", + "chapters": [ + { + "title": "Two Wires and a Fold", + "blurb": "A comparator, a mountain-shaped sequence, and the half-cleaner that splits a mountain into two independent halves.", + "captions": [ + "Most sorting looks at the data and decides what to do next. A sorting network never decides. It is a fixed list of compare-and-swap steps, wired the same way for every input.", + "The one part is the comparator. Two numbers go in. The smaller comes out on the top wire, the larger on the bottom. Already in order, nothing happens. Out of order, they trade places.", + "In nineteen sixty-eight, Ken Batcher showed how to wire comparators into a bitonic sorter. For eight numbers it takes twenty-four comparators, and the same twenty-four sort any list of eight.", + "It rests on one shape: a sequence that climbs and then falls, which the name bitonic describes. Picture a mountain: one, two, six, seven, eight, five, three, zero.", + "Here is the half-cleaner. Pair each wire in the first half with its partner in the second half: wire zero with wire four, one with five, two with six, three with seven.", + "Four comparators fire at once. Each pair's smaller number drops into the left half, the larger into the right. The left half is now one, two, three, zero. The right is eight, five, six, seven.", + "Check the border. Everything on the left is at most three, and everything on the right is at least five. Nothing needs to cross, so the two halves can be sorted separately.", + "Each half is still a mountain, in a wider sense. The right half, eight, five, six, seven, dips like a valley, but start reading at the five and it climbs. A rotated mountain counts.", + "Wikipedia's definition allows only straight mountains, but the argument needs rotated ones too. Across all two thousand eighty-three eight-long mountains with values zero to three, the split never failed.", + "So split again. Half-clean each half into quarters, then each quarter into pairs. Run on this input, the pieces come out as zero, one, two, three, five, six, seven, eight.", + "Three rounds of this do it for eight numbers: pairs at distance four, then two, then one. Two wires is the smallest case, and a bitonic sorter of that size is simply a comparator.", + "That is the merging machine. Hand it any mountain whose length is a power of two, and it hands back a sorted line. What it cannot do yet is make mountains. That comes next." + ] + }, + { + "title": "Building the Mountains", + "blurb": "Eight shuffled numbers become mountains of two, then four, then eight, and finally one sorted line in six stages.", + "captions": [ + "A shuffled list is not a mountain, so build them from the bottom up. Any two numbers form a mountain already, and one comparator can set them to climb or to fall.", + "Take three, seven, four, eight, six, two, one, five. Stage zero compares neighbors, and the second and fourth pairs run downhill. Out comes three, seven, eight, four, two, six, five, one.", + "Now there are two mountains of four. Merge each with a half-cleaner, across a distance of two and then one. The first block sorts upward, the second downward.", + "That gives three, four, seven, eight, then six, five, two, one. Sorted up beside sorted down is exactly a mountain, which is why the directions alternate. Two sorted runs became one bigger mountain.", + "Stage three is the half-cleaner from before, across a distance of four. The four smallest values land on the left, three, four, two, one, and the four largest on the right.", + "Two more stages finish it, at distance two and then one. After the sixth stage: one, two, three, four, five, six, seven, eight.", + "Count the work. Six stages, each with four comparators on separate wires, so all four fire at the same moment. Twenty-four comparators, but only six steps in time.", + "The distances tell the story: one, then two and one, then four, two and one. Each merge is one stage longer than the last, so eight numbers take one plus two plus three, six stages.", + "Notice what the comparators never do: choose where to look. The wiring is the same for every input, and only the swaps depend on the values. That is what lets a fixed machine run it.", + "Does it sort everything? We ran all two hundred fifty-six lists of zeros and ones through eight wires, and all sixty-five thousand five hundred thirty-six through sixteen. Every one came out sorted.", + "Two thousand random lists per size, from two up to sixty-four wires, also matched the built-in sort. Thirty-two and sixty-four wires were sampled this way, not checked exhaustively." + ] + }, + { + "title": "The Staircase", + "blurb": "How many stages and comparators a bitonic sorter needs as the list doubles, why a lockstep processor likes it, and two traps for implementers.", + "captions": [ + "Let the list have two to the k wires. The last merge takes k stages, the merge before it k minus one, and so on down to one. Add them up: k times k plus one, over two.", + "For two, four, eight, sixteen, thirty-two and sixty-four wires, that is one, three, six, ten, fifteen and twenty-one stages. Each doubling adds only a few more stages.", + "Every stage has half as many comparators as wires. So the total is the number of wires times k times k plus one, over four: one, six, twenty-four, eighty, two hundred forty, six hundred seventy-two.", + "In the usual shorthand, comparators grow like n times log n squared, and stages like log n squared. From thirty-two to sixty-four wires, comparators nearly triple, while stages go only from fifteen to twenty-one.", + "That gap is why hardware likes it. A graphics processor has many units running in lockstep, and each stage gives all of them, thirty-two comparators for sixty-four numbers, work in the same instant.", + "The article also says many consecutive operations keep locality of reference, which makes them cache-friendly in practice. That is the article's claim, citing Batcher. We did not measure it.", + "The original design needs a length that is a power of two. For other lengths, pad with a sentinel bigger than everything, then drop the extras. That worked for every length from one to sixty-four in our check.", + "It is easy to get a stage wrong. The article's loop, read literally, runs one comparison too far, reaching into the neighboring block. At eight wires that fails eight of the two hundred fifty-six zeros-and-ones lists.", + "Bitonic is not the only Batcher network. His odd-even merge sort needs fewer comparators: five hundred forty-three at sixty-four wires, against six hundred seventy-two.", + "So the picture so far: half as many comparators as wires per stage, k times k plus one over two stages, and wiring that never looks at the data. Now a stranger machine, one with no swaps at all." + ] + }, + { + "title": "What Handwired Pays", + "blurb": "A comparator as three layers of sums and relu, the measured price of a bitonic network, and why ranks by counting win at interface sizes.", + "captions": [ + "Handwired writes a program as one fixed circuit of weighted sums and relu, over thirty-two-bit floats. Relu keeps a positive number and turns a negative one into zero. No loops, no branches.", + "The circuit is cut into layers. Operations that do not depend on each other share a layer, and each layer reads only earlier ones. So cost has two numbers: operations in all, and layers deep.", + "A sort that branches on comparisons cannot be written that way, but a sorting network can, because its comparators never depend on the data. The numbers ahead are measurements from Handwired's own exploration notes.", + "A comparator becomes arithmetic. Subtract the two keys, run the difference through relu, subtract that from the first key for the minimum, and add it to the second for the maximum. Four operations, three layers.", + "Twenty-one stages at three layers each predicts sixty-three layers for sixty-four keys. Handwired measured the bitonic network, keys only, at sixty-four layers and two thousand seven hundred fifty-two operations.", + "Four operations times six hundred seventy-two comparators would be two thousand six hundred eighty-eight. The notes do not explain the small gap, so read these as measurements, not a derivation.", + "Real lists carry a payload, like a photo that travels with its key. With one payload value per item, the bitonic network measured one thousand two hundred forty-eight operations in fifty layers at sixteen items.", + "The rival needs no network. A rank counts the items ahead of this one, so compare every pair and add. Equal keys break ties by index, and that count is the sorted position. A one-hot read fetches the payload.", + "At sixteen items, counting wins on work: one thousand fifty-six operations against one thousand two hundred forty-eight, in five layers against fifty.", + "At sixty-four, the network wins on work: ten thousand six hundred twenty-four operations against sixteen thousand five hundred twelve. But it is one hundred five layers deep, and counting stays at five.", + "Pairs grow with the square of the item count. Handwired's notes still call counting the better choice for the item counts interfaces usually have, with trouble starting around hundreds of items.", + "One caveat. Minimum and maximum built from arithmetic drift on fractional keys. On the odd-even network, twenty to twenty-four percent of outputs matched no input bit for bit, with errors up to six in a hundred thousand.", + "The verdict, from that historical record: at interface sizes, count ranks. Whether to package a sorting network for apps that sort well past sixty-four items is still an open question." + ] + } + ] +} diff --git a/docs/book-plans/handwired-bitonic-sorter/plan.md b/docs/book-plans/handwired-bitonic-sorter/plan.md new file mode 100644 index 00000000..63f3248b --- /dev/null +++ b/docs/book-plans/handwired-bitonic-sorter/plan.md @@ -0,0 +1,145 @@ +# Handwired: Bitonic Sorter +- **Title:** Bitonic Sorter +- **Subtitle:** Ken Batcher's fixed wiring of comparators, and what a circuit of sums and relu pays to run it. +- **Animal:** pangolin (scales that fold over each other, like the half-cleaner folding a sequence) +- **Accent:** #3e9b73 +- **Source:** Wikipedia "Bitonic sorter" rev 1373262602 (cited as wiki:LINE); chenglou/handwired @ 4ea7fcb27ebe93acf49b9f29a93e162b975bad46 (`lab/ui-exploration`); exploration-branch notes at d01b763 (notes-outside, notes-runtime); numbers verified by the reference implementation in `.orly-work/hw-books/bitonic-ref/` (report.txt). Lab status: historical record of measurements; the sorting-network-versus-counting verdict holds at UI sizes; packaging a network past 64 items is open. +- **Throughline noun:** the mountain (a bitonic sequence), then the comparator in Handwired. +- **Vocabulary fixed for the whole book:** "stage" = one parallel column of comparators in a network; "layer" = one dependency level of Handwired's circuit (a stage costs three layers there); "delay"/"parallel time" are never used. +- **Honesty notes:** Credit Ken Batcher (1968). Handwired's numbers are measurements from its exploration notes, quoted as measured; the 2,752 vs 2,688 and 64 vs 63 gaps are stated as unexplained and the payload per-comparator cost is never stated. Fractional-key drift was measured on the odd-even network only. The locality claim is the article's, unmeasured. Article defects (infobox "space", inclusive loop bound, the bitonic definition gap, the correctness sketch, the "k levels" conflation, "k" as count) are never repeated; chapter 1 and 3 state the corrected forms. Chapter 3 beat 8 must recompute the inclusive-bound output by running the reference code, not from memory. +- Estimated runtime: 4 chapters x 70-110 s. + +## Chapter 1: Two Wires and a Fold + +**Thesis:** One fixed fold, comparing wire i with wire i plus half, turns any bitonic sequence into two halves that never need to talk to each other again. + +**Visual machine:** Eight vertical bars on eight wires (heights 1 2 6 7 8 5 3 0). Four arcs link wire i to wire i+4; the comparators fire together and the bars physically slide into a low half and a high half separated by a shaded gap; recursion then folds each half again until the bars stand sorted. + +**Beat sheet** (caption numbers in brackets) +1. Two wires with bars of height 3 and 7. The comparator rod between them lights; the in-order pair does not move, then the pair 7,3 crosses over on swept arcs so the smaller bar rises to the top wire. Camera tight on the pair. [captions 1, 2] +2. Pull back: a faint ghost of the 8-wire, 24-comparator network passes behind, a counter reads "24 comparators", and a plaque reads "Ken Batcher, 1968". [captions 3] +3. Eight bars [1, 2, 6, 7, 8, 5, 3, 0] grow in on wires 0 to 7. A smooth outline traces the climb and fall; a peak marker sits at wire 4 (value 8); label "bitonic: up, then down". [captions 4] +4. Camera pushes in. Four arcs link wire i to wire i+4 (0-4, 1-5, 2-6, 3-7), index labels at each end, each pair pulsing in its own hue. [captions 5] +5. The four comparators fire at once. In each pair the smaller bar slides into slots 0 to 3 and the larger into slots 4 to 7. Left half [1, 2, 3, 0] tinted cool, right half [8, 5, 6, 7] tinted warm. A small table of x[i], x[i+4], min, max builds beside it from the real values. [captions 6] +6. A divider drops between the halves. A ceiling line at height 3 on the left and a floor line at height 5 on the right; the gap between them is shaded. [captions 7] +7. The right half outline is traced as a valley (8 5 6 7). The four bars then slide around a ring so 5 comes first and the outline becomes a climb; a counter ticks up to "2,083 mountains - 415 rotated right halves - 0 failures". [captions 8, 9] +8. Recursion: arcs at distance 2 on each half, then distance 1. Left half goes [1, 2, 3, 0] to [1, 0, 3, 2] to [0, 1, 2, 3]; right half [8, 5, 6, 7] to [6, 5, 8, 7] to [5, 6, 7, 8]. Bars stand in order 0 1 2 3 5 6 7 8. [captions 10] +9. Beneath, a three-column mini network assembles with labels "distance 4", "distance 2", "distance 1", four comparators in each column; a bracket marks the 2-wire case "= one comparator". [captions 11] +10. Quiet end: the sorted bars remain, everything else fades to 0.15; label "bitonic in, sorted out". [captions 12] + +**Captions** +1. Most sorting looks at the data and decides what to do next. A sorting network never decides. It is a fixed list of compare-and-swap steps, wired the same way for every input. [loops.md:185] +2. The one part is the comparator. Two numbers go in. The smaller comes out on the top wire, the larger on the bottom. Already in order, nothing happens. Out of order, they trade places. [wikipedia §Bitonic sorter, wiki:26] +3. In nineteen sixty-eight, Ken Batcher showed how to wire comparators into a bitonic sorter. For eight numbers it takes twenty-four comparators, and the same twenty-four sort any list of eight. [wikipedia §intro, wiki:14; report.txt §1 n=8, §5, §5b] +4. It rests on one shape: a sequence that climbs and then falls, which the name bitonic describes. Picture a mountain: one, two, six, seven, eight, five, three, zero. [wikipedia, wiki:16; report.txt §4] +5. Here is the half-cleaner. Pair each wire in the first half with its partner in the second half: wire zero with wire four, one with five, two with six, three with seven. [wikipedia, wiki:41-42] +6. Four comparators fire at once. Each pair's smaller number drops into the left half, the larger into the right. The left half is now one, two, three, zero. The right is eight, five, six, seven. [report.txt §4 table] +7. Check the border. Everything on the left is at most three, and everything on the right is at least five. Nothing needs to cross, so the two halves can be sorted separately. [report.txt §4 (max low 3 <= min high 5); wiki:45-46] +8. Each half is still a mountain, in a wider sense. The right half, eight, five, six, seven, dips like a valley, but start reading at the five and it climbs. A rotated mountain counts. [report.txt §4 (cyclic-bitonic true)] +9. Wikipedia's definition allows only straight mountains, but the argument needs rotated ones too. Across all two thousand eighty-three eight-long mountains with values zero to three, the split never failed. [report.txt §4b; inputs/bitonic-sorter.md Article defects #3] +10. So split again. Half-clean each half into quarters, then each quarter into pairs. Run on this input, the pieces come out as zero, one, two, three, five, six, seven, eight. [wikipedia, wiki:33-36; report.txt §6b (exclusive merge output)] +11. Three rounds of this do it for eight numbers: pairs at distance four, then two, then one. Two wires is the smallest case, and a bitonic sorter of that size is simply a comparator. [report.txt §2 stages 3-5; wiki:26] +12. That is the merging machine. Hand it any mountain whose length is a power of two, and it hands back a sorted line. What it cannot do yet is make mountains. That comes next. [wiki:20, wiki:33-36] + +**On-screen labels:** `x[i]` vs `x[i + 4]` (low = min, high = max); bars [1, 2, 6, 7, 8, 5, 3, 0]; low half [1, 2, 3, 0]; high half [8, 5, 6, 7]; "max(low) = 3 <= min(high) = 5"; rotated high half [5, 6, 7, 8]; counter "2,083 bitonic inputs, 415 rotated high halves, 0 failures (values 0..3, n = 8)"; "24 comparators", "Ken Batcher, 1968"; distance labels 4, 2, 1; sub-results [0, 1, 2, 3] and [5, 6, 7, 8]; final [0, 1, 2, 3, 5, 6, 7, 8]. + +## Chapter 2: Building the Mountains + +**Thesis:** Sorting is merging mountains bottom-up: sorted-up beside sorted-down is a mountain, so alternating directions feed each merge. + +**Visual machine:** An 8-wire network drawn left to right with a value disc riding each wire. Comparator rods are blue when they send the smaller value up and orange when down. Stage columns light in turn while the discs trade places; a trace table on the right fills with the real array after each stage, and brackets under the wires show block sizes 2, 4, 8. + +**Beat sheet** (caption numbers in brackets) +1. Eight discs [3, 7, 4, 8, 6, 2, 1, 5] sit on eight bare wires. Brackets of size 2 appear under pairs, each labelled as a tiny mountain. [captions 1] +2. Stage 0 column: four rods, 0-1 blue, 2-3 orange, 4-5 blue, 6-7 orange. Discs trade; the table row reads [3, 7, 8, 4, 2, 6, 5, 1]. [captions 2] +3. Brackets grow to size 4. Stages 1 and 2 light with distances 2 then 1: rods 0-2, 1-3 blue, 4-6, 5-7 orange; then 0-1, 2-3 blue, 4-5, 6-7 orange. Table rows [3, 4, 8, 7, 5, 6, 2, 1] then [3, 4, 7, 8, 6, 5, 2, 1]. [captions 3] +4. Silhouette of the two blocks: 3 4 7 8 climbing, 6 5 2 1 falling; an outline traces the mountain they form together. [captions 4] +5. Stage 3: four long rods at distance 4, all blue. Camera pushes in; the 4 smallest values slide to the left half. Row [3, 4, 2, 1, 6, 5, 7, 8]. [captions 5] +6. Stages 4 and 5 (distance 2, then 1), all blue. Row [2, 1, 3, 4, 6, 5, 7, 8] then [1, 2, 3, 4, 5, 6, 7, 8]; the final line flashes sorted. [captions 6] +7. All six columns visible; each lights all four rods simultaneously. Counter: "6 stages x 4 comparators = 24". [captions 7] +8. Distance ribbon under the columns: 1 | 2 1 | 4 2 1, grouped by merge; the sum "1 + 2 + 3 = 6" appears; ghost of the n = 16 version (10 stages) fades in beside it. [captions 8] +9. Replay with another input: the same 24 rods in the same places, different discs swapping. Label "wiring fixed, swaps data-dependent". [captions 9] +10. Two counters tick up: "256 / 256 zero-one lists sorted at 8 wires", "65,536 / 65,536 at 16 wires", then a sample strip "2,000 random lists per size, 2 to 64 wires, 0 mismatches" with 32 and 64 marked "sampled". [captions 10, 11] + +**Captions** +1. A shuffled list is not a mountain, so build them from the bottom up. Any two numbers form a mountain already, and one comparator can set them to climb or to fall. [wiki:80-82; report.txt §2] +2. Take three, seven, four, eight, six, two, one, five. Stage zero compares neighbors, and the second and fourth pairs run downhill. Out comes three, seven, eight, four, two, six, five, one. [report.txt §3, §2 stage 0] +3. Now there are two mountains of four. Merge each with a half-cleaner, across a distance of two and then one. The first block sorts upward, the second downward. [report.txt §2 stages 1-2] +4. That gives three, four, seven, eight, then six, five, two, one. Sorted up beside sorted down is exactly a mountain, which is why the directions alternate. Two sorted runs became one bigger mountain. [report.txt §3 after stage 2; wiki:80-82] +5. Stage three is the half-cleaner from before, across a distance of four. The four smallest values land on the left, three, four, two, one, and the four largest on the right. [report.txt §3 after stage 3, §2 stage 3] +6. Two more stages finish it, at distance two and then one. After the sixth stage: one, two, three, four, five, six, seven, eight. [report.txt §3 after stages 4-5] +7. Count the work. Six stages, each with four comparators on separate wires, so all four fire at the same moment. Twenty-four comparators, but only six steps in time. [report.txt §1 n=8] +8. The distances tell the story: one, then two and one, then four, two and one. Each merge is one stage longer than the last, so eight numbers take one plus two plus three, six stages. [report.txt §2; wiki:138] +9. Notice what the comparators never do: choose where to look. The wiring is the same for every input, and only the swaps depend on the values. That is what lets a fixed machine run it. [report.txt §2 (stage list is input-independent); loops.md:185] +10. Does it sort everything? We ran all two hundred fifty-six lists of zeros and ones through eight wires, and all sixty-five thousand five hundred thirty-six through sixteen. Every one came out sorted. [report.txt §5] +11. Two thousand random lists per size, from two up to sixty-four wires, also matched the built-in sort. Thirty-two and sixty-four wires were sampled this way, not checked exhaustively. [report.txt §5b; inputs/bitonic-sorter.md "What the checks cover"] + +**On-screen labels:** input [3, 7, 4, 8, 6, 2, 1, 5]; trace [3, 7, 8, 4, 2, 6, 5, 1], [3, 4, 8, 7, 5, 6, 2, 1], [3, 4, 7, 8, 6, 5, 2, 1], [3, 4, 2, 1, 6, 5, 7, 8], [2, 1, 3, 4, 6, 5, 7, 8], [1, 2, 3, 4, 5, 6, 7, 8]; stage labels "stage 0" to "stage 5" with pairs e.g. "(0,1 asc) (2,3 desc) (4,5 asc) (6,7 desc)"; legend asc / desc; "6 stages x 4 comparators = 24"; distance ribbon 1 | 2 1 | 4 2 1; "256 inputs, 0 not sorted (n = 8)"; "65536 inputs, 0 not sorted (n = 16)"; "2000 random arrays per size, 0 mismatches". Calls: `bitonicSort(a, 0, n, 1)`, `bitonicMerge`. + +## Chapter 3: The Staircase + +**Thesis:** Stages grow like k(k+1)/2 and comparators like n times k(k+1)/4, so doubling the list adds a handful of stages but nearly triples the comparators. + +**Visual machine:** A bitonic network that grows: 8 wires and 6 columns morph to 16 wires and 10 columns, then 32 and 64 (21 columns). Columns are colored by comparator distance. A sweeping highlight lights one whole column at a time, and a stage staircase (bars 1, 3, 6, 10, 15, 21) plus a comparator counter sit beside it. + +**Beat sheet** (caption numbers in brackets) +1. Start from the 8-wire, 6-column network of chapter 2. Under it, merge brackets of 1, 2, 3 columns; the sum "k = 3: 1 + 2 + 3" appears as "k(k+1)/2 = 6". [captions 1] +2. Network morphs to 16 wires (new wires splice in, new columns slide in, 10 columns); a staircase on the right grows one step per size: 1, 3, 6, 10, 15, 21 for 2, 4, 8, 16, 32, 64 wires, each step labelled with its increment. [captions 2] +3. Camera pushes to a single column: half as many rods as wires; a counter reads n/2 per column and multiplies out to 1, 6, 24, 80, 240, 672 comparators as the sizes cycle. [captions 3] +4. Two curves share an axis on log2 n: stages (gentle) and comparators (steep). Marker at 32 to 64 wires: stages 15 to 21, comparators 240 to 672 ("about 2.8 times"). [captions 4] +5. Full 64-wire network (21 columns); a sweep lights one column at a time; each lit column flashes 32 rods at once with a counter "32 comparators per step, 21 steps". A row of lockstep lanes labelled "parallel units" lights in unison. [captions 5] +6. A small quote card, attributed: "the article's claim: locality of reference, cache-friendly". Label "not measured here". [captions 6] +7. Seven values pad to eight: a bar of infinity ("sentinel") joins at the end, the 8-wire network runs, and the extra bar is trimmed. A strip "m = 1 to 64, 3,200 arrays, 0 mismatches". [captions 7] +8. Inset on the 8-wire network: a ghost comparator reaches from the last wire of a block into the next block; the zero-one list 1 1 1 1 0 0 0 0 fails with the extra rod, passes without it. Counter "8 of 256 fail". Recompute the failing output by running the inclusive-bound variant in bitonic-ref/bitonic.ts and show only what it prints. [captions 8] +9. Second network overlay in a different hue: odd-even merge, 64 wires, with counter "543 comparators" beside "672". [captions 9] +10. Quiet: the staircase and two formulas remain; the network fades to 0.15. [captions 10] + +**Captions** +1. Let the list have two to the k wires. The last merge takes k stages, the merge before it k minus one, and so on down to one. Add them up: k times k plus one, over two. [wiki:138; report.txt §1] +2. For two, four, eight, sixteen, thirty-two and sixty-four wires, that is one, three, six, ten, fifteen and twenty-one stages. Each doubling adds only a few more stages. [report.txt §1] +3. Every stage has half as many comparators as wires. So the total is the number of wires times k times k plus one, over four: one, six, twenty-four, eighty, two hundred forty, six hundred seventy-two. [report.txt §1] +4. In the usual shorthand, comparators grow like n times log n squared, and stages like log n squared. From thirty-two to sixty-four wires, comparators nearly triple, while stages go only from fifteen to twenty-one. [wiki:14; report.txt §1] +5. That gap is why hardware likes it. A graphics processor has many units running in lockstep, and each stage gives all of them, thirty-two comparators for sixty-four numbers, work in the same instant. [wiki:14 ("such as a typical GPU"); report.txt §1] +6. The article also says many consecutive operations keep locality of reference, which makes them cache-friendly in practice. That is the article's claim, citing Batcher. We did not measure it. [wiki:140; inputs/bitonic-sorter.md fact 7] +7. The original design needs a length that is a power of two. For other lengths, pad with a sentinel bigger than everything, then drop the extras. That worked for every length from one to sixty-four in our check. [wiki:20, wiki:99; report.txt §7] +8. It is easy to get a stage wrong. The article's loop, read literally, runs one comparison too far, reaching into the neighboring block. At eight wires that fails eight of the two hundred fifty-six zeros-and-ones lists. [wiki:109; report.txt §6; Article defects #2] +9. Bitonic is not the only Batcher network. His odd-even merge sort needs fewer comparators: five hundred forty-three at sixty-four wires, against six hundred seventy-two. [report.txt §1 odd-even line; loops.md:185] +10. So the picture so far: half as many comparators as wires per stage, k times k plus one over two stages, and wiring that never looks at the data. Now a stranger machine, one with no swaps at all. [report.txt §1; loops.md:185] + +**On-screen labels:** n = 2, 4, 8, 16, 32, 64; k = 1..6; stages k(k+1)/2 = 1, 3, 6, 10, 15, 21; comparators n*k(k+1)/4 = 1, 6, 24, 80, 240, 672; "32 comparators per stage at n = 64"; "240 to 672 comparators, 15 to 21 stages"; "O(n log^2 n) comparators, O(log^2 n) stages"; sentinel +Infinity, "m = 1..64, 3200 arrays, 0 mismatches"; loop bound `i < low + k` (correct) versus `i <= low + k` (as printed): "8 of 256 zero-one inputs fail at n = 8, 256 of 65,536 at n = 16"; failing input [1, 1, 1, 1, 0, 0, 0, 0]; odd-even "543 comparators, n = 64". + +## Chapter 4: What Handwired Pays + +**Thesis:** A sorting network fits a circuit of sums and relu, but measured against ranks by counting it costs more depth and, past sixty-four items, an open question. + +**Visual machine:** A circuit-to-scoreboard machine. First a two-input compare-exchange drawn as nodes (a, b, difference, relu, minimum, maximum). Then the circuit tiles into a tower of layers, one slab per layer, growing upward. Finally two towers per item count (16 and 64) stand side by side, bitonic versus counting, with a horizontal operations bar under each and a heatmap of the pairwise comparison grid for counting. + +**Beat sheet** (caption numbers in brackets) +1. A bare grid of cells labelled "weighted sum" and a relu curve (flat at zero, then rising) draw in; three slabs stack, labelled layer 1, 2, 3, each reading only the slab below. [captions 1, 2] +2. A conventional branching sort flow dims with an "X"; a fixed 8-wire network fades in with the stamp "measurements from exploration notes, September 2026". [captions 3] +3. Zoom to the compare-exchange: a and b into a subtract node (d = a - b), relu node (relu(d)), then two outputs: min = a - relu(d), max = b + relu(d). Counters "4 operations", "3 layers". Numeric example a = 7, b = 3 flowing through (d = 4, relu = 4, min = 3, max = 7). [captions 4] +4. 21 stage slabs of 3 layers pile into a tower to 63 layers, ghosted; beside it the measured 64-layer tower overlays with the extra layer highlighted; operations bar "2,688 predicted" vs "2,752 measured" with a "?" gap marker labelled "notes do not explain". [captions 5, 6] +5. Photos (cards) ride along with their keys through the comparator: a payload tag on each disc. Towers for 16 items (50 layers, 1,248 ops) and 64 items (105 layers, 10,624 ops) rise. [captions 7] +6. Counting grid: an 8 x 8 matrix of compare bits lights `x_j < x_i`; row sums print as ranks 0..7; each card slides straight to its rank slot (a one-hot read). [captions 8] +7. Race at 16 items: bitonic ops bar 1,248 vs counting 1,056 (counting shorter), towers 50 vs 5. [captions 9] +8. Race at 64 items: bitonic 10,624 vs counting 16,512 (network shorter), towers 105 vs 5. Item-count axis sketch shows n squared curve rising past the network curve; marker "hundreds of items". [captions 10] +9. Fractional-key inset: a number line with rounding error ticks; labels "20-24% of outputs not bit-equal, errors up to 6e-5 (odd-even network, measured)" and "integers: exact"; counting grid shown unaffected. [captions 11] +10. Status stamps appear on the scoreboard: "historical record" on the measurements, "holds: count ranks at UI sizes", "open: package a network past 64 items". [captions 12] +11. Quiet recap strip fading in left to right: comparator, mountain fold, k(k+1)/2 staircase, 3 layers per comparator, two towers; everything else at 0.15. [captions 13] + +**Captions** +1. Handwired writes a program as one fixed circuit of weighted sums and relu, over thirty-two-bit floats. Relu keeps a positive number and turns a negative one into zero. No loops, no branches. [README.md:5 (lab/ui-exploration); README.md:18] +2. The circuit is cut into layers. Operations that do not depend on each other share a layer, and each layer reads only earlier ones. So cost has two numbers: operations in all, and layers deep. [docs/execution-model.md:40] +3. A sort that branches on comparisons cannot be written that way, but a sorting network can, because its comparators never depend on the data. The numbers ahead are measurements from Handwired's own exploration notes. [docs/patterns.md:358; loops.md:185; notes-outside:109] +4. A comparator becomes arithmetic. Subtract the two keys, run the difference through relu, subtract that from the first key for the minimum, and add it to the second for the maximum. Four operations, three layers. [loops.md:185; notes-runtime:66] +5. Twenty-one stages at three layers each predicts sixty-three layers for sixty-four keys. Handwired measured the bitonic network, keys only, at sixty-four layers and two thousand seven hundred fifty-two operations. [notes-outside:32; report.txt §1 n=64] +6. Four operations times six hundred seventy-two comparators would be two thousand six hundred eighty-eight. The notes do not explain the small gap, so read these as measurements, not a derivation. [notes-outside:32; report.txt §1; inputs/bitonic-sorter.md "Handwired's measurements"] +7. Real lists carry a payload, like a photo that travels with its key. With one payload value per item, the bitonic network measured one thousand two hundred forty-eight operations in fifty layers at sixteen items. [loops.md:189] +8. The rival needs no network. A rank counts the items ahead of this one, so compare every pair and add. Equal keys break ties by index, and that count is the sorted position. A one-hot read fetches the payload. [notes-runtime:238; docs/patterns.md:358; ranks-instead-of-sorting.md:73] +9. At sixteen items, counting wins on work: one thousand fifty-six operations against one thousand two hundred forty-eight, in five layers against fifty. [loops.md:189, :192] +10. At sixty-four, the network wins on work: ten thousand six hundred twenty-four operations against sixteen thousand five hundred twelve. But it is one hundred five layers deep, and counting stays at five. [loops.md:190, :192] +11. Pairs grow with the square of the item count. Handwired's notes still call counting the better choice for the item counts interfaces usually have, with trouble starting around hundreds of items. [notes-outside:113; ranks-instead-of-sorting.md:84] +12. One caveat. Minimum and maximum built from arithmetic drift on fractional keys. On the odd-even network, twenty to twenty-four percent of outputs matched no input bit for bit, with errors up to six in a hundred thousand. [loops.md:194; notes-runtime:237; execution-model.md:282] +13. The verdict, from that historical record: at interface sizes, count ranks. Whether to package a sorting network for apps that sort well past sixty-four items is still an open question. [review-and-decisions.md:269; notes-outside:113; loops.md:192] + +**On-screen labels:** `emitMinimum`, `emitMaximum`, `emitCompare`; "min = a - relu(a - b)", "max = b + relu(a - b)"; "4 operations, 3 layers"; example a = 7, b = 3; rank formula `rank_i = sum_j [x_j < x_i or (x_j = x_i and j < i)]` (ties break by index); table (measured, integer keys): 16 items with payload: bitonic 1,248 operations / 50 layers, counting 1,056 / 5; 64 items with payload: bitonic 10,624 / 105, counting 16,512 / 5; 64 keys only: bitonic 2,752 / 64, odd-even 2,172 / 63 (Batcher network); predicted 21 x 3 = 63 layers, 672 x 4 = 2,688 operations (arithmetic, not measured); counting alone 4,096 operations in 3 layers (ranks only, different conditions); float drift "20-24% not bit-equal, errors up to 6e-5 (odd-even, fractional keys)"; stamps "measurements from exploration notes", "historical", "holds at UI sizes", "open past 64 items". Source tag "chenglou/handwired @ 4ea7fcb27ebe", notes tag exploration-2026-09. diff --git a/docs/book-plans/handwired-bitonic-sorter/review.md b/docs/book-plans/handwired-bitonic-sorter/review.md new file mode 100644 index 00000000..5cfca335 --- /dev/null +++ b/docs/book-plans/handwired-bitonic-sorter/review.md @@ -0,0 +1,99 @@ +# Review: fact-check of the Bitonic Sorter captions + +Reviewer: Claude Haiku 5.5 (role: review). Checked: all 46 captions in `book.json` (chapters 1 to 4, 12 + 11 + 10 + 13), each against its cited source. Three captions were edited in place in both `book.json` and `plan.md`: 3 FIX, 43 OK. + +Checks run: +- `bun .orly-work/hw-books/bitonic-ref/report.ts` re-run: output byte-identical to `report.txt`. +- Scratch `.orly-work/hw-books/bitonic-ref/review/merge-check.ts`: the bitonic merge sorts every article-bitonic sequence (n = 4: 130, n = 8: 2,083, n = 16 zero-one: 137; all 0 unsorted) and 2,000 random mountains each at n = 8, 16, 32, 64 (0 unsorted). +- Scratch `.orly-work/hw-books/bitonic-ref/review/check-captions.ts`: `book.json` parses, all 46 captions are at most 220 characters, and each caption appears verbatim in `plan.md`. +- Hand arithmetic: n·k(k+1)/4 and k(k+1)/2 for n = 2 to 64; 4 × 672 = 2,688; 2,752 / 2,688 = 1.024; 10,624 vs 16,512; 672 / 240 = 2.8; odd-even closed form (k²−k+4)·2^(k−2) − 1 = 543 at k = 6. +- Grep of the exploration notes for any explanation of 2,752, 2,688, or 64 vs 63 layers: none found, so "the notes do not explain" stands. + +Source abbreviations (paths relative to the worktree): +- `wiki` = `.orly-work/hw-books/inputs/bitonic/wikipedia-bitonic-sorter.wikitext` +- `report` = `.orly-work/hw-books/bitonic-ref/report.txt` +- `bitonic.ts` = `.orly-work/hw-books/bitonic-ref/bitonic.ts` +- `merge-check` = `.orly-work/hw-books/bitonic-ref/review/merge-check.ts` (scratch, not a source of the book) +- `loops` = `.orly-work/hw-books/repo/lab/ui-exploration/guide/sections/loops.md` +- `ranks` = `.orly-work/hw-books/repo/lab/ui-exploration/guide/patterns/ranks-instead-of-sorting.md` +- `review-dec` = `.orly-work/hw-books/repo/lab/ui-exploration/guide/review-and-decisions.md` +- `lab-readme` = `.orly-work/hw-books/repo/lab/ui-exploration/README.md` +- `exec` = `.orly-work/hw-books/repo/docs/execution-model.md` +- `patterns` = `.orly-work/hw-books/repo/docs/patterns.md` +- `notes-out` = `.orly-work/hw-books/inputs/bitonic/handwired-notes-outside-techniques.md` +- `notes-rt` = `.orly-work/hw-books/inputs/bitonic/handwired-notes-runtime.md` + +The verbatim quote is taken from the cited source. Each quote was copied from the file and checked against it. + +## Table + +| chapter | # | verdict | old → new | source path:line | verbatim quote | +|---|---|---|---|---|---| +| 1 Two Wires and a Fold | 1 | OK | | `loops`:185 | "A sorting network is a fixed list of compare-exchange steps that sorts any input" | +| 1 Two Wires and a Fold | 2 | OK | | `wiki`:26 | "X and Y represent the inputs, while H and L represent the higher and lower outputs, respectively." (smaller-on-top is the ascending convention, not stated in source) | +| 1 Two Wires and a Fold | 3 | OK | | `wiki`:14; `report`:6 | "Proceedings of the April 30--May 2, 1968" / "8 3 6 6 24 24 yes" | +| 1 Two Wires and a Fold | 4 | OK | | `wiki`:16; `report`:44 | "A sequence is bitonic when it consists of a non-decreasing sequence followed by a non-increasing sequence" / "== 4. worked half-cleaner, bitonic 8-sequence [1, 2, 6, 7, 8, 5, 3, 0] ==" | +| 1 Two Wires and a Fold | 5 | OK | | `wiki`:41-42 | "elements of the first half of the input sequence are pairwise compared against corresponding elements of the last" | +| 1 Two Wires and a Fold | 6 | OK | | `report`:51-52 | "low half [1, 2, 3, 0] article-bitonic: true cyclic-bitonic: true" / "high half [8, 5, 6, 7] article-bitonic: false cyclic-bitonic: true" | +| 1 Two Wires and a Fold | 7 | OK | | `report`:53; `wiki`:45-47 | "max(low) = 3 <= min(high) = 5: true" / "all elements in the red sequence are guaranteed to be higher than all elements in the blue series." (example input; the general claim is caption 9) | +| 1 Two Wires and a Fold | 8 | OK | | `report`:52 | "high half [8, 5, 6, 7] article-bitonic: false cyclic-bitonic: true" | +| 1 Two Wires and a Fold | 9 | OK | | `report`:56, 61 | "article-bitonic inputs: 2083" / "max(low) > min(high): 0" (also `merge-check`: 2,083 mountains, 0 unsorted) | +| 1 Two Wires and a Fold | 10 | OK | | `report`:84-85; `wiki`:33-36 | "bitonicMerge(a, 0, 8, 1, exclusive) on the same input -> [0, 1, 2, 3, 5, 6, 7, 8]" | +| 1 Two Wires and a Fold | 11 | OK | | `report`:17; `wiki`:26 | "stage 3: (0,4 asc) (1,5 asc) (2,6 asc) (3,7 asc)" / "A bitonic sorter for k=1 (n=2) is simply a comparator." | +| 1 Two Wires and a Fold | 12 | OK | | `wiki`:20; `merge-check` | "which requires an input sequence whose length n is a perfect power of two" (`merge-check`: every mountain tested sorts; n = 8 exhaustive over 2,083, n up to 64 random) | +| 2 Building the Mountains | 1 | OK | | `wiki`:80-82; `report`:14 | "These bitonic sorters are recursively used to create two monotonic sequences" (a 2-number sequence is always monotone, so always a mountain) | +| 2 Building the Mountains | 2 | OK | | `report`:14, 35 | "stage 0: (0,1 asc) (2,3 desc) (4,5 asc) (6,7 desc)" / "after stage 0 [3, 7, 8, 4, 2, 6, 5, 1]" | +| 2 Building the Mountains | 3 | OK | | `report`:15-16 | "stage 1: (0,2 asc) (1,3 asc) (4,6 desc) (5,7 desc)" | +| 2 Building the Mountains | 4 | OK | | `report`:37 | "after stage 2 [3, 4, 7, 8, 6, 5, 2, 1]" | +| 2 Building the Mountains | 5 | OK | | `report`:38 | "after stage 3 [3, 4, 2, 1, 6, 5, 7, 8]" | +| 2 Building the Mountains | 6 | OK | | `report`:39-40 | "after stage 4 [2, 1, 3, 4, 6, 5, 7, 8]" / "after stage 5 [1, 2, 3, 4, 5, 6, 7, 8]" (stages are numbered 0 to 5, so "sixth stage" is stage 5) | +| 2 Building the Mountains | 7 | OK | | `report`:6, 14-19 | "8 3 6 6 24 24 yes" (6 stages, 4 comparators each, as in each stage line) | +| 2 Building the Mountains | 8 | OK | | `report`:17; `wiki`:138 | "stage 3: (0,4 asc) (1,5 asc) (2,6 asc) (3,7 asc)" / "the number of levels in which each comparison can be done in parallel is given by" (distances 1; 2,1; 4,2,1 total 1+2+3 = 6 stages) | +| 2 Building the Mountains | 9 | OK | | `bitonic.ts`:59; `notes-out`:109 | "export function bitonicNetwork(n: number): Comparator[][]" / "so its structure does not depend on the data" (network takes only n, so the wiring is input-independent) | +| 2 Building the Mountains | 10 | OK | | `report`:64-65 | "n=8: 256 inputs, 0 not sorted" / "n=16: 65536 inputs, 0 not sorted" | +| 2 Building the Mountains | 11 | OK | | `report`:67-73 | "2000 arrays per size, values 0..999" / "n=64 network mismatches 0, recursive pseudocode mismatches 0" | +| 3 The Staircase | 1 | OK | | `wiki`:138; `report`:3-9 | "the number of levels in which each comparison can be done in parallel is given by" / "64 6 21 21 672 672 yes" | +| 3 The Staircase | 2 | OK | | `report`:4-9 | "16 4 10 10 80 80 yes" (stages column: 1, 3, 6, 10, 15, 21) | +| 3 The Staircase | 3 | OK | | `report`:4-9 | "16 4 10 10 80 80 yes" (comparators column: 1, 6, 24, 80, 240, 672) | +| 3 The Staircase | 4 | FIX | "and delay like log n squared" → "and stages like log n squared" | `wiki`:14; `plan.md` line 8 (vocabulary rule); research notes Article defects #10 | "and have a delay of" (book term is "stages"; "delay", "levels" and "parallel time" name the same count, article defect 10) | +| 3 The Staircase | 5 | OK | | `wiki`:14; `report`:9 | "such as a typical" (GPU, lockstep units); 672 / 21 = 32 comparators per stage at n = 64 | +| 3 The Staircase | 6 | FIX | "consecutive operations keep good locality of reference, so it is cache-friendly in practice" → "many consecutive operations keep locality of reference, which makes them cache-friendly in practice" | `wiki`:140 | "many of the consecutive operations in a bitonic sort retain a locality of reference, making implementations more cache-friendly" (article says "many", not all) | +| 3 The Staircase | 7 | OK | | `report`:88; `wiki`:20, 99 | "m = 1..64, 3200 arrays: 0 mismatches" / "using sentinel values" (the sentinel result is for the JS reference, not for Handwired's circuit) | +| 3 The Staircase | 8 | OK | | `report`:75-77; `wiki`:109 | "== 6. article pseudocode with the inclusive loop bound (i <= low + k) ==" / "0-1 n=8: 8 of 256 inputs not sorted, first: [1, 1, 1, 1, 0, 0, 0, 0]" / "for i ← low to low + k do" | +| 3 The Staircase | 9 | OK | | `report`:11, 9 | "odd-even mergesort, n=64: comparators 543, closed form (k^2-k+4)*2^(k-2)-1 = 543" (672 bitonic at line 9) | +| 3 The Staircase | 10 | OK | | `report`:14 | "stage 0: (0,1 asc) (2,3 desc) (4,5 asc) (6,7 desc)" (4 comparators = n/2 per stage at n = 8) | +| 4 What Handwired Pays | 1 | OK | | `lab-readme`:5; `exec`:3, 7 | "one fixed numeric circuit of affine operations, ReLU-gated products, and exact bits" / "one `Float32Array`" / "## FP32" (32-bit comes from `exec`; the cited README line does not state it) | +| 4 What Handwired Pays | 2 | OK | | `exec`:40 | "Independent instructions at the same dependency depth share a layer." | +| 4 What Handwired Pays | 3 | OK | | `patterns`:358; `notes-out`:109 | "A sort reaches each item's place through comparisons that depend on earlier comparisons." / "so its structure does not depend on the data" | +| 4 What Handwired Pays | 4 | OK | | `loops`:185; `notes-rt`:66 | "A compare-exchange on keys alone is a minimum and a maximum, 4 operations in 3 layers" / "minimum and maximum of one pair \| shares the difference and relu through deduplication \| 4 / 3 / 7" (min = a − relu(a−b), max = b + relu(a−b); checked by hand) | +| 4 What Handwired Pays | 5 | OK | | `notes-out`:32, 113 | "Sort 64 items by key, carrying a payload \| bitonic network, keys only, `emitMinimum`/`emitMaximum` \| 2,752 \| 64 \| 4,768" / "a bitonic network on keys only takes 64 layers and 4,768 weights" (21 × 3 = 63 predicted, arithmetic) | +| 4 What Handwired Pays | 6 | OK | | `report`:9; `notes-out`:32 | "64 6 21 21 672 672 yes" (672 × 4 = 2,688 is arithmetic; no notes explain the gap, checked by grep) | +| 4 What Handwired Pays | 7 | OK | | `loops`:189; `notes-out`:31 | "16 items: a bitonic network was 1,248 operations and 50 layers, and ranks by counting with gathers were 1,056 operations and 5 layers." | +| 4 What Handwired Pays | 8 | FIX | "A rank counts how many items are smaller than this one, … That count is the sorted position" → "A rank counts the items ahead of this one, … Equal keys break ties by index, and that count is the sorted position." | `ranks`:16; `notes-out`:113; `notes-rt`:238 | "Items 0 and 2 have equal keys, and the earlier item comes first, like a stable sort." (a strict count gives tied items the same rank, so the tie-break is needed; the cited `notes-rt`:238 formula `rank_i = sum_j [x_j < x_i]` omits it) | +| 4 What Handwired Pays | 9 | OK | | `loops`:189, 192 | "So at 16 items counting was less work, at 64 items the network was, and counting stayed at a constant 5 layers." | +| 4 What Handwired Pays | 10 | OK | | `loops`:190, 192 | "64 items: 10,624 operations and 105 layers, against 16,512 operations and 5 layers." | +| 4 What Handwired Pays | 11 | OK | | `notes-out`:113; `ranks`:84 | "For the item counts UIs usually have, counting is the better formulation; n² comparisons start to hurt around hundreds of items." | +| 4 What Handwired Pays | 12 | OK | | `notes-rt`:237; `exec`:282 | "on fractional values 20-24% of outputs aren't bit-equal to any sorted input (errors up to 6e-5)" / "Fractional values can drift even at small magnitudes." (6e-5 = six in a hundred thousand) | +| 4 What Handwired Pays | 13 | OK | | `review-dec`:269; `notes-out`:113 | "Whether to package a sorting network for apps that sort well past 64 items, next to ranks by counting" | + +## Visuals in plan.md with a wrong number or unsafe term + +Plan text was not edited here (only the captions were in scope). The scene authors should fix these: + +1. **Chapter 3, on-screen labels (line 109):** "O(n log^2 n) comparators, O(log^2 n) delay". "delay" breaks the plan's own vocabulary rule (line 8) and article defect 10. Use "stages". +2. **Chapter 4, on-screen labels (line 145):** "rank formula `rank_i = sum_j [x_j < x_i]`". It has no tie-break, so tied keys get the same rank and the ranks are not a permutation. Use the tie-broken form, or state that keys are assumed distinct, as in `ranks`:16. +3. **Chapter 4, beat 6 and on-screen labels:** the counting demo must show ties break by index, since the caption now says so. Showing only the strict count would contradict caption 8. +4. **Chapter 3, beat 8:** "a ghost comparator reaches from the last wire of a block into the next block" is loose. The extra comparison runs from wire `low + k` (the last wire of the lower half of a sub-block) to wire `low + 2k`, the first wire of the next sub-block. The counter "8 of 256 fail" and the failing input [1, 1, 1, 1, 0, 0, 0, 0] are correct (`report`:77). + +All other numbers in plan.md checked out: +- Chapter 1 beats 3 to 8: the bars, the half-cleaner table, "2,083 / 415 / 0", and the recursion [1, 2, 3, 0] → [1, 0, 3, 2] → [0, 1, 2, 3] and [8, 5, 6, 7] → [6, 5, 8, 7] → [5, 6, 7, 8] (hand-run through the stage rule). +- Chapter 2 beats 1 to 10: the trace rows and stage colours match `report`:14-19 and 35-40. +- Chapter 3 beats 1 to 7 and 9 to 10: staircases, 1, 6, 24, 80, 240, 672, "about 2.8 times", 32 per step, 21 steps, 543 odd-even. +- Chapter 4 beats 4, 5, 7 to 9: 4 ops / 3 layers, example a = 7, b = 3 gives d = 4, relu = 4, min = 3, max = 7; 63 / 64 layers; 2,688 "predicted" (labelled as arithmetic); 1,248 / 50; 1,056 / 5; 10,624 / 105; 16,512 / 5; 20–24%, 6e-5. + +## Source-pointer notes (claims are right; the pointer is weak) + +- Chapter 4, caption 1: the cited `README.md:5` (root and lab) does not say "thirty-two-bit"; the FP32 support is in `exec`:3 and 7. +- Chapter 2, caption 9: the pointer `loops`:185 supports "fixed list", not "never decides". The stage-list check (`report`:14–19) and `bitonic.ts`:59 support the claim. +- Chapter 3, caption 9 and chapter 4, caption 4: the `report` and `loops` line numbers are right, but "odd-even" and "sixty-four layers" are from the notes, not the report. +- The `notes-rt`:238 formula is wrong for ties (see chapter 4, caption 8). The `ranks`:16 text is correct. This is a defect in the notes, not in the book's claim. diff --git a/public/generated/handwired-bitonic-sorter/audio/chapter-1.mp3 b/public/generated/handwired-bitonic-sorter/audio/chapter-1.mp3 new file mode 100644 index 00000000..65bfd8a4 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/audio/chapter-1.mp3 differ diff --git a/public/generated/handwired-bitonic-sorter/audio/chapter-2.mp3 b/public/generated/handwired-bitonic-sorter/audio/chapter-2.mp3 new file mode 100644 index 00000000..57d2b600 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/audio/chapter-2.mp3 differ diff --git a/public/generated/handwired-bitonic-sorter/audio/chapter-3.mp3 b/public/generated/handwired-bitonic-sorter/audio/chapter-3.mp3 new file mode 100644 index 00000000..8d437a70 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/audio/chapter-3.mp3 differ diff --git a/public/generated/handwired-bitonic-sorter/audio/chapter-4.mp3 b/public/generated/handwired-bitonic-sorter/audio/chapter-4.mp3 new file mode 100644 index 00000000..71c7cfc9 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/audio/chapter-4.mp3 differ diff --git a/public/generated/handwired-bitonic-sorter/blog.md b/public/generated/handwired-bitonic-sorter/blog.md new file mode 100644 index 00000000..12ad9bc2 --- /dev/null +++ b/public/generated/handwired-bitonic-sorter/blog.md @@ -0,0 +1,126 @@ +# Bitonic Sorter + +A bitonic sorter is Ken Batcher's fixed wiring of comparators, published in 1968 in *Sorting networks and their applications*. This post follows the Wikipedia article at revision 1373262602, and it checks the article against Handwired, Cheng Lou's repository for writing programs as one fixed circuit of weighted sums and ReLU. The repository is pinned at commit 4ea7fcb, and the measurements come from its exploration notes at d01b763. + +### A comparator is the only part + +A sorting network never decides. It is a fixed list of compare-and-swap steps, wired the same way for every input. The one part is the comparator: two numbers go in, the smaller comes out on the top wire, and the larger on the bottom. The bitonic construction works on a sequence that climbs and then falls, which the name describes. One, two, six, seven, eight, five, three, zero is such a mountain. + +{% viz scene="books/handwired-bitonic-sorter/chapter-1" section="chapter-1-comparator-and-mountain" cue="0" from="0.000" to="45.976" title="A comparator orders two wires, and a bitonic sequence climbs and then falls." %} +{% endviz %} + +Ken Batcher's bitonic construction is built from comparators like this one. For a sorter of two wires, the article says, it is simply a comparator. + +### The half-cleaner splits a mountain in two + +The half-cleaner pairs each wire in the first half with its partner in the second half. On eight wires, wire zero meets wire four, one meets five, two meets six, and three meets seven. On the example, the four comparators leave the low values one, two, three, zero on the left and the high values eight, five, six, seven on the right. + +{% viz scene="books/handwired-bitonic-sorter/chapter-1" section="chapter-1-half-cleaner-splits" cue="4" from="45.976" to="89.675" title="Four comparators fire at once and split the mountain into two halves." %} +{% endviz %} + +The border is clean. The largest value on the left is three and the smallest on the right is five, so nothing needs to cross. The article says all elements of one bitonic subsequence are guaranteed to be higher than all elements of the other, so the two halves can be sorted separately. + +### Split again until the line is sorted + +The right half, eight, five, six, seven, dips like a valley. Read from the five, it climbs, so it is a mountain in a wider sense. The article's definition allows only straight mountains, but the argument needs rotated ones too. Across all 2,083 eight-long mountains with values zero to three, our check found no split that failed. + +Halving each half again gives quarters and then pairs. On the example the pieces come out as zero, one, two, three, five, six, seven, eight. Three rounds at distances four, two and one do the job for eight numbers. + +{% viz scene="books/handwired-bitonic-sorter/chapter-1" section="chapter-1-split-again" cue="8" from="89.675" to="136.673" title="Recursive halving leaves a sorted line of eight values." %} +{% endviz %} + +That is the merging machine. Give it a mountain whose length is a power of two, and it hands back a sorted line. What it cannot do yet is make mountains. + +### Pairs are tiny mountains + +A shuffled list is not a mountain, so the network builds them from the bottom up. Any two numbers already form a bitonic pair, and one comparator sets them to climb or to fall. The article describes its recursive sorters as producing two monotonic sequences, one decreasing and one increasing, which feed the next stage. + +Take three, seven, four, eight, six, two, one, five. Stage zero compares neighbors, and the second and fourth pairs run downhill. The output is three, seven, eight, four, two, six, five, one. + +{% viz scene="books/handwired-bitonic-sorter/chapter-2" section="chapter-2-pairs-are-mountains" cue="0" from="0.000" to="42.075" title="Stage zero sorts pairs up and down, and the output is already a row of mountains." %} +{% endviz %} + +Sorted up beside sorted down is exactly a mountain, which is why the directions alternate. Two sorted runs become one bigger bitonic series for the next stage. + +### Merging the blocks, stage by stage + +Two blocks of four merge with half-cleaners at distance four, then two, then one. After the third stage the four smallest values sit on the left: three, four, two, one. Two more stages finish the job, and after the sixth stage the line reads one, two, three, four, five, six, seven, eight. + +Count the work. Six stages, each with four comparators on separate wires, make 24 comparators, and all four in a stage fire together, so the whole sort takes six steps in time. The distances go one; two, one; four, two, one. That is one plus two plus three, six stages in all. + +{% viz scene="books/handwired-bitonic-sorter/chapter-2" section="chapter-2-merging-blocks" cue="4" from="42.075" to="86.483" title="Three merges at distances four, two and one finish a sorted eight-wire line." %} +{% endviz %} + +### The wiring never looks at the data + +The comparators never choose where to look. The wiring depends only on the number of wires, and only the swaps depend on the values. That is what lets a fixed machine run the same sort on any input. + +The check is exhaustive where it can be. All 256 zero-one lists at eight wires came out sorted, and all 65,536 at sixteen did too. Random lists at every size from two to sixty-four wires matched the built-in sort. Thirty-two and sixty-four wires were sampled that way, not checked exhaustively. + +{% viz scene="books/handwired-bitonic-sorter/chapter-2" section="chapter-2-wiring-never-looks" cue="8" from="86.483" to="120.373" title="The same fixed comparators sort every input; only the swaps change." %} +{% endviz %} + +### Stages grow like k(k+1)/2 + +Let the list have two to the k wires. The last merge takes k stages, the merge before it k minus one, and so on down to one. Add them up and you get k times k plus one, over two. For two through sixty-four wires that is one, three, six, ten, fifteen and twenty-one stages, so each doubling adds only a few more. + +Every stage has half as many comparators as wires. The total is the number of wires times k times k plus one, over four: one, six, 24, 80, 240 and 672 comparators. + +{% viz scene="books/handwired-bitonic-sorter/chapter-3" section="chapter-3-stages-grow" cue="0" from="0.000" to="41.215" title="The stage count grows like k(k+1)/2 as the wires double." %} +{% endviz %} + +### Comparators nearly triple as the list doubles + +In the usual shorthand, comparators grow like n log squared n, and stages like log squared n. Going from 32 to 64 wires takes the stages from 15 to 21 and the comparators from 240 to 672, about 2.8 times as many. Each stage holds 32 comparators at 64 wires, and all of them can act in the same instant. + +That suits hardware with many units in lockstep. The article points to a typical GPU. It also says many consecutive operations in a bitonic sort keep locality of reference, which makes implementations more cache-friendly. That is the article's claim, and we did not measure it. + +{% viz scene="books/handwired-bitonic-sorter/chapter-3" section="chapter-3-comparators-triple" cue="3" from="41.215" to="81.839" title="Stages rise gently, but comparators climb steeply with the list length." %} +{% endviz %} + +### Lengths that are not powers of two + +The original design needs a list whose length is a power of two. For other lengths the article suggests sentinel values. Pad with a value bigger than everything, sort, and drop the extras. Our check ran that for every length from one to sixty-four. + +It is easy to get a stage wrong. Read literally, the article's loop uses an inclusive bound and runs one comparison too far, into the neighboring block. At eight wires that fails eight of the 256 zero-one lists. Batcher's odd-even merge sort needs fewer comparators, 543 at 64 wires against bitonic's 672, though the article says bitonic is typically higher. + +The staircase in one sentence: half as many comparators as wires per stage, k times k plus one over two stages, and wiring that never looks at the data. + +{% viz scene="books/handwired-bitonic-sorter/chapter-3" section="chapter-3-lengths-and-traps" cue="6" from="81.839" to="132.122" title="Padding fixes the length, but one loop bound breaks the sort." %} +{% endviz %} + +### A circuit of sums and ReLU + +Handwired writes a program as one fixed circuit of weighted sums and ReLU, over 32-bit floats. ReLU keeps a positive number and turns a negative one into zero. The circuit is cut into layers: operations that do not depend on each other share a layer, and each layer reads only earlier ones. So a program's cost has two numbers, the operations in all and the layers deep. + +{% viz scene="books/handwired-bitonic-sorter/chapter-4" section="chapter-4-circuit-of-sums" cue="0" from="0.000" to="41.053" title="A program is a fixed circuit of sums and ReLU, cut into layers." %} +{% endviz %} + +### A comparator is four operations in three layers + +A sort that branches on comparisons cannot be written that way. A sorting network can, because its comparators never depend on the data. A compare-exchange on keys alone is a minimum and a maximum: subtract the two keys, run the difference through ReLU, subtract that from the first key for the minimum, and add it to the second for the maximum. That is four operations in three layers. + +Twenty-one stages at three layers each predicts 63 layers for 64 keys. Handwired measured 64 layers and 2,752 operations for the bitonic network on keys only. The notes do not explain the small gap to the 2,688 that four times 672 would give, so read these as measurements, not a derivation. + +{% viz scene="books/handwired-bitonic-sorter/chapter-4" section="chapter-4-comparator-in-three-layers" cue="3" from="41.053" to="81.677" title="One comparator is four operations in three layers, and the measured network costs what the notes say." %} +{% endviz %} + +### Payloads, ranks, and the race + +Real lists carry a payload, like a photo that travels with its key. With one payload value per item, the bitonic network at 16 items measured 1,248 operations in 50 layers. The rival needs no network. A rank counts the items ahead of this one, comparing every pair. Equal keys break ties by index, and that count is the sorted position. A one-hot read then fetches the payload. + +At 16 items counting wins on work, 1,056 operations against 1,248, and in five layers against 50. At 64 items the network wins on work, 10,624 operations against 16,512, but it is 105 layers deep while counting stays at five. Pairs grow with the square of the item count. The notes still call counting the better formulation for the item counts UIs usually have, with trouble starting around hundreds of items. + +{% viz scene="books/handwired-bitonic-sorter/chapter-4" section="chapter-4-payloads-and-ranks" cue="6" from="81.677" to="144.673" title="Counting ranks wins at 16 items, and the network wins on work at 64." %} +{% endviz %} + +### Caveats and the open question + +One caveat: minimum and maximum built from arithmetic drift on fractional keys. On the odd-even network, 20 to 24 percent of outputs were not bit-equal to any sorted input, with errors up to six in a hundred thousand. Integer keys are exact. + +The verdict, from that historical record, is that at interface sizes counting ranks is the better choice. Whether to package a sorting network for apps that sort well past 64 items is still an open question. + +{% viz scene="books/handwired-bitonic-sorter/chapter-4" section="chapter-4-caveats-and-verdict" cue="11" from="144.673" to="171.085" title="Fractional keys drift, and at interface sizes counting ranks." %} +{% endviz %} + +The main sources are the Wikipedia article [Bitonic sorter](https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602) at revision 1373262602, and Batcher's paper [Sorting networks and their applications](https://doi.org/10.1145/1468075.1468121), which we cite through the article. The Handwired claims come from the repository at commit [4ea7fcb](https://github.com/chenglou/handwired/tree/4ea7fcb27ebe93acf49b9f29a93e162b975bad46), especially its [execution model](https://github.com/chenglou/handwired/blob/4ea7fcb27ebe93acf49b9f29a93e162b975bad46/docs/execution-model.md) and [patterns](https://github.com/chenglou/handwired/blob/4ea7fcb27ebe93acf49b9f29a93e162b975bad46/docs/patterns.md) docs, and from the exploration notes at commit d01b763. The sources page under the player lists each spoken claim with its file and lines. diff --git a/public/generated/handwired-bitonic-sorter/manifest.json b/public/generated/handwired-bitonic-sorter/manifest.json new file mode 100644 index 00000000..8d2a0b58 --- /dev/null +++ b/public/generated/handwired-bitonic-sorter/manifest.json @@ -0,0 +1,98 @@ +{ + "format": 3, + "slug": "handwired-bitonic-sorter", + "title": "Bitonic Sorter", + "subtitle": "Ken Batcher's fixed wiring of comparators, and what a circuit of sums and relu pays to run it.", + "animal": "animal.webp", + "accent": "#3e9b73", + "voiceSpeed": 0.94, + "chapters": [ + { + "number": 1, + "title": "Two Wires and a Fold", + "blurb": "A comparator, a mountain-shaped sequence, and the half-cleaner that splits a mountain into two independent halves.", + "scene": "books/handwired-bitonic-sorter/chapter-1", + "audio": "audio/chapter-1.mp3", + "cues": [ + 0, + 11.564, + 23.487, + 34.842, + 45.976, + 55.728, + 67.802, + 76.405, + 89.675, + 101.912, + 114.625, + 125.98 + ], + "duration": 136.673 + }, + { + "number": 2, + "title": "Building the Mountains", + "blurb": "Eight shuffled numbers become mountains of two, then four, then eight, and finally one sorted line in six stages.", + "scene": "books/handwired-bitonic-sorter/chapter-2", + "audio": "audio/chapter-2.mp3", + "cues": [ + 0, + 9.265, + 21.49, + 30.697, + 42.075, + 52.327, + 64.065, + 74.212, + 86.483, + 97.141, + 109.204 + ], + "duration": 120.373 + }, + { + "number": 3, + "title": "The Staircase", + "blurb": "How many stages and comparators a bitonic sorter needs as the list doubles, why a lockstep processor likes it, and two traps for implementers.", + "scene": "books/handwired-bitonic-sorter/chapter-3", + "audio": "audio/chapter-3.mp3", + "cues": [ + 0, + 11.784, + 25.913, + 41.215, + 55.229, + 68.673, + 81.839, + 93.391, + 107.254, + 118.121 + ], + "duration": 132.122 + }, + { + "number": 4, + "title": "What Handwired Pays", + "blurb": "A comparator as three layers of sums and relu, the measured price of a bitonic network, and why ranks by counting win at interface sizes.", + "scene": "books/handwired-bitonic-sorter/chapter-4", + "audio": "audio/chapter-4.mp3", + "cues": [ + 0, + 13.851, + 26.843, + 41.053, + 55.183, + 68.964, + 81.677, + 95.969, + 109.993, + 119.537, + 132.992, + 144.673, + 159.01 + ], + "duration": 171.085 + } + ], + "sources": "sources.json" +} \ No newline at end of file diff --git a/public/generated/handwired-bitonic-sorter/previews/chapter-1.png b/public/generated/handwired-bitonic-sorter/previews/chapter-1.png new file mode 100644 index 00000000..7e576510 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/previews/chapter-1.png differ diff --git a/public/generated/handwired-bitonic-sorter/previews/chapter-2.png b/public/generated/handwired-bitonic-sorter/previews/chapter-2.png new file mode 100644 index 00000000..17757aa9 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/previews/chapter-2.png differ diff --git a/public/generated/handwired-bitonic-sorter/previews/chapter-3.png b/public/generated/handwired-bitonic-sorter/previews/chapter-3.png new file mode 100644 index 00000000..3d995156 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/previews/chapter-3.png differ diff --git a/public/generated/handwired-bitonic-sorter/previews/chapter-4.png b/public/generated/handwired-bitonic-sorter/previews/chapter-4.png new file mode 100644 index 00000000..9dcaf5d5 Binary files /dev/null and b/public/generated/handwired-bitonic-sorter/previews/chapter-4.png differ diff --git a/public/generated/handwired-bitonic-sorter/sources.json b/public/generated/handwired-bitonic-sorter/sources.json new file mode 100644 index 00000000..6cf84f53 --- /dev/null +++ b/public/generated/handwired-bitonic-sorter/sources.json @@ -0,0 +1,563 @@ +{ + "version": 1, + "slug": "handwired-bitonic-sorter", + "chapters": [ + { + "number": 1, + "title": "Two Wires and a Fold", + "claims": [ + { + "id": "c1-network-is-data-blind", + "claim": "A sorting network is a fixed list of compare-and-swap steps, wired the same way for every input, so its structure does not depend on the data.", + "caption": 0, + "status": "paraphrase", + "sources": [ + { "label": "Handwired exploration notes, outside techniques, line 109", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "109", "quote": "so its structure does not depend on the data" }, + { "label": "Handwired guide, loops section, line 185", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/sections/loops.md", "lines": "185", "quote": "A sorting network is a fixed list of compare-exchange steps that sorts any input" } + ] + }, + { + "id": "c1-comparator-order", + "claim": "A comparator takes two numbers and puts the smaller on the top output and the larger on the bottom.", + "caption": 1, + "status": "paraphrase", + "sources": [ + { "label": "Handwired guide, loops section, line 185", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/sections/loops.md", "lines": "185", "quote": "where each compare-exchange puts the smaller of two values first" }, + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 26", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "X and Y represent the inputs, while H and L represent the higher and lower outputs, respectively." } + ] + }, + { + "id": "c1-batcher-1968", + "claim": "Ken Batcher devised the bitonic sorter and published it in 1968.", + "caption": 2, + "status": "verified", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "The algorithm was devised by" }, + { "label": "Batcher, Sorting networks and their applications (AFIPS 1968), cited in the article", "url": "https://doi.org/10.1145/1468075.1468121", "quote": "Proceedings of the April 30--May 2, 1968" } + ] + }, + { + "id": "c1-eight-wires-24-comparators", + "claim": "By the article's level count, eight wires take six levels of four comparators, which means 24 comparators in all.", + "caption": 2, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" }, + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "The resulting sorting networks consist of" } + ] + }, + { + "id": "c1-bitonic-shape", + "claim": "A bitonic sequence rises to a peak and then falls, so the example one, two, six, seven, eight, five, three, zero is one.", + "caption": 3, + "status": "verified", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 16", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "A sequence is bitonic when it consists of a non-decreasing sequence followed by a non-increasing sequence" } + ] + }, + { + "id": "c1-half-cleaner-pairs", + "claim": "The half-cleaner compares each element of the first half with the corresponding element of the second half, so wire 0 pairs with wire 4 when there are eight wires.", + "caption": 4, + "status": "verified", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, lines 41-42", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "elements of the first half of the input sequence are pairwise compared against corresponding elements of the last" } + ] + }, + { + "id": "c1-four-comparators-split", + "claim": "By the article's half-cleaner, the four comparators on 1, 2, 6, 7, 8, 5, 3, 0 leave the low values 1, 2, 3, 0 on the left and the high values 8, 5, 6, 7 on the right.", + "caption": 5, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, lines 41-42", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "elements of the first half of the input sequence are pairwise compared against corresponding elements of the last" } + ] + }, + { + "id": "c1-halves-never-cross", + "claim": "The article says every element of one bitonic subsequence is guaranteed to be higher than every element of the other, so the two halves can be sorted separately.", + "caption": 6, + "status": "verified", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, lines 45-46", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "all elements in the red sequence are guaranteed to be higher than all elements in the blue series." } + ] + }, + { + "id": "c1-border-values", + "claim": "On this input the largest low value is three and the smallest high value is five, which means no value needs to cross the border.", + "caption": 6, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, lines 45-46", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "all elements in the red sequence are guaranteed to be higher than all elements in the blue series." } + ] + }, + { + "id": "c1-rotated-mountain", + "claim": "Read cyclically, the high half 8, 5, 6, 7 counts as a mountain, which means the split argument also needs rotated mountains.", + "caption": 7, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 16", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "A sequence is bitonic when it consists of a non-decreasing sequence followed by a non-increasing sequence" } + ] + }, + { + "id": "c1-straight-mountains-only", + "claim": "The article's definition allows only straight mountains: one non-decreasing run followed by one non-increasing run.", + "caption": 8, + "status": "verified", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 16", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "A sequence is bitonic when it consists of a non-decreasing sequence followed by a non-increasing sequence" } + ] + }, + { + "id": "c1-2083-no-failures", + "claim": "By the article's definition and the split above, checking all 2,083 eight-long mountains with values zero to three shows the split never failed.", + "caption": 8, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 16", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "A sequence is bitonic when it consists of a non-decreasing sequence followed by a non-increasing sequence" }, + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, lines 45-46", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "all elements in the red sequence are guaranteed to be higher than all elements in the blue series." } + ] + }, + { + "id": "c1-recursion-quarters-pairs", + "claim": "By the article's recursion, halving each half again gives quarters and then pairs, which means the pieces of this input end as zero, one, two, three, five, six, seven, eight.", + "caption": 9, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 37", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "This structure may be extended recursively for higher values of" } + ] + }, + { + "id": "c1-two-wires-comparator", + "claim": "A bitonic sorter for two wires is simply a comparator.", + "caption": 10, + "status": "verified", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 26", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "is simply a comparator." } + ] + }, + { + "id": "c1-three-rounds-distances", + "claim": "By the article's level count, three rounds at distances four, two and one sort eight numbers, which means six levels in all.", + "caption": 10, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" } + ] + }, + { + "id": "c1-merge-power-of-two", + "claim": "By the article's construction, the merge sorts a bitonic input whose length is a power of two, which means the merging machine works on any such mountain.", + "caption": 11, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 20", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "is a perfect power of two" } + ] + } + ] + }, + { + "number": 2, + "title": "Building the Mountains", + "claims": [ + { + "id": "c2-pairs-build-up", + "claim": "By the article's construction, sorters on small blocks are recursively used to create two monotonic sequences, which means two numbers already form a bitonic pair and one comparator sets it to climb or fall.", + "caption": 0, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 81", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "bitonic sorters are recursively used to create two monotonic sequences" } + ] + }, + { + "id": "c2-stage-zero-alternates", + "claim": "By the article's construction, the recursive sorters alternate increasing and decreasing blocks, which means stage zero on the example produces 3, 7, 8, 4, 2, 6, 5, 1.", + "caption": 1, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 81", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "one decreasing and one increasing" } + ] + }, + { + "id": "c2-merge-blocks-of-four", + "claim": "By the article's construction, the sorted blocks feed a bitonic input to the next merge, which means two blocks of four are merged with half-cleaners at distances two and one.", + "caption": 2, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 90", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "increasingly sorted sequence in order to create a bitonic input for the bitonic sorter." } + ] + }, + { + "id": "c2-up-beside-down", + "claim": "Two sorted runs, one up and one down, form a bitonic series for the next stage.", + "caption": 3, + "status": "paraphrase", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 82", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "a bitonic series for the next stage" } + ] + }, + { + "id": "c2-stage-three-half-cleaner", + "claim": "By the article's construction, the stage at distance four pairs each element of the first half with the corresponding element of the second half, which means the four smallest values land on the left.", + "caption": 4, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, lines 41-42", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "elements of the first half of the input sequence are pairwise compared against corresponding elements of the last" } + ] + }, + { + "id": "c2-last-two-stages", + "claim": "By the article's level count, the last two stages run at distances two and one, which means six stages in all for eight numbers.", + "caption": 5, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" } + ] + }, + { + "id": "c2-six-stages-24-comparators", + "claim": "Six stages of four comparators make 24 comparators, and each stage's comparators act on separate wires, so they run together in six steps in time.", + "caption": 6, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "The resulting sorting networks consist of" } + ] + }, + { + "id": "c2-distances-sum", + "claim": "The merges at distances one, then two and one, then four, two and one add up to six stages, which means one plus two plus three.", + "caption": 7, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" } + ] + }, + { + "id": "c2-wiring-fixed", + "claim": "The comparators never choose where to look: a sorting network's structure does not depend on the data, only the swaps do.", + "caption": 8, + "status": "verified", + "sources": [ + { "label": "Handwired exploration notes, outside techniques, line 109", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "109", "quote": "so its structure does not depend on the data" } + ] + }, + { + "id": "c2-zero-one-counts", + "claim": "By our zero-one count, which checked 256 inputs at eight wires and 65,536 at sixteen, all of them came out sorted, which means both sizes pass that check.", + "caption": 9, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 18", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "Bitonic sorters can be used to build a bitonic sort network that can sort arbitrary sequences" } + ] + }, + { + "id": "c2-random-check", + "claim": "By our random check against the built-in sort, lists from 2 to 64 wires matched, which means 32 and 64 wires were sampled rather than checked exhaustively.", + "caption": 10, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 18", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "Bitonic sorters can be used to build a bitonic sort network that can sort arbitrary sequences" } + ] + } + ] + }, + { + "number": 3, + "title": "The Staircase", + "claims": [ + { + "id": "c3-levels-formula", + "claim": "The article gives the number of levels as one half of k times k plus one, with n equal to two to the k.", + "caption": 0, + "status": "paraphrase", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 136", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "This sum can be reduced using the" }, + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" } + ] + }, + { + "id": "c3-stage-counts", + "claim": "By that formula, two to sixty-four wires need one, three, six, ten, fifteen and twenty-one stages.", + "caption": 1, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" } + ] + }, + { + "id": "c3-comparator-counts", + "claim": "Half as many comparators as wires in each stage, times the stage count, gives one, six, twenty-four, eighty, two hundred forty and six hundred seventy-two comparators.", + "caption": 2, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "The resulting sorting networks consist of" } + ] + }, + { + "id": "c3-shorthand-orders", + "claim": "The article gives the comparator count as order n times log squared n, and the delay as order log squared n.", + "caption": 3, + "status": "paraphrase", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "comparators and have a delay of" } + ] + }, + { + "id": "c3-32-to-64-growth", + "claim": "By the stage and comparator formulas, going from 32 to 64 wires takes stages from 15 to 21 and comparators from 240 to 672, which means about 2.8 times as many comparators.", + "caption": 3, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" } + ] + }, + { + "id": "c3-gpu-lockstep", + "claim": "The article says the construction suits machines with many parallel execution units running in lockstep, such as a typical GPU.", + "caption": 4, + "status": "paraphrase", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "a large number of parallel execution units running in" } + ] + }, + { + "id": "c3-per-stage-32", + "claim": "By the formulas, 672 comparators over 21 stages is 32 comparators per stage at sixty-four wires.", + "caption": 4, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "The resulting sorting networks consist of" } + ] + }, + { + "id": "c3-locality", + "claim": "The article says many consecutive operations in a bitonic sort retain a locality of reference, which makes implementations more cache-friendly.", + "caption": 5, + "status": "paraphrase", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 140", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "many of the consecutive operations in a bitonic sort retain a" } + ] + }, + { + "id": "c3-power-of-two-design", + "claim": "The original design needs a sorter whose input length is a power of two.", + "caption": 6, + "status": "verified", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 99", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "This overall scheme requires the sorter to have an input of sequence that is a power of two" } + ] + }, + { + "id": "c3-sentinel-padding", + "claim": "The article suggests sentinel values for other lengths, and padding with a sentinel bigger than everything then dropping the extras worked for every length from one to sixty-four in our check.", + "caption": 6, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 99", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "using sentinel values" } + ] + }, + { + "id": "c3-loop-bound", + "claim": "The article's loop, read literally with its inclusive bound, runs one comparison too far, and by our count that fails 8 of the 256 zero-one lists at eight wires.", + "caption": 7, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 109", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "'''for''' i ← ''low'' '''to''' ''low'' + k '''do'''" } + ] + }, + { + "id": "c3-odd-even-543", + "claim": "Batcher's odd-even merge sort needs fewer comparators than bitonic: by our count 543 at sixty-four wires, against 672.", + "caption": 8, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 140", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "Although the absolute number of comparisons is typically higher than Batcher's" } + ] + } + ] + }, + { + "number": 4, + "title": "What Handwired Pays", + "claims": [ + { + "id": "c4-fixed-circuit-fp32", + "claim": "Handwired is a fixed list of arithmetic operations over 32-bit floats, mostly weighted sums and products with a ReLU.", + "caption": 0, + "status": "verified", + "sources": [ + { "label": "Handwired docs, README, line 3", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "docs/README.md", "lines": "3", "quote": "a fixed list of arithmetic operations over 32-bit floats" } + ] + }, + { + "id": "c4-relu", + "claim": "ReLU keeps a positive number and turns a negative one into zero.", + "caption": 0, + "status": "verified", + "sources": [ + { "label": "Handwired README, line 18", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "README.md", "lines": "18", "quote": "Math.max(0, x) is ReLU" } + ] + }, + { + "id": "c4-no-loops-no-branches", + "claim": "Handwired rewrites if-else and loops as matrix multiplication and ReGLU over floats, so the program itself has no loops and no branches.", + "caption": 0, + "status": "paraphrase", + "sources": [ + { "label": "Handwired README, line 7", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "README.md", "lines": "7", "quote": "Handwired transforms them into matrix multiplication + reglu over floats." } + ] + }, + { + "id": "c4-layers", + "claim": "Operations that do not depend on each other share a layer, and each layer reads only earlier ones.", + "caption": 1, + "status": "verified", + "sources": [ + { "label": "Handwired execution model, line 40", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "docs/execution-model.md", "lines": "40", "quote": "Independent instructions at the same dependency depth share a layer." } + ] + }, + { + "id": "c4-branches-vs-network", + "claim": "A sort reaches each item's place through comparisons that depend on earlier comparisons, while a sorting network's structure does not depend on the data.", + "caption": 2, + "status": "verified", + "sources": [ + { "label": "Handwired patterns, line 358", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "docs/patterns.md", "lines": "358", "quote": "A sort reaches each item's place through comparisons that depend on earlier comparisons" }, + { "label": "Handwired exploration notes, outside techniques, line 109", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "109", "quote": "so its structure does not depend on the data" } + ] + }, + { + "id": "c4-compare-exchange-cost", + "claim": "A compare-exchange on keys alone is a minimum and a maximum, four operations in three layers.", + "caption": 3, + "status": "verified", + "sources": [ + { "label": "Handwired guide, loops section, line 185", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/sections/loops.md", "lines": "185", "quote": "A compare-exchange on keys alone is a minimum and a maximum, 4 operations in 3 layers" }, + { "label": "Handwired exploration notes, runtime, line 66", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/runtime.md", "lines": "66", "quote": "shares the difference and relu through deduplication" } + ] + }, + { + "id": "c4-min-max-identity", + "claim": "By the identity minimum of a and b equals a minus relu of a minus b, the minimum takes a subtract, a relu, and a subtract, and the maximum adds the relu to b.", + "caption": 3, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 26", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "higher and lower outputs, respectively." } + ] + }, + { + "id": "c4-twenty-one-stages-63-layers", + "claim": "By the article's level count, twenty-one stages at three layers each predicts 63 layers for sixty-four keys.", + "caption": 4, + "status": "inferred", + "sources": [ + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 138", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "the number of levels in which each comparison can be done in parallel is given by" } + ] + }, + { + "id": "c4-measured-64-keys", + "claim": "Handwired measured the bitonic network, keys only, at 64 layers and 2,752 operations.", + "caption": 4, + "status": "verified", + "sources": [ + { "label": "Handwired exploration notes, outside techniques, line 32", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "32", "quote": "bitonic network, keys only, `emitMinimum`/`emitMaximum` | 2,752 | 64" } + ] + }, + { + "id": "c4-predicted-2688-gap", + "claim": "Four operations times 672 comparators would be 2,688, which means a gap of 64 against the measured 2,752 that the notes do not explain.", + "caption": 5, + "status": "inferred", + "sources": [ + { "label": "Handwired exploration notes, outside techniques, line 32", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "32", "quote": "bitonic network, keys only, `emitMinimum`/`emitMaximum` | 2,752 | 64" }, + { "label": "Wikipedia: Bitonic sorter, rev 1373262602, line 14", "url": "https://en.wikipedia.org/w/index.php?title=Bitonic_sorter&oldid=1373262602", "quote": "The resulting sorting networks consist of" } + ] + }, + { + "id": "c4-payload-16-bitonic", + "claim": "With one payload value per item, the bitonic network at sixteen items was 1,248 operations in 50 layers.", + "caption": 6, + "status": "verified", + "sources": [ + { "label": "Handwired guide, loops section, line 189", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/sections/loops.md", "lines": "189", "quote": "16 items: a bitonic network was 1,248 operations and 50 layers" } + ] + }, + { + "id": "c4-rank-ties", + "claim": "A rank counts the items ahead of this one, and equal keys break ties by index, so the count is the sorted position.", + "caption": 7, + "status": "verified", + "sources": [ + { "label": "Handwired guide, ranks instead of sorting, line 16", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/patterns/ranks-instead-of-sorting.md", "lines": "16", "quote": "Items 0 and 2 have equal keys, and the earlier item comes first, like a stable sort." } + ] + }, + { + "id": "c4-one-hot-payload", + "claim": "For each rank, a one-hot group of bits selects the item with that rank, and the sorted payload is a masked sum that lowers to a gather.", + "caption": 7, + "status": "verified", + "sources": [ + { "label": "Handwired exploration notes, outside techniques, line 113", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "113", "quote": "form a declared one-hot group" } + ] + }, + { + "id": "c4-sixteen-counting-wins", + "claim": "At sixteen items, counting costs 1,056 operations in five layers, against 1,248 operations in fifty layers for the bitonic network.", + "caption": 8, + "status": "verified", + "sources": [ + { "label": "Handwired guide, loops section, line 189", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/sections/loops.md", "lines": "189", "quote": "ranks by counting with gathers were 1,056 operations and 5 layers" } + ] + }, + { + "id": "c4-sixty-four-network-wins", + "claim": "At 64 items the network costs 10,624 operations in 105 layers, against 16,512 operations and 5 layers for counting.", + "caption": 9, + "status": "verified", + "sources": [ + { "label": "Handwired guide, loops section, line 190", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/sections/loops.md", "lines": "190", "quote": "64 items: 10,624 operations and 105 layers, against 16,512 operations and 5 layers." } + ] + }, + { + "id": "c4-pairs-grow-square", + "claim": "Counting's pairwise comparisons grow with the square of the item count, and the notes still call counting the better formulation for the item counts UIs usually have.", + "caption": 10, + "status": "verified", + "sources": [ + { "label": "Handwired exploration notes, outside techniques, line 113", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "113", "quote": "For the item counts UIs usually have, counting is the better formulation" }, + { "label": "Handwired exploration notes, outside techniques, line 113 (cost)", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "113", "quote": "n² comparisons start to hurt around hundreds of items" } + ] + }, + { + "id": "c4-fractional-drift", + "claim": "On fractional keys the Batcher odd-even network drifted: 20 to 24 percent of its outputs were not bit-equal to any sorted input, with errors up to 6e-5.", + "caption": 11, + "status": "paraphrase", + "sources": [ + { "label": "Handwired exploration notes, runtime, line 237", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/runtime.md", "lines": "237", "quote": "on fractional values 20-24% of outputs aren't bit-equal to any sorted input (errors up to 6e-5)" }, + { "label": "Handwired exploration notes, runtime, line 237 (network)", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/runtime.md", "lines": "237", "quote": "Batcher odd-even merge network of min/max compare-exchanges" } + ] + }, + { + "id": "c4-verdict-open", + "claim": "Whether to package a sorting network for apps that sort well past 64 items is still an open question, next to ranks by counting.", + "caption": 12, + "status": "verified", + "sources": [ + { "label": "Handwired guide, review and decisions, line 269", "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", "path": "lab/ui-exploration/guide/review-and-decisions.md", "lines": "269", "quote": "Whether to package a sorting network for apps that sort well past 64 items" } + ] + }, + { + "id": "c4-count-ranks-at-ui-sizes", + "claim": "For the item counts UIs usually have, the notes say counting is the better formulation, so counting ranks at interface sizes.", + "caption": 12, + "status": "verified", + "sources": [ + { "label": "Handwired exploration notes, outside techniques, line 113", "repo": "chenglou/handwired@d01b763a4f0cee3a6047f66e007291a35c98193d", "path": "lab/ui-exploration/notes/outside-techniques.md", "lines": "113", "quote": "For the item counts UIs usually have, counting is the better formulation" } + ] + } + ] + } + ] +} diff --git a/public/generated/handwired-cascade-argmax/previews/chapter-1.png b/public/generated/handwired-cascade-argmax/previews/chapter-1.png index 410c4ca3..33b884c1 100644 Binary files a/public/generated/handwired-cascade-argmax/previews/chapter-1.png and b/public/generated/handwired-cascade-argmax/previews/chapter-1.png differ diff --git a/public/generated/handwired-cascade-argmax/previews/chapter-2.png b/public/generated/handwired-cascade-argmax/previews/chapter-2.png index 9150a7ea..32a51c97 100644 Binary files a/public/generated/handwired-cascade-argmax/previews/chapter-2.png and b/public/generated/handwired-cascade-argmax/previews/chapter-2.png differ diff --git a/public/generated/handwired-cascade-argmax/previews/chapter-3.png b/public/generated/handwired-cascade-argmax/previews/chapter-3.png index c19a4832..4bc150cd 100644 Binary files a/public/generated/handwired-cascade-argmax/previews/chapter-3.png and b/public/generated/handwired-cascade-argmax/previews/chapter-3.png differ diff --git a/public/generated/handwired-generative-fields/previews/chapter-1.png b/public/generated/handwired-generative-fields/previews/chapter-1.png index 9973e67d..1749b3df 100644 Binary files a/public/generated/handwired-generative-fields/previews/chapter-1.png and b/public/generated/handwired-generative-fields/previews/chapter-1.png differ diff --git a/public/generated/handwired-generative-fields/previews/chapter-3.png b/public/generated/handwired-generative-fields/previews/chapter-3.png index 3b7ab437..7eb6b219 100644 Binary files a/public/generated/handwired-generative-fields/previews/chapter-3.png and b/public/generated/handwired-generative-fields/previews/chapter-3.png differ diff --git a/public/generated/handwired-learned-interpreter/previews/chapter-1.png b/public/generated/handwired-learned-interpreter/previews/chapter-1.png index f5cf3301..111ada06 100644 Binary files a/public/generated/handwired-learned-interpreter/previews/chapter-1.png and b/public/generated/handwired-learned-interpreter/previews/chapter-1.png differ diff --git a/public/generated/handwired-learned-interpreter/previews/chapter-3.png b/public/generated/handwired-learned-interpreter/previews/chapter-3.png index 2e1fe625..f18196b7 100644 Binary files a/public/generated/handwired-learned-interpreter/previews/chapter-3.png and b/public/generated/handwired-learned-interpreter/previews/chapter-3.png differ diff --git a/public/generated/handwired-reusable-layout/previews/chapter-3.png b/public/generated/handwired-reusable-layout/previews/chapter-3.png index a30503b4..021264b9 100644 Binary files a/public/generated/handwired-reusable-layout/previews/chapter-3.png and b/public/generated/handwired-reusable-layout/previews/chapter-3.png differ diff --git a/public/generated/handwired-trace-compiler/previews/chapter-1.png b/public/generated/handwired-trace-compiler/previews/chapter-1.png index 82a5ecc2..78f2558f 100644 Binary files a/public/generated/handwired-trace-compiler/previews/chapter-1.png and b/public/generated/handwired-trace-compiler/previews/chapter-1.png differ diff --git a/public/generated/handwired-trace-compiler/previews/chapter-3.png b/public/generated/handwired-trace-compiler/previews/chapter-3.png index fcd73a1a..b64d9301 100644 Binary files a/public/generated/handwired-trace-compiler/previews/chapter-3.png and b/public/generated/handwired-trace-compiler/previews/chapter-3.png differ diff --git a/public/generated/handwired-trace-compiler/previews/chapter-4.png b/public/generated/handwired-trace-compiler/previews/chapter-4.png index e62998bd..a8db235a 100644 Binary files a/public/generated/handwired-trace-compiler/previews/chapter-4.png and b/public/generated/handwired-trace-compiler/previews/chapter-4.png differ diff --git a/public/generated/handwired-ui-exploration/previews/chapter-2.png b/public/generated/handwired-ui-exploration/previews/chapter-2.png index cc2f2816..fb02a4a8 100644 Binary files a/public/generated/handwired-ui-exploration/previews/chapter-2.png and b/public/generated/handwired-ui-exploration/previews/chapter-2.png differ diff --git a/public/generated/handwired-ui-exploration/previews/chapter-3.png b/public/generated/handwired-ui-exploration/previews/chapter-3.png index 42196f38..0ec1b8e7 100644 Binary files a/public/generated/handwired-ui-exploration/previews/chapter-3.png and b/public/generated/handwired-ui-exploration/previews/chapter-3.png differ diff --git a/public/generated/handwired-user-effort/previews/chapter-1.png b/public/generated/handwired-user-effort/previews/chapter-1.png index db359439..edc8b7ca 100644 Binary files a/public/generated/handwired-user-effort/previews/chapter-1.png and b/public/generated/handwired-user-effort/previews/chapter-1.png differ diff --git a/public/generated/handwired-user-effort/previews/chapter-2.png b/public/generated/handwired-user-effort/previews/chapter-2.png index c71b74c1..152acb70 100644 Binary files a/public/generated/handwired-user-effort/previews/chapter-2.png and b/public/generated/handwired-user-effort/previews/chapter-2.png differ diff --git a/public/generated/library.json b/public/generated/library.json index 10e7cf77..98df9085 100644 --- a/public/generated/library.json +++ b/public/generated/library.json @@ -1,5 +1,39 @@ { "books": [ + { + "slug": "handwired-bitonic-sorter", + "title": "Bitonic Sorter", + "subtitle": "Ken Batcher's fixed wiring of comparators, and what a circuit of sums and relu pays to run it.", + "author": "Brett Lamy", + "role": "Software Engineer at Replay", + "color": "#3e9b73", + "href": "?bundle=handwired-bitonic-sorter", + "series": "Handwired", + "seriesOrder": 11, + "chapters": [ + { + "number": 1, + "title": "Two Wires and a Fold", + "duration": 136.673 + }, + { + "number": 2, + "title": "Building the Mountains", + "duration": 120.373 + }, + { + "number": 3, + "title": "The Staircase", + "duration": 132.122 + }, + { + "number": 4, + "title": "What Handwired Pays", + "duration": 171.085 + } + ], + "createdAt": "2026-10-21T20:32:29.736Z" + }, { "slug": "handwired-user-effort", "title": "User Effort", @@ -9,7 +43,7 @@ "color": "#8a9a5b", "href": "?bundle=handwired-user-effort", "series": "Handwired", - "seriesOrder": 13, + "seriesOrder": 14, "chapters": [ { "number": 1, @@ -38,7 +72,7 @@ "color": "#b23a48", "href": "?bundle=handwired-json-search", "series": "Handwired", - "seriesOrder": 16, + "seriesOrder": 17, "chapters": [ { "number": 1, @@ -72,7 +106,7 @@ "color": "#e0457b", "href": "?bundle=handwired-runtime-cost", "series": "Handwired", - "seriesOrder": 14, + "seriesOrder": 15, "chapters": [ { "number": 1, @@ -135,7 +169,7 @@ "color": "#d6b82e", "href": "?bundle=handwired-reusable-layout", "series": "Handwired", - "seriesOrder": 11, + "seriesOrder": 12, "chapters": [ { "number": 1, @@ -193,7 +227,7 @@ "color": "#5b8bd6", "href": "?bundle=handwired-knuth-plass", "series": "Handwired", - "seriesOrder": 15, + "seriesOrder": 16, "chapters": [ { "number": 1, @@ -222,7 +256,7 @@ "color": "#4f7cc8", "href": "?bundle=handwired-learned-gallery", "series": "Handwired", - "seriesOrder": 12, + "seriesOrder": 13, "chapters": [ { "number": 1,