diff --git a/.github/workflows/silicon.yml b/.github/workflows/silicon.yml new file mode 100644 index 0000000..deabd0a --- /dev/null +++ b/.github/workflows/silicon.yml @@ -0,0 +1,44 @@ +name: Silicon and brain +on: [pull_request] +permissions: + contents: read +jobs: + sources-and-models: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "22" + - run: node tests/hardware-catalog.mjs + - run: node tests/architecture.mjs + - run: node tests/timescales.mjs + - run: python3 tools/check_content.py + - run: cargo test -p doke-core --locked + browser-and-wasm: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "22" + - run: rustup target add wasm32-unknown-unknown + - run: cargo install wasm-bindgen-cli --version 0.2.100 --locked + - run: cargo build -p doke-web --target wasm32-unknown-unknown --release --locked + - run: wasm-bindgen target/wasm32-unknown-unknown/release/doke_web.wasm --target web --out-dir public/pkg --out-name doke + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: node tests/silicon-wasm.mjs + - name: Exercise rendered dataflow and controls + run: | + python3 -m http.server 4181 --bind 127.0.0.1 --directory public > /tmp/doke-preview.log 2>&1 & + DOKE_SERVER_PID=$! + trap 'kill "$DOKE_SERVER_PID"' EXIT + DOKE_SKIP_BRAIN=1 node tests/focused-browser.mjs + DOKE_SKIP_BRAIN=1 node tests/hardware-expansion.mjs + node tests/interaction-review.mjs diff --git a/README.md b/README.md index fa8591a..e5975b5 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,35 @@ # doke -A floating, interactive brain explorer built with Rust, WebAssembly and wgpu. Explore reference anatomy, an enlarged neural tissue window, chemical synapses, a neural circuit and molecular messengers. Mostly grayscale, with restrained color accents and adaptive HDR. +Look inside CPUs, GPUs, a tensor processor, a network ASIC and the human brain. A floating 3D explorer with source-linked architecture, close-up execution, and an input you can change. -![A short tour of doke: brain anatomy, activity playback, enlarged neurons, synaptic signaling, neural circuits, molecular messengers and scientific source tabs.](docs/media/doke-demo.gif) +Start with the whole processor, open a core or streaming multiprocessor, then follow one value through actual operand reads and arithmetic. Choose Ryzen 9 9950X, RTX 5090, H100 SXM, Google TPU v1 or Cisco Silicon One G200 from one hardware selector. These are original functional schematics, not physical die floorplans. -*A 24-second tour of the real app, captured with SDR glow. [Recording instructions](docs/GUIDE.md#recording-a-demo).* +The Rust models compute editable matrix transforms or route packets through queues. Inspect returned operands, cache residency, requested GPU sectors and each completed product. Model ticks are authored teaching time, not vendor cycles or chip benchmarks. The human reference atlas remains available on its own or alongside the chip, with sourced anatomy and a visual-processing guide. A shared logarithmic time scale compares documented chip clock periods with human physiological measurements; their conditions and limits are one click away. + +![Inside an RTX Blackwell streaming multiprocessor](docs/media/compute-explorer.webp) ## Run locally -Follow the [first-time setup](docs/GUIDE.md#building-from-a-fresh-clone) to install the tools and generate the anatomy assets. Then: +See the [setup guide](docs/GUIDE.md#build-and-run) for first-time dependencies and anatomy generation. After setup: ```sh bash tools/build.sh python3 tools/serve.py ``` -Open [localhost:4173](http://localhost:4173). Rendering and simulation run in the browser; the server serves static files. - -The [doke guide](docs/GUIDE.md) covers controls, scientific interpretation, builds, verification and hosting. It is the single maintained project guide. - -## What is calculated - -Brain geometry derives from the Allen Human Reference Atlas; molecule coordinates come from PubChem. Activity highlights, enlarged neurons, synaptic contacts and traveling particles illustrate supported concepts. They are not reconstructed individual cells or measured firing/chemical concentrations. The simplified neural circuit integrates population dynamics in Rust. Source records and scientific limitations are available in the interface. - -## Source map +Open [localhost:4173](http://localhost:4173). Everything runs in the browser; hosting only serves static files. -| Directory | Purpose | -| ------------------ | ---------------------------------------------------- | -| `crates/doke-core` | Activity envelopes and numerical E/I model | -| `crates/doke-web` | Rust rendering, camera, procedural cells, shaders | -| `public` | Floating interface, source records, runtime assets | -| `tools` | Asset generation, validation, builds and deployment | -| `tests` | Browser, playback, HDR, fallbacks and hosting checks | -| `docs/GUIDE.md` | Usage, development and publishing guide | +- [Guide](docs/GUIDE.md): controls, builds, verification and hosting. +- [Architecture and model scope](docs/SILICON.md): sources, what is calculated, and next steps. -## Repository contents and attribution +## Repository -Generated Wasm, anatomy binaries, downloaded source data, packaged builds, test screenshots, historical working notes and private deployment settings are excluded from Git. Keep the required fallback image, local fonts, source metadata and third-party notices: they are part of the application. +| Path | Purpose | +| ------------------ | ----------------------------------------------------------------- | +| `crates/doke-core` | Deterministic execution models and bounded snapshots | +| `crates/doke-web` | Rust/Wasm rendering, geometry, camera and HDR | +| `public` | Interface, source records, anatomy metadata and runtime assets | +| `tools` | Reproducible anatomy generation, validation, builds and packaging | +| `tests` | Source, geometry, execution, browser and static-hosting checks | -Atlas attribution is in [the asset notice](public/assets/NOTICE.md), font licenses in `public/fonts/`, and Rust dependency notices in `public/licenses/`. A license for doke's original source code has not been selected; third-party licenses apply to their respective material. +Generated Wasm, anatomy binaries, downloads, packaged builds, raw screenshots and local deployment settings are ignored. Retain the [atlas notice](public/assets/NOTICE.md), font licenses and dependency notices when distributing the app. A license for doke's original source has not yet been selected; third-party materials retain their respective licenses. diff --git a/crates/doke-core/src/fabric.rs b/crates/doke-core/src/fabric.rs new file mode 100644 index 0000000..e86c61e --- /dev/null +++ b/crates/doke-core/src/fabric.rs @@ -0,0 +1,752 @@ +//! A bounded, deterministic input-queued switch for explanation, not vendor emulation. +//! +//! Fixed-size packets, a shared 32-slot capacity, three inputs/outputs, one-tick +//! transfers and rotating arbitration are authored educational assumptions. +//! VOQ is represented by selecting each destination's first packet from a +//! canonical arrival-ordered ingress queue. No commercial ASIC timings, SDK, +//! buffer capacities or proprietary arbitration behavior are reproduced here. +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +pub const PORTS: usize = 3; +pub const BUFFER_CAPACITY: usize = 32; +pub const MAX_BURST: u32 = 64; +pub const MAX_ADVANCE: u32 = 4096; +pub const EVENT_CAPACITY: usize = 128; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum QueueMode { + Fifo, + #[default] + Voq, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct FabricConfig { + pub queue_mode: QueueMode, +} + +#[derive(Clone, Debug)] +struct Packet { + id: u64, + ingress: usize, + egress: usize, + born_tick: u64, +} + +#[derive(Clone, Debug)] +struct Transfer { + packet: Packet, + grant_tick: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct FabricPacket { + pub id: u64, + pub ingress: usize, + pub egress: usize, + pub born_tick: u64, + pub age_ticks: u64, + pub stage: String, + pub node: String, + pub grant_tick: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct FabricQueue { + pub node: String, + pub ingress: usize, + /// None for the shared FIFO; Some(destination) for a virtual output queue. + pub egress: Option, + pub packet_ids: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct FabricEvent { + pub sequence: u64, + pub tick: u64, + pub kind: String, + pub packet_id: Option, + pub from: String, + pub to: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct FabricSnapshot { + pub schema_version: u32, + pub config: FabricConfig, + pub tick: u64, + pub capacity_packets: usize, + pub blocked: [bool; PORTS], + pub queues: Vec, + pub packets: Vec, + /// Recent events only. Sequence numbers allow a renderer to de-duplicate. + pub events: Vec, + pub attempted: u64, + pub admitted: u64, + /// Rejected on admission because the shared capacity was full. + pub dropped: u64, + pub delivered: u64, + pub delivered_by_egress: [u64; PORTS], + pub buffered: usize, + pub in_flight: usize, + pub occupied: usize, + /// Sum over delivered packets only; ticks include transit, not just queueing. + pub total_latency_ticks: u64, + pub max_latency_ticks: u64, + pub last_delivered_id: Option, + /// True iff attempted = dropped + delivered + all currently held packets. + pub conserved: bool, +} + +#[derive(Clone, Debug)] +pub struct FabricModel { + config: FabricConfig, + tick: u64, + queues: [VecDeque; PORTS], + transfers: [Option; PORTS], + blocked: [bool; PORTS], + next_id: u64, + next_ingress: usize, + output_cursor: usize, + input_cursor: [usize; PORTS], + events: VecDeque, + event_sequence: u64, + attempted: u64, + admitted: u64, + dropped: u64, + delivered: u64, + delivered_by_egress: [u64; PORTS], + total_latency_ticks: u64, + max_latency_ticks: u64, + last_delivered_id: Option, +} + +impl Default for FabricModel { + fn default() -> Self { + Self::with_config(FabricConfig::default()) + } +} + +impl FabricModel { + pub fn new() -> Self { + Self::default() + } + + pub fn with_config(config: FabricConfig) -> Self { + Self { + config, + tick: 0, + queues: std::array::from_fn(|_| VecDeque::new()), + transfers: std::array::from_fn(|_| None), + blocked: [false; PORTS], + next_id: 1, + next_ingress: 0, + output_cursor: 0, + input_cursor: [0; PORTS], + events: VecDeque::new(), + event_sequence: 0, + attempted: 0, + admitted: 0, + dropped: 0, + delivered: 0, + delivered_by_egress: [0; PORTS], + total_latency_ticks: 0, + max_latency_ticks: 0, + last_delivered_id: None, + } + } + + /// Reset packets, counters, blocks and arbitration, retaining the selected mode. + pub fn reset(&mut self) { + *self = Self::with_config(self.config); + } + + pub fn set_mode(&mut self, mode: QueueMode) { + if self.config.queue_mode != mode { + self.config.queue_mode = mode; + self.event("mode", None, "scheduler".into(), mode_name(mode).into()); + } + } + + pub fn set_blocked(&mut self, egress: usize, blocked: bool) -> Result<(), &'static str> { + if egress >= PORTS { + return Err("Egress must be 0, 1 or 2"); + } + if self.blocked[egress] != blocked { + self.blocked[egress] = blocked; + self.event( + if blocked { "block" } else { "unblock" }, + None, + "control".into(), + output_node(egress), + ); + } + Ok(()) + } + + /// A bounded visitor burst distributed cyclically across the three inputs. + /// Returns the number admitted; all valid attempts receive unique packet IDs. + pub fn inject_burst(&mut self, egress: usize, count: u32) -> Result { + self.validate_injection(0, egress, count)?; + let mut accepted = 0; + for _ in 0..count { + let ingress = self.next_ingress; + self.next_ingress = (self.next_ingress + 1) % PORTS; + accepted += u32::from(self.admit(ingress, egress)); + } + Ok(accepted) + } + + /// Explicit input for deterministic scenarios and tests. + pub fn inject( + &mut self, + ingress: usize, + egress: usize, + count: u32, + ) -> Result { + self.validate_injection(ingress, egress, count)?; + Ok((0..count) + .map(|_| u32::from(self.admit(ingress, egress))) + .sum()) + } + + fn validate_injection( + &self, + ingress: usize, + egress: usize, + count: u32, + ) -> Result<(), &'static str> { + if ingress >= PORTS || egress >= PORTS { + return Err("Ports must be 0, 1 or 2"); + } + if !(1..=MAX_BURST).contains(&count) { + return Err("Burst must contain 1 to 64 packets"); + } + if self.next_id.checked_add(u64::from(count)).is_none() { + return Err("Packet identifier capacity reached; reset the model"); + } + Ok(()) + } + + fn occupied(&self) -> usize { + self.queues.iter().map(VecDeque::len).sum::() + + self.transfers.iter().filter(|t| t.is_some()).count() + } + + fn admit(&mut self, ingress: usize, egress: usize) -> bool { + let id = self.next_id; + self.next_id += 1; + self.attempted += 1; + if self.occupied() == BUFFER_CAPACITY { + self.dropped += 1; + self.event("drop", Some(id), input_node(ingress), "drop".into()); + return false; + } + self.queues[ingress].push_back(Packet { + id, + ingress, + egress, + born_tick: self.tick, + }); + self.admitted += 1; + self.event( + "admit", + Some(id), + input_node(ingress), + queue_node(self.config.queue_mode, ingress, egress), + ); + true + } + + /// One authored logical tick. Prior transfers complete before new grants. + /// Blocking an output also holds a previously granted transfer until release. + pub fn step(&mut self) { + self.tick = self.tick.saturating_add(1); + for egress in 0..PORTS { + if self.blocked[egress] { + continue; + } + if let Some(transfer) = self.transfers[egress].take() { + let packet = transfer.packet; + let latency = self.tick - packet.born_tick; + self.delivered += 1; + self.delivered_by_egress[egress] += 1; + self.total_latency_ticks = self.total_latency_ticks.saturating_add(latency); + self.max_latency_ticks = self.max_latency_ticks.max(latency); + self.last_delivered_id = Some(packet.id); + self.event( + "deliver", + Some(packet.id), + "fabric".into(), + output_node(egress), + ); + } + } + let mut input_used = [false; PORTS]; + for offset in 0..PORTS { + let egress = (self.output_cursor + offset) % PORTS; + if self.blocked[egress] || self.transfers[egress].is_some() { + continue; + } + for distance in 0..PORTS { + let ingress = (self.input_cursor[egress] + distance) % PORTS; + if input_used[ingress] { + continue; + } + let position = match self.config.queue_mode { + QueueMode::Fifo => self.queues[ingress] + .front() + .filter(|p| p.egress == egress) + .map(|_| 0), + QueueMode::Voq => self.queues[ingress].iter().position(|p| p.egress == egress), + }; + if let Some(position) = position { + let packet = self.queues[ingress].remove(position).unwrap(); + self.event( + "credit", + Some(packet.id), + output_node(egress), + queue_node(self.config.queue_mode, ingress, egress), + ); + self.event( + "transfer", + Some(packet.id), + queue_node(self.config.queue_mode, ingress, egress), + output_node(egress), + ); + self.transfers[egress] = Some(Transfer { + packet, + grant_tick: self.tick, + }); + input_used[ingress] = true; + self.input_cursor[egress] = (ingress + 1) % PORTS; + break; + } + } + } + self.output_cursor = (self.output_cursor + 1) % PORTS; + } + + pub fn advance(&mut self, ticks: u32) -> Result<(), &'static str> { + if ticks > MAX_ADVANCE { + return Err("Advance is limited to 4096 ticks per call"); + } + for _ in 0..ticks { + self.step(); + } + Ok(()) + } + + fn event(&mut self, kind: &str, packet_id: Option, from: String, to: String) { + self.event_sequence = self.event_sequence.saturating_add(1); + if self.events.len() == EVENT_CAPACITY { + self.events.pop_front(); + } + self.events.push_back(FabricEvent { + sequence: self.event_sequence, + tick: self.tick, + kind: kind.into(), + packet_id, + from, + to, + }); + } + + pub fn snapshot(&self) -> FabricSnapshot { + let mut queues = Vec::new(); + let mut packets = Vec::new(); + for (ingress, queue) in self.queues.iter().enumerate() { + if self.config.queue_mode == QueueMode::Fifo { + queues.push(FabricQueue { + node: queue_node(QueueMode::Fifo, ingress, 0), + ingress, + egress: None, + packet_ids: queue.iter().map(|p| p.id).collect(), + }); + } else { + for egress in 0..PORTS { + queues.push(FabricQueue { + node: queue_node(QueueMode::Voq, ingress, egress), + ingress, + egress: Some(egress), + packet_ids: queue + .iter() + .filter(|p| p.egress == egress) + .map(|p| p.id) + .collect(), + }); + } + } + for p in queue { + packets.push(FabricPacket { + id: p.id, + ingress: p.ingress, + egress: p.egress, + born_tick: p.born_tick, + age_ticks: self.tick - p.born_tick, + stage: "queued".into(), + node: queue_node(self.config.queue_mode, ingress, p.egress), + grant_tick: None, + }); + } + } + for transfer in self.transfers.iter().flatten() { + let p = &transfer.packet; + packets.push(FabricPacket { + id: p.id, + ingress: p.ingress, + egress: p.egress, + born_tick: p.born_tick, + age_ticks: self.tick - p.born_tick, + stage: "transfer".into(), + node: "fabric".into(), + grant_tick: Some(transfer.grant_tick), + }); + } + packets.sort_by_key(|p| p.id); + let buffered = self.queues.iter().map(VecDeque::len).sum(); + let in_flight = self.transfers.iter().filter(|t| t.is_some()).count(); + let occupied = buffered + in_flight; + FabricSnapshot { + schema_version: 1, + config: self.config, + tick: self.tick, + capacity_packets: BUFFER_CAPACITY, + blocked: self.blocked, + queues, + packets, + events: self.events.iter().cloned().collect(), + attempted: self.attempted, + admitted: self.admitted, + dropped: self.dropped, + delivered: self.delivered, + delivered_by_egress: self.delivered_by_egress, + buffered, + in_flight, + occupied, + total_latency_ticks: self.total_latency_ticks, + max_latency_ticks: self.max_latency_ticks, + last_delivered_id: self.last_delivered_id, + conserved: self.attempted == self.dropped + self.delivered + occupied as u64 + && self.admitted == self.delivered + occupied as u64, + } + } +} + +fn mode_name(mode: QueueMode) -> &'static str { + match mode { + QueueMode::Fifo => "fifo", + QueueMode::Voq => "voq", + } +} +fn input_node(ingress: usize) -> String { + format!("ingress-{ingress}") +} +fn output_node(egress: usize) -> String { + format!("egress-{egress}") +} +fn queue_node(mode: QueueMode, ingress: usize, egress: usize) -> String { + match mode { + QueueMode::Fifo => format!("fifo-{ingress}"), + QueueMode::Voq => format!("voq-{ingress}-{egress}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + fn assert_conserved(model: &FabricModel) { + let s = model.snapshot(); + assert!(s.conserved); + assert!(s.occupied <= BUFFER_CAPACITY); + assert_eq!(s.packets.len(), s.occupied); + assert_eq!( + s.packets.iter().map(|p| p.id).collect::>().len(), + s.occupied + ); + assert_eq!( + s.queues.iter().map(|q| q.packet_ids.len()).sum::(), + s.buffered + ); + } + + #[test] + fn voq_avoids_blocked_fifo_head_for_the_same_input_sequence() { + let mut fifo = FabricModel::with_config(FabricConfig { + queue_mode: QueueMode::Fifo, + }); + fifo.set_blocked(0, true).unwrap(); + fifo.inject(0, 0, 1).unwrap(); + fifo.inject(0, 1, 1).unwrap(); + let mut voq = fifo.clone(); + voq.set_mode(QueueMode::Voq); + fifo.advance(4).unwrap(); + voq.advance(4).unwrap(); + assert_eq!(fifo.snapshot().delivered, 0); + assert_eq!(voq.snapshot().delivered_by_egress, [0, 1, 0]); + assert_eq!(voq.snapshot().packets[0].id, 1); + fifo.set_blocked(0, false).unwrap(); + voq.set_blocked(0, false).unwrap(); + fifo.advance(4).unwrap(); + voq.advance(4).unwrap(); + assert_eq!(fifo.snapshot().delivered, 2); + assert_eq!(voq.snapshot().delivered, 2); + assert_conserved(&fifo); + assert_conserved(&voq); + } + + #[test] + fn shared_capacity_drops_only_excess_and_includes_transit() { + let mut m = FabricModel::default(); + assert_eq!(m.inject_burst(0, 40).unwrap(), 32); + assert_eq!(m.snapshot().dropped, 8); + m.step(); + assert_eq!(m.snapshot().in_flight, 1); + assert_eq!(m.inject(1, 1, 1).unwrap(), 0); + m.advance(40).unwrap(); + assert_eq!(m.snapshot().delivered, 32); + assert_eq!(m.snapshot().dropped, 9); + assert_conserved(&m); + } + + #[test] + fn blocking_a_granted_transfer_holds_it_and_consumes_credit() { + let mut m = FabricModel::default(); + m.inject(0, 0, 2).unwrap(); + m.step(); + m.set_blocked(0, true).unwrap(); + m.advance(5).unwrap(); + assert_eq!(m.snapshot().in_flight, 1); + assert_eq!(m.snapshot().buffered, 1); + assert_eq!(m.snapshot().delivered, 0); + m.set_blocked(0, false).unwrap(); + m.advance(2).unwrap(); + assert_eq!(m.snapshot().delivered, 2); + assert_eq!(m.snapshot().total_latency_ticks, 15); + assert_conserved(&m); + } + + #[test] + fn grants_use_each_input_and_output_at_most_once_per_tick() { + let mut m = FabricModel::default(); + for i in 0..PORTS { + for o in 0..PORTS { + m.inject(i, o, 2).unwrap(); + } + } + for _ in 0..12 { + m.step(); + let snapshot = m.snapshot(); + let transfers: Vec<_> = snapshot + .packets + .iter() + .filter(|p| p.stage == "transfer") + .collect(); + assert_eq!( + transfers + .iter() + .map(|p| p.ingress) + .collect::>() + .len(), + transfers.len() + ); + assert_eq!( + transfers + .iter() + .map(|p| p.egress) + .collect::>() + .len(), + transfers.len() + ); + assert_conserved(&m); + } + assert_eq!(m.snapshot().delivered, 18); + } + + #[test] + fn competing_inputs_receive_round_robin_service() { + let mut m = FabricModel::default(); + for i in 0..PORTS { + m.inject(i, 0, 5).unwrap(); + } + let mut order = Vec::new(); + for _ in 0..6 { + m.step(); + order.push( + m.snapshot() + .packets + .iter() + .find(|p| p.stage == "transfer") + .unwrap() + .ingress, + ); + } + assert_eq!(order, [0, 1, 2, 0, 1, 2]); + } + + #[test] + fn same_input_destinations_also_receive_bounded_service() { + let mut m = FabricModel::default(); + for o in 0..PORTS { + m.inject(0, o, 4).unwrap(); + } + m.advance(4).unwrap(); + assert_eq!(m.snapshot().delivered_by_egress, [1, 1, 1]); + } + + #[test] + fn mode_changes_reorganize_without_losing_packets_or_ids() { + let mut m = FabricModel::default(); + m.inject_burst(2, 7).unwrap(); + let before = m.snapshot(); + m.set_mode(QueueMode::Fifo); + let after = m.snapshot(); + assert_eq!(after.queues.len(), 3); + assert_eq!( + before.packets.iter().map(|p| p.id).collect::>(), + after.packets.iter().map(|p| p.id).collect::>() + ); + assert_eq!(after.admitted, before.admitted); + assert_conserved(&m); + } + + #[test] + fn invalid_commands_are_atomic() { + let mut m = FabricModel::default(); + let before = m.snapshot(); + assert!(m.inject(3, 0, 4).is_err()); + assert!(m.inject_burst(3, 4).is_err()); + assert!(m.inject_burst(0, 0).is_err()); + assert!(m.inject_burst(0, MAX_BURST + 1).is_err()); + assert!(m.set_blocked(3, true).is_err()); + assert!(m.advance(MAX_ADVANCE + 1).is_err()); + assert_eq!(before, m.snapshot()); + } + + #[test] + fn replay_is_deterministic_and_reset_retains_only_mode() { + fn run(m: &mut FabricModel) { + m.inject_burst(0, 12).unwrap(); + m.inject_burst(1, 12).unwrap(); + m.set_blocked(0, true).unwrap(); + m.advance(5).unwrap(); + m.set_blocked(0, false).unwrap(); + m.advance(8).unwrap(); + } + let mut a = FabricModel::default(); + let mut b = FabricModel::default(); + run(&mut a); + for _ in 0..50 { + let _ = a.snapshot(); + } + run(&mut b); + assert_eq!(a.snapshot(), b.snapshot()); + a.set_mode(QueueMode::Fifo); + a.reset(); + assert_eq!( + a.snapshot(), + FabricModel::with_config(FabricConfig { + queue_mode: QueueMode::Fifo + }) + .snapshot() + ); + } + + #[test] + fn batched_advance_matches_individual_ticks_across_interventions() { + let mut batched = FabricModel::default(); + let mut stepped = FabricModel::default(); + for phase in 0..12 { + for model in [&mut batched, &mut stepped] { + model.inject_burst(phase % PORTS, 7).unwrap(); + model + .set_blocked((phase + 1) % PORTS, phase % 3 == 0) + .unwrap(); + model.set_mode(if phase % 2 == 0 { + QueueMode::Fifo + } else { + QueueMode::Voq + }); + } + let ticks = (phase % 5 + 1) as u32; + batched.advance(ticks).unwrap(); + for _ in 0..ticks { + stepped.step(); + } + assert_eq!(batched.snapshot(), stepped.snapshot()); + assert_conserved(&batched); + } + let before = batched.snapshot(); + batched.advance(0).unwrap(); + assert_eq!(before, batched.snapshot()); + } + + #[test] + fn changing_mode_preserves_a_blocked_transfer_and_destination_order() { + let mut m = FabricModel::with_config(FabricConfig { + queue_mode: QueueMode::Fifo, + }); + m.inject(0, 0, 2).unwrap(); // ids 1,2: same destination, arrival order + m.inject(0, 1, 1).unwrap(); // id 3: independent destination behind them + m.step(); // id 1 owns the output credit + m.set_blocked(0, true).unwrap(); + m.set_mode(QueueMode::Voq); + m.advance(2).unwrap(); // id 3 bypasses blocked id 2, then completes + let held = m.snapshot(); + assert_eq!(held.delivered, 1); + assert_eq!(held.last_delivered_id, Some(3)); + assert_eq!(held.in_flight, 1); + let packet = held.packets.iter().find(|p| p.id == 1).unwrap(); + assert_eq!(packet.stage, "transfer"); + assert_eq!(packet.grant_tick, Some(1)); + assert_conserved(&m); + m.set_mode(QueueMode::Fifo); + m.advance(2).unwrap(); + assert_eq!(m.snapshot().delivered, 1); + assert_eq!(m.snapshot().in_flight, 1); + m.set_blocked(0, false).unwrap(); + m.advance(2).unwrap(); + let final_state = m.snapshot(); + let delivery_order: Vec<_> = final_state + .events + .iter() + .filter(|e| e.kind == "deliver") + .map(|e| e.packet_id.unwrap()) + .collect(); + assert_eq!(delivery_order, [3, 1, 2]); + assert_eq!(final_state.delivered_by_egress, [2, 1, 0]); + assert_eq!(final_state.occupied, 0); + assert_conserved(&m); + } + + #[test] + fn long_intervention_stream_remains_bounded_and_conserved() { + let mut m = FabricModel::default(); + for i in 0..1000 { + m.inject_burst(i % PORTS, 4).unwrap(); + m.set_blocked((i + 1) % PORTS, i % 5 == 0).unwrap(); + if i % 7 == 0 { + m.set_mode(if i % 2 == 0 { + QueueMode::Fifo + } else { + QueueMode::Voq + }); + } + m.step(); + assert_conserved(&m); + assert!(m.snapshot().events.len() <= EVENT_CAPACITY); + } + for i in 0..PORTS { + m.set_blocked(i, false).unwrap(); + } + m.advance(100).unwrap(); + assert_eq!(m.snapshot().occupied, 0); + assert_conserved(&m); + let json = serde_json::to_value(m.snapshot()).unwrap(); + assert!(json["config"]["queue_mode"].is_string()); + } +} diff --git a/crates/doke-core/src/lib.rs b/crates/doke-core/src/lib.rs index edee7c9..6b1a885 100644 --- a/crates/doke-core/src/lib.rs +++ b/crates/doke-core/src/lib.rs @@ -1,4 +1,7 @@ //! Deterministic educational timelines and a separate, uncalibrated rate model. +pub mod fabric; +pub mod silicon; + use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/crates/doke-core/src/silicon.rs b/crates/doke-core/src/silicon.rs new file mode 100644 index 0000000..ea6df1b --- /dev/null +++ b/crates/doke-core/src/silicon.rs @@ -0,0 +1,1849 @@ +//! Small deterministic dataflow models, not cycle-accurate vendor emulators. +//! All three machines compute C=A*B exactly with i32 arithmetic. Public chip +//! specifications are deliberately absent from the timing/resource configuration. +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeSet, VecDeque}; + +const SIDE: usize = 8; +const CELLS: usize = SIDE * SIDE; +const MAX_BATCH: u32 = 256; +const TRANSFER_HISTORY: usize = 64; +const MEMORY_SLOTS: usize = 4; +const HIT_TICKS: u64 = 1; +const MISS_TICKS: u64 = 7; +const MAC_TICKS: u64 = 2; +const CACHE_LINES: usize = 4; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Mode { + Cpu, + Gpu, + Tensor, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Layout { + Packed, + Strided, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Filter { + Difference, + Identity, + Mix, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct SiliconConfig { + pub mode: Mode, + pub input: Vec, + pub filter: Filter, + pub layout: Layout, + pub cache_enabled: bool, + pub issue_width: u8, +} +impl Default for SiliconConfig { + fn default() -> Self { + Self { + mode: Mode::Cpu, + input: (0..CELLS) + .map(|i| { + if (i / SIDE + i % SIDE).is_multiple_of(3) { + 6 + } else { + 1 + } + }) + .collect(), + filter: Filter::Difference, + layout: Layout::Packed, + cache_enabled: true, + issue_width: 2, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum RunStatus { + Ready, + Running, + Complete, +} +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] +pub struct Counters { + pub loads: u64, + pub cache_hits: u64, + pub cache_misses: u64, + pub merged_misses: u64, + pub sectors: u64, + /// External memory read payload (including fetched sectors/lines) plus final writes. + pub bytes_moved: u64, + pub issued: u64, + pub macs: u64, + pub completed_outputs: u64, + pub active_lane_ops: u64, + pub issued_lane_slots: u64, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Transfer { + pub id: u64, + pub from: String, + pub to: String, + pub start_tick: u64, + pub end_tick: u64, + pub bytes: u32, + pub value: Option, +} +#[derive(Clone, Debug, Serialize)] +pub struct NodeState { + pub id: String, + pub state: &'static str, + pub occupancy: usize, + pub capacity: usize, +} +#[derive(Clone, Debug, Serialize)] +pub struct LaneState { + pub lane: usize, + pub active: bool, + pub output_index: Option, +} +/// A multiplication that has really completed, not a precomputed reference term. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct RetiredTerm { + pub k: usize, + pub a: i32, + pub b: i32, + pub product: i32, + pub sum_after: i32, + pub retired_tick: u64, +} +#[derive(Clone, Debug, Serialize)] +pub struct OutputDetail { + pub output_index: usize, + pub retired_terms: usize, + pub accumulated_sum: i32, + pub stored: bool, + pub terms: Vec, + /// Independent expected answer, never used to drive execution or activity. + pub reference_sum: i32, +} +#[derive(Clone, Debug, Serialize)] +pub struct CpuJobDetail { + pub output_index: usize, + pub k: usize, + pub stage: &'static str, + pub reason: &'static str, + pub ready_tick: Option, + /// Model byte addresses of this term; an address does not imply a request. + pub a_address: Option, + pub b_address: Option, + /// An operand is visible only after its load completes for the current k. + pub a: Option, + pub b: Option, + pub accumulated_sum: i32, + pub retired_terms: usize, + pub stored: bool, +} +#[derive(Clone, Debug, Serialize)] +pub struct CacheLineDetail { + pub tag: u64, + pub base_address: u64, +} +#[derive(Clone, Debug, Serialize)] +pub struct CacheFillDetail { + pub tag: u64, + pub arrival_tick: u64, +} +#[derive(Clone, Debug, Serialize)] +pub struct CacheDetail { + pub enabled: bool, + pub capacity_lines: usize, + pub line_bytes: u32, + /// Actual replacement order, not physical cache ways or sets. + pub lru_to_mru: Vec, + pub pending_fills: Vec, +} +#[derive(Clone, Debug, Serialize)] +pub struct GpuLaneDetail { + pub lane: usize, + pub output_index: usize, + pub a_address: Option, + pub b_address: Option, + pub a: Option, + pub b: Option, + pub accumulated_sum: i32, + pub retired_terms: usize, + pub stored: bool, +} +#[derive(Clone, Debug, Serialize)] +pub struct WarpDetail { + pub warp: usize, + pub k: usize, + pub stage: &'static str, + pub reason: &'static str, + pub ready_tick: Option, + /// Actual sector IDs requested for the current k. Empty before issue. + pub requested_a_sectors: Vec, + pub requested_b_sectors: Vec, + pub lanes: Vec, +} +#[derive(Clone, Debug, Serialize)] +pub struct ExecutionDetail { + pub cpu_jobs: Vec, + pub cache: CacheDetail, + pub warps: Vec, + pub outputs: Vec, +} +#[derive(Clone, Debug, Serialize)] +pub struct Rules { + pub label: &'static str, + pub version: u32, + pub time_unit: &'static str, + pub cpu_hit_ticks: u64, + pub cpu_miss_ticks: u64, + pub mac_ticks: u64, + pub memory_slots: usize, + pub cpu_cache_line_bytes: u32, + pub cpu_cache_lines: usize, + pub cpu_cache_policy: &'static str, + pub gpu_sector_bytes: u32, + pub transfer_base_ticks: u64, + pub sectors_per_transfer_tick: u32, + pub gpu_warp_lanes: usize, + pub tensor_tile_side: usize, + pub tensor_mac_ticks: u64, + pub tensor_wave_ticks: usize, + pub memory_slot_meaning: &'static str, +} +#[derive(Clone, Debug, Serialize)] +pub struct SiliconSnapshot { + pub tick: u64, + pub status: RunStatus, + pub mode: Mode, + pub input: Vec, + pub weights: Vec, + pub output: Vec, + pub valid: Vec, + pub nodes: Vec, + pub transfers: Vec, + pub lanes: Vec, + /// Warp or output-tile currently represented by lane/PE nodes. + pub group_index: Option, + pub counters: Counters, + pub rules: Rules, + pub config: SiliconConfig, + pub detail: ExecutionDetail, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Stage { + LoadA, + WaitingA, + LoadB, + WaitingB, + Mac, + WaitingMac, + Done, +} +#[derive(Clone, Debug)] +struct Job { + k: usize, + a: i32, + b: i32, + sum: i32, + stage: Stage, +} +impl Default for Job { + fn default() -> Self { + Self { + k: 0, + a: 0, + b: 0, + sum: 0, + stage: Stage::LoadA, + } + } +} +#[derive(Clone, Debug)] +enum Completion { + Load { + job: usize, + a: bool, + value: i32, + fill: Option, + }, + Mac { + job: usize, + }, + WarpLoad { + warp: usize, + }, + WarpMac { + warp: usize, + }, + TileLoad { + tile: usize, + }, + TileMac { + tile: usize, + a: Box<[Option<(usize, i32)>; 16]>, + b: Box<[Option<(usize, i32)>; 16]>, + }, + Store { + values: Vec<(usize, i32)>, + }, +} +#[derive(Clone, Debug)] +struct Pending { + end: u64, + action: Completion, +} +#[derive(Clone, Debug)] +struct Group { + k: usize, + stage: Stage, +} +impl Default for Group { + fn default() -> Self { + Self { + k: 0, + stage: Stage::LoadA, + } + } +} + +#[derive(Clone, Debug)] +pub struct SiliconModel { + config: SiliconConfig, + weights: Vec, + tick: u64, + status: RunStatus, + output: Vec, + valid: Vec, + jobs: Vec, + warps: Vec, + tiles: Vec, + pending: Vec, + cache: VecDeque, + transfers: VecDeque, + next_transfer: u64, + counters: Counters, + cursor: usize, + selected_group: Option, + scratch_a: [i32; 32], + scratch_b: [i32; 32], + pe_a: [Option<(usize, i32)>; 16], + pe_b: [Option<(usize, i32)>; 16], + retired: Vec>, +} + +/// Distinct 32-byte sectors touched by active accesses. Address overflow is an +/// error, and inactive lanes cannot generate traffic. This counts request sectors, +/// not downstream DRAM transactions. Each access may cross a sector boundary. +pub fn coalesced_sectors( + addresses: &[u64], + active: &[bool], + bytes: u32, +) -> Result, &'static str> { + if addresses.len() != active.len() || addresses.len() > 32 || !(1..=16).contains(&bytes) { + return Err("Expected at most 32 lanes and 1..16 bytes per access"); + } + let mut sectors = BTreeSet::new(); + for (&address, &enabled) in addresses.iter().zip(active) { + if enabled { + let last = address + .checked_add(u64::from(bytes) - 1) + .ok_or("Address overflow")?; + for sector in address / 32..=last / 32 { + sectors.insert(sector); + } + } + } + Ok(sectors.into_iter().collect()) +} + +fn weights(filter: Filter) -> Vec { + (0..CELLS) + .map(|i| { + let k = i / SIDE; + let col = i % SIDE; + match filter { + Filter::Identity => i32::from(k == col), + // C[row,col] = A[row,col] - A[row,col-1], with a zero left boundary. + Filter::Difference => { + if k == col { + 1 + } else if col > 0 && k == col - 1 { + -1 + } else { + 0 + } + } + Filter::Mix => { + if k == col { + 2 + } else if k.abs_diff(col) == 1 { + 1 + } else { + 0 + } + } + } + }) + .collect() +} + +impl SiliconModel { + pub fn new(config: SiliconConfig) -> Result { + if config.input.len() != CELLS || config.input.iter().any(|v| !(-8..=8).contains(v)) { + return Err("Input must contain exactly 64 integers in -8..8"); + } + if !(1..=2).contains(&config.issue_width) { + return Err("CPU issue width must be 1 or 2"); + } + let matrix = weights(config.filter); + Ok(Self { + config, + weights: matrix, + tick: 0, + status: RunStatus::Ready, + output: vec![0; CELLS], + valid: vec![false; CELLS], + jobs: vec![Job::default(); CELLS], + warps: vec![Group::default(); 2], + tiles: vec![Group::default(); 4], + pending: Vec::new(), + cache: VecDeque::new(), + transfers: VecDeque::new(), + next_transfer: 0, + counters: Counters::default(), + cursor: 0, + selected_group: None, + scratch_a: [0; 32], + scratch_b: [0; 32], + pe_a: [None; 16], + pe_b: [None; 16], + retired: vec![Vec::new(); CELLS], + }) + } + pub fn reset(&mut self) { + *self = Self::new(self.config.clone()).expect("Validated configuration"); + } + pub fn config(&self) -> &SiliconConfig { + &self.config + } + pub fn step(&mut self, ticks: u32) -> Result<(), &'static str> { + if ticks > MAX_BATCH { + return Err("Tick batch must be 0..256"); + } + for _ in 0..ticks { + if self.status == RunStatus::Complete { + break; + } + self.status = RunStatus::Running; + self.tick += 1; + self.complete_pending(); + match self.config.mode { + Mode::Cpu => self.issue_cpu(), + Mode::Gpu => self.issue_gpu(), + Mode::Tensor => self.issue_tensor(), + } + if self.valid.iter().all(|v| *v) + && self.pending.is_empty() + && self.transfers.iter().all(|t| t.end_tick <= self.tick) + { + self.status = RunStatus::Complete; + } + } + Ok(()) + } + pub fn run_to_completion(&mut self, max_ticks: u32) -> Result<(), &'static str> { + if max_ticks > 100_000 { + return Err("Run budget must not exceed 100000 ticks"); + } + for _ in 0..max_ticks { + if self.status == RunStatus::Complete { + return Ok(()); + } + self.step(1)?; + } + if self.status == RunStatus::Complete { + Ok(()) + } else { + Err("Run budget exhausted") + } + } + fn transfer(&mut self, from: &str, to: &str, end: u64, bytes: u32, value: Option) { + self.next_transfer += 1; + self.transfers.push_back(Transfer { + id: self.next_transfer, + from: from.into(), + to: to.into(), + start_tick: self.tick, + end_tick: end.max(self.tick + 1), + bytes, + value, + }); + while self.transfers.len() > TRANSFER_HISTORY { + self.transfers.pop_front(); + } + } + // A remains row-major. B is either row-major or physically transposed. Both + // layouts represent the SAME mathematical B. Strided makes adjacent output + // columns read B at 32-byte strides for a fixed k. + fn address(&self, a: bool, row: usize, col: usize, k: usize) -> u64 { + if a { + ((row * SIDE + k) * 4) as u64 + } else { + 4096 + (match self.config.layout { + Layout::Packed => k * SIDE + col, + Layout::Strided => col * SIDE + k, + } * 4) as u64 + } + } + fn cache_hit(&mut self, line: u64) -> bool { + if let Some(at) = self.cache.iter().position(|v| *v == line) { + self.cache.remove(at); + self.cache.push_back(line); + true + } else { + false + } + } + fn fill_cache(&mut self, line: u64) { + if let Some(at) = self.cache.iter().position(|v| *v == line) { + self.cache.remove(at); + } + self.cache.push_back(line); + while self.cache.len() > CACHE_LINES { + self.cache.pop_front(); + } + } + fn memory_inflight(&self) -> usize { + self.pending + .iter() + .filter(|p| { + matches!( + p.action, + Completion::Load { .. } + | Completion::WarpLoad { .. } + | Completion::TileLoad { .. } + ) + }) + .count() + } + fn compute_inflight(&self) -> usize { + self.pending + .iter() + .filter(|p| { + matches!( + p.action, + Completion::Mac { .. } + | Completion::WarpMac { .. } + | Completion::TileMac { .. } + ) + }) + .count() + } + fn issue_cpu(&mut self) { + let mut issued = 0; + // Finite independent dot products, round-robin oldest scan. This is not + // an x86 front end, reorder buffer or speculative execution simulator. + for offset in 0..CELLS { + if issued >= self.config.issue_width { + break; + } + let id = (self.cursor + offset) % CELLS; + let stage = self.jobs[id].stage; + match stage { + Stage::LoadA | Stage::LoadB if self.memory_inflight() < MEMORY_SLOTS => { + let a = stage == Stage::LoadA; + let k = self.jobs[id].k; + let address = self.address(a, id / SIDE, id % SIDE, k); + let value = if a { + self.config.input[id / SIDE * SIDE + k] + } else { + self.weights[k * SIDE + id % SIDE] + }; + let line = address / 64; + let mut fill = None; + let mut source = "memory"; + let end; + self.counters.loads += 1; + if self.config.cache_enabled && self.cache_hit(line) { + self.counters.cache_hits += 1; + source = "cache"; + end = self.tick + HIT_TICKS; + } else if let Some(p) = self.pending.iter().find(|p| self.config.cache_enabled && matches!(p.action, Completion::Load { fill: Some(l), .. } if l == line)) { + self.counters.merged_misses += 1; + end = p.end; + } else { + self.counters.cache_misses += 1; + let bytes = if self.config.cache_enabled { 64 } else { 4 }; + self.counters.bytes_moved += bytes; + if self.config.cache_enabled { fill = Some(line); } + end = self.tick + MISS_TICKS; + if fill.is_some() { self.transfer("memory", "cache", end, 64, Some(value)); } + } + self.jobs[id].stage = if a { Stage::WaitingA } else { Stage::WaitingB }; + self.pending.push(Pending { + end, + action: Completion::Load { + job: id, + a, + value, + fill, + }, + }); + self.transfer(source, "registers", end, 4, Some(value)); + self.counters.issued += 1; + issued += 1; + } + Stage::Mac if self.compute_inflight() < self.config.issue_width as usize => { + let end = self.tick + MAC_TICKS; + self.jobs[id].stage = Stage::WaitingMac; + self.pending.push(Pending { + end, + action: Completion::Mac { job: id }, + }); + self.transfer( + "registers", + "compute", + end, + 8, + Some(self.jobs[id].a * self.jobs[id].b), + ); + self.counters.issued += 1; + issued += 1; + } + _ => {} + } + } + self.cursor = (self.cursor + 1) % CELLS; + } + fn group_load(&mut self, ids: &[usize], k: usize, destination: &str) -> u64 { + let active = vec![true; ids.len()]; + let a: Vec<_> = ids + .iter() + .map(|id| self.address(true, id / SIDE, id % SIDE, k)) + .collect(); + let b: Vec<_> = ids + .iter() + .map(|id| self.address(false, id / SIDE, id % SIDE, k)) + .collect(); + let sectors = coalesced_sectors(&a, &active, 4) + .expect("Bounded addresses") + .len() + + coalesced_sectors(&b, &active, 4) + .expect("Bounded addresses") + .len(); + self.counters.loads += (ids.len() * 2) as u64; + self.counters.sectors += sectors as u64; + self.counters.bytes_moved += (sectors * 32) as u64; + let end = self.tick + 2 + (sectors as u64).div_ceil(4); + self.transfer("memory", destination, end, (sectors * 32) as u32, None); + end + } + fn issue_gpu(&mut self) { + // One modeled warp issue slot, two resident warps; not all SM resources. + for offset in 0..2 { + let warp = (self.cursor + offset) % 2; + match self.warps[warp].stage { + Stage::LoadA => { + let ids: Vec<_> = (warp * 32..warp * 32 + 32).collect(); + let end = self.group_load(&ids, self.warps[warp].k, "registers"); + self.warps[warp].stage = Stage::WaitingA; + self.pending.push(Pending { + end, + action: Completion::WarpLoad { warp }, + }); + self.selected_group = Some(warp); + } + Stage::Mac => { + let end = self.tick + MAC_TICKS; + self.warps[warp].stage = Stage::WaitingMac; + self.pending.push(Pending { + end, + action: Completion::WarpMac { warp }, + }); + self.transfer("registers", "compute", end, 32 * 8, None); + self.selected_group = Some(warp); + } + _ => continue, + } + self.counters.issued += 1; + self.cursor = (warp + 1) % 2; + break; + } + } + fn tile_ids(tile: usize) -> Vec { + let r = tile / 2 * 4; + let c = tile % 2 * 4; + (0..4) + .flat_map(|dr| (0..4).map(move |dc| (r + dr) * SIDE + c + dc)) + .collect() + } + fn issue_tensor(&mut self) { + // An actual skewed systolic wave. A advances east, B advances south; + // a PE performs one MAC only when both operands with the same k arrive. + // This declared 4x4 array is not a vendor Tensor Core or TPU layout. + let Some(tile) = self.tiles.iter().position(|t| t.stage != Stage::Done) else { + return; + }; + match self.tiles[tile].stage { + Stage::LoadA => { + let r0 = tile / 2 * 4; + let c0 = tile % 2 * 4; + let mut a = Vec::new(); + let mut b = Vec::new(); + for local in 0..4 { + for k in 0..8 { + a.push(self.address(true, r0 + local, 0, k)); + b.push(self.address(false, 0, c0 + local, k)); + } + } + let sectors = coalesced_sectors(&a, &[true; 32], 4) + .expect("Bounded addresses") + .len() + + coalesced_sectors(&b, &[true; 32], 4) + .expect("Bounded addresses") + .len(); + self.counters.loads += 64; + self.counters.sectors += sectors as u64; + self.counters.bytes_moved += (sectors * 32) as u64; + let end = self.tick + 2 + (sectors as u64).div_ceil(4); + self.transfer("memory", "scratch", end, (sectors * 32) as u32, None); + self.tiles[tile].stage = Stage::WaitingA; + self.pending.push(Pending { + end, + action: Completion::TileLoad { tile }, + }); + self.counters.issued += 1; + self.selected_group = Some(tile); + } + Stage::Mac => { + let wave = self.tiles[tile].k; + let end = self.tick + 1; + let mut a = [None; 16]; + let mut b = [None; 16]; + for row in 0..4 { + for col in 0..4 { + let pe = row * 4 + col; + a[pe] = if col == 0 { + wave.checked_sub(row) + .filter(|k| *k < 8) + .map(|k| (k, self.scratch_a[row * 8 + k])) + } else { + self.pe_a[pe - 1] + }; + b[pe] = if row == 0 { + wave.checked_sub(col) + .filter(|k| *k < 8) + .map(|k| (k, self.scratch_b[col * 8 + k])) + } else { + self.pe_b[pe - 4] + }; + if let Some((_, value)) = a[pe] { + let from = if col == 0 { + "scratch".into() + } else { + format!("pe{}", pe - 1) + }; + self.transfer(&from, &format!("pe{pe}"), end, 4, Some(value)); + } + if let Some((_, value)) = b[pe] { + let from = if row == 0 { + "scratch".into() + } else { + format!("pe{}", pe - 4) + }; + self.transfer(&from, &format!("pe{pe}"), end, 4, Some(value)); + } + } + } + self.pending.push(Pending { + end, + action: Completion::TileMac { + tile, + a: Box::new(a), + b: Box::new(b), + }, + }); + self.tiles[tile].stage = Stage::WaitingMac; + self.counters.issued += 1; + self.selected_group = Some(tile); + } + _ => {} + } + } + fn finish_output(&mut self, id: usize, value: i32) { + self.output[id] = value; + self.valid[id] = true; + self.counters.completed_outputs += 1; + // External traffic: memory reads plus final output writes. + self.counters.bytes_moved += 4; + } + fn retire_term(&mut self, id: usize, k: usize, a: i32, b: i32, sum_after: i32) { + // One bounded record per completed MAC, independent of transfer-history + // truncation. Reset starts a new run and clears every output's provenance. + debug_assert_eq!(self.retired[id].len(), k); + debug_assert!(k < SIDE); + self.retired[id].push(RetiredTerm { + k, + a, + b, + product: a * b, + sum_after, + retired_tick: self.tick, + }); + } + fn complete_pending(&mut self) { + let mut finished = Vec::new(); + self.pending.retain(|p| { + if p.end <= self.tick { + finished.push(p.action.clone()); + false + } else { + true + } + }); + for action in finished { + match action { + Completion::Load { + job, + a, + value, + fill, + } => { + if let Some(line) = fill { + self.fill_cache(line); + } + if a { + self.jobs[job].a = value; + self.jobs[job].stage = Stage::LoadB; + } else { + self.jobs[job].b = value; + self.jobs[job].stage = Stage::Mac; + } + } + Completion::Mac { job } => { + let j = &mut self.jobs[job]; + j.sum += j.a * j.b; + let term = (j.k, j.a, j.b, j.sum); + j.k += 1; + self.counters.macs += 1; + if j.k == SIDE { + let value = j.sum; + j.stage = Stage::Done; + self.pending.push(Pending { + end: self.tick + 1, + action: Completion::Store { + values: vec![(job, value)], + }, + }); + self.transfer("compute", "output", self.tick + 1, 4, Some(value)); + } else { + j.stage = Stage::LoadA; + } + self.retire_term(job, term.0, term.1, term.2, term.3); + } + Completion::WarpLoad { warp } => { + let k = self.warps[warp].k; + for id in warp * 32..warp * 32 + 32 { + // Latch actual operands at completion, just like the CPU + // model. Neither the inspector nor execution reads them + // early from the known input matrix. + self.jobs[id].a = self.config.input[id / SIDE * SIDE + k]; + self.jobs[id].b = self.weights[k * SIDE + id % SIDE]; + } + self.warps[warp].stage = Stage::Mac; + } + Completion::WarpMac { warp } => { + let k = self.warps[warp].k; + for id in warp * 32..warp * 32 + 32 { + let (a, b) = (self.jobs[id].a, self.jobs[id].b); + self.output[id] += a * b; + self.retire_term(id, k, a, b, self.output[id]); + } + self.counters.macs += 32; + self.counters.active_lane_ops += 32; + self.counters.issued_lane_slots += 32; + self.warps[warp].k += 1; + self.warps[warp].stage = if k + 1 == SIDE { + Stage::Done + } else { + Stage::LoadA + }; + if k + 1 == SIDE { + let values = (warp * 32..warp * 32 + 32) + .map(|id| (id, self.output[id])) + .collect(); + self.pending.push(Pending { + end: self.tick + 1, + action: Completion::Store { values }, + }); + self.transfer("compute", "output", self.tick + 1, 128, None); + } + } + Completion::TileLoad { tile } => { + let r0 = tile / 2 * 4; + let c0 = tile % 2 * 4; + for local in 0..4 { + for k in 0..8 { + self.scratch_a[local * 8 + k] = self.config.input[(r0 + local) * 8 + k]; + self.scratch_b[local * 8 + k] = self.weights[k * 8 + c0 + local]; + } + } + self.pe_a = [None; 16]; + self.pe_b = [None; 16]; + self.tiles[tile].stage = Stage::Mac; + } + Completion::TileMac { tile, a, b } => { + self.pe_a = *a; + self.pe_b = *b; + let ids = Self::tile_ids(tile); + for pe in 0..16 { + if let (Some((ka, av)), Some((kb, bv))) = (a[pe], b[pe]) { + debug_assert_eq!(ka, kb, "Systolic operands must have matching k"); + let id = ids[pe]; + self.output[id] += av * bv; + self.retire_term(id, ka, av, bv, self.output[id]); + self.counters.macs += 1; + if ka == 7 { + self.pending.push(Pending { + end: self.tick + 1, + action: Completion::Store { + values: vec![(id, self.output[id])], + }, + }); + self.transfer( + &format!("pe{pe}"), + "output", + self.tick + 1, + 4, + Some(self.output[id]), + ); + } + } + } + self.tiles[tile].k += 1; + self.tiles[tile].stage = if self.tiles[tile].k == 14 { + Stage::Done + } else { + Stage::Mac + }; + } + Completion::Store { values } => { + for (id, value) in values { + self.finish_output(id, value); + } + } + } + } + } + fn store_ready_tick(&self, id: usize) -> Option { + self.pending + .iter() + .find_map(|pending| match &pending.action { + Completion::Store { values } if values.iter().any(|(output, _)| *output == id) => { + Some(pending.end) + } + _ => None, + }) + } + fn execution_detail(&self) -> ExecutionDetail { + let outputs = (0..CELLS) + .map(|id| OutputDetail { + output_index: id, + retired_terms: self.retired[id].len(), + accumulated_sum: self.retired[id].last().map_or(0, |term| term.sum_after), + stored: self.valid[id], + terms: self.retired[id].clone(), + reference_sum: (0..SIDE) + .map(|k| { + self.config.input[id / SIDE * SIDE + k] * self.weights[k * SIDE + id % SIDE] + }) + .sum(), + }) + .collect(); + let cpu_jobs = if self.config.mode == Mode::Cpu { + self.jobs + .iter() + .enumerate() + .map(|(id, job)| { + let (stage, reason) = match job.stage { + Stage::LoadA | Stage::LoadB => ( + if job.stage == Stage::LoadA { + "ready_a" + } else { + "ready_b" + }, + if self.memory_inflight() >= MEMORY_SLOTS { + "All operand request slots are occupied" + } else { + "Operand load is ready; awaiting a round-robin issue slot" + }, + ), + Stage::WaitingA => ("waiting_a", "Operand A request has not completed"), + Stage::WaitingB => ("waiting_b", "Operand B request has not completed"), + Stage::Mac => ( + "ready_mac", + if self.compute_inflight() >= self.config.issue_width as usize { + "All multiply-accumulate slots are occupied" + } else { + "Both operands are ready; awaiting a round-robin issue slot" + }, + ), + Stage::WaitingMac => ( + "executing_mac", + "Multiply-accumulate is in flight; its sum is not retired yet", + ), + Stage::Done if self.valid[id] => ( + "stored", + "All eight products and the final output write have completed", + ), + Stage::Done => ( + "waiting_store", + "All eight products have retired; the output write is in flight", + ), + }; + let ready_tick = self + .pending + .iter() + .find_map(|pending| match pending.action { + Completion::Load { job, .. } | Completion::Mac { job } if job == id => { + Some(pending.end) + } + _ => None, + }) + .or_else(|| self.store_ready_tick(id)); + CpuJobDetail { + output_index: id, + k: job.k, + stage, + reason, + ready_tick, + a_address: (job.k < SIDE) + .then(|| self.address(true, id / SIDE, id % SIDE, job.k)), + b_address: (job.k < SIDE) + .then(|| self.address(false, id / SIDE, id % SIDE, job.k)), + a: matches!( + job.stage, + Stage::LoadB | Stage::WaitingB | Stage::Mac | Stage::WaitingMac + ) + .then_some(job.a), + b: matches!(job.stage, Stage::Mac | Stage::WaitingMac).then_some(job.b), + accumulated_sum: job.sum, + retired_terms: self.retired[id].len(), + stored: self.valid[id], + } + }) + .collect() + } else { + Vec::new() + }; + let warps = if self.config.mode == Mode::Gpu { + self.warps + .iter() + .enumerate() + .map(|(warp, group)| { + let first = warp * 32; + let (stage, reason) = match group.stage { + Stage::LoadA => ( + "ready_operands", + "Warp is ready; awaiting the shared issue slot", + ), + Stage::WaitingA => ( + "waiting_operands", + "Requested memory sectors have not completed", + ), + Stage::Mac => ( + "ready_mac", + "Both operands are ready; awaiting the shared issue slot", + ), + Stage::WaitingMac => ( + "executing_mac", + "All 32 lanes execute this product; their sums are not retired yet", + ), + Stage::Done if self.valid[first] => ( + "stored", + "All eight products and the warp's output write have completed", + ), + Stage::Done => ( + "waiting_store", + "All eight products have retired; the warp's output write is in flight", + ), + _ => unreachable!("GPU groups load both operands together"), + }; + let ready_tick = self + .pending + .iter() + .find_map(|pending| match pending.action { + Completion::WarpLoad { warp: w } | Completion::WarpMac { warp: w } + if w == warp => + { + Some(pending.end) + } + _ => None, + }) + .or_else(|| self.store_ready_tick(first)); + let arrived = matches!(group.stage, Stage::Mac | Stage::WaitingMac); + let lanes: Vec<_> = (0..32) + .map(|lane| { + let id = first + lane; + GpuLaneDetail { + lane, + output_index: id, + a_address: (group.k < SIDE) + .then(|| self.address(true, id / SIDE, id % SIDE, group.k)), + b_address: (group.k < SIDE) + .then(|| self.address(false, id / SIDE, id % SIDE, group.k)), + a: arrived.then_some(self.jobs[id].a), + b: arrived.then_some(self.jobs[id].b), + accumulated_sum: self.retired[id] + .last() + .map_or(0, |term| term.sum_after), + retired_terms: self.retired[id].len(), + stored: self.valid[id], + } + }) + .collect(); + let requested = matches!( + group.stage, + Stage::WaitingA | Stage::Mac | Stage::WaitingMac + ); + let sectors = |a: bool| { + if !requested { + return Vec::new(); + } + let addresses: Vec<_> = lanes + .iter() + .map(|lane| { + if a { lane.a_address } else { lane.b_address } + .expect("In-flight term has an address") + }) + .collect(); + coalesced_sectors(&addresses, &[true; 32], 4) + .expect("Bounded model addresses") + }; + WarpDetail { + warp, + k: group.k, + stage, + reason, + ready_tick, + requested_a_sectors: sectors(true), + requested_b_sectors: sectors(false), + lanes, + } + }) + .collect() + } else { + Vec::new() + }; + ExecutionDetail { + cpu_jobs, + cache: CacheDetail { + enabled: self.config.mode == Mode::Cpu && self.config.cache_enabled, + capacity_lines: CACHE_LINES, + line_bytes: 64, + lru_to_mru: self + .cache + .iter() + .map(|&tag| CacheLineDetail { + tag, + base_address: tag * 64, + }) + .collect(), + pending_fills: self + .pending + .iter() + .filter_map(|pending| match pending.action { + Completion::Load { + fill: Some(tag), .. + } => Some(CacheFillDetail { + tag, + arrival_tick: pending.end, + }), + _ => None, + }) + .collect(), + }, + warps, + outputs, + } + } + pub fn snapshot(&self) -> SiliconSnapshot { + let active_transfers: Vec<_> = self + .transfers + .iter() + .filter(|t| t.start_tick <= self.tick && t.end_tick > self.tick) + .collect(); + let ready = self + .jobs + .iter() + .filter(|j| matches!(j.stage, Stage::LoadA | Stage::LoadB | Stage::Mac)) + .count(); + let (scheduler_used, scheduler_cap, compute_cap) = match self.config.mode { + Mode::Cpu => (ready, CELLS, self.config.issue_width as usize), + Mode::Gpu => ( + self.warps + .iter() + .filter(|w| matches!(w.stage, Stage::LoadA | Stage::Mac)) + .count(), + 2, + 2, + ), + Mode::Tensor => ( + usize::from( + self.tiles + .iter() + .any(|t| matches!(t.stage, Stage::LoadA | Stage::Mac)), + ), + 1, + 1, + ), + }; + let mut nodes: Vec<_> = [ + ("memory", self.memory_inflight(), MEMORY_SLOTS), + ("cache", self.cache.len(), CACHE_LINES), + ("scheduler", scheduler_used, scheduler_cap), + ( + "registers", + self.memory_inflight() + self.compute_inflight(), + MEMORY_SLOTS + compute_cap, + ), + ("compute", self.compute_inflight(), compute_cap), + ( + "scratch", + if self.config.mode == Mode::Tensor && self.compute_inflight() > 0 { + 64 + } else { + 0 + }, + 64, + ), + ("output", self.counters.completed_outputs as usize, CELLS), + ] + .into_iter() + .map(|(id, occupancy, capacity)| { + let active = active_transfers.iter().any(|t| t.from == id || t.to == id) + || (id == "compute" && occupancy > 0); + NodeState { + id: id.into(), + state: if active { "busy" } else { "idle" }, + occupancy, + capacity, + } + }) + .collect(); + let mut lanes = Vec::new(); + // Prefer an actually executing group over a just-issued memory request. + // The exposed group_index makes this one-group view unambiguous. + let group_index = match self.config.mode { + Mode::Cpu => None, + Mode::Gpu => self + .warps + .iter() + .position(|w| w.stage == Stage::WaitingMac) + .or(self.selected_group), + Mode::Tensor => self.selected_group, + }; + if self.config.mode == Mode::Gpu { + let warp = group_index.unwrap_or(0); + let active = self.warps[warp].stage == Stage::WaitingMac; + for lane in 0..32 { + lanes.push(LaneState { + lane, + active, + output_index: Some(warp * 32 + lane), + }); + nodes.push(NodeState { + id: format!("lane{lane}"), + state: if active { "busy" } else { "idle" }, + occupancy: usize::from(active), + capacity: 1, + }); + } + } + if self.config.mode == Mode::Tensor { + let tile = self.selected_group.unwrap_or(0); + let in_flight = self.pending.iter().find_map(|p| match &p.action { + Completion::TileMac { tile: t, a, b } if *t == tile => Some((a, b)), + _ => None, + }); + for (pe, id) in Self::tile_ids(tile).into_iter().enumerate() { + let active = in_flight.is_some_and(|(a, b)| a[pe].is_some() && b[pe].is_some()); + lanes.push(LaneState { + lane: pe, + active, + output_index: Some(id), + }); + nodes.push(NodeState { + id: format!("pe{pe}"), + state: if active { "busy" } else { "idle" }, + occupancy: usize::from(active), + capacity: 1, + }); + } + } + SiliconSnapshot { + tick: self.tick, + status: self.status, + mode: self.config.mode, + input: self.config.input.clone(), + weights: self.weights.clone(), + output: self.output.clone(), + valid: self.valid.clone(), + nodes, + transfers: self.transfers.iter().cloned().collect(), + lanes, + group_index, + counters: self.counters.clone(), + config: self.config.clone(), + detail: self.execution_detail(), + rules: Rules { + label: "Educational dataflow model; no vendor cycle or power prediction", + version: 1, + time_unit: "model tick", + cpu_hit_ticks: HIT_TICKS, + cpu_miss_ticks: MISS_TICKS, + mac_ticks: MAC_TICKS, + memory_slots: MEMORY_SLOTS, + cpu_cache_line_bytes: 64, + cpu_cache_lines: CACHE_LINES, + cpu_cache_policy: + "fully associative true LRU; cold start; no prefetch; merged misses; miss return and cache fill complete together", + gpu_sector_bytes: 32, + transfer_base_ticks: 2, + sectors_per_transfer_tick: 4, + gpu_warp_lanes: 32, + tensor_tile_side: 4, + tensor_mac_ticks: 1, + tensor_wave_ticks: 14, + memory_slot_meaning: "Outstanding operand requests; merged CPU requests still use a slot", + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn reference(config: &SiliconConfig) -> Vec { + let b = weights(config.filter); + (0..64) + .map(|id| { + (0..8) + .map(|k| config.input[id / 8 * 8 + k] * b[k * 8 + id % 8]) + .sum() + }) + .collect() + } + fn run(config: SiliconConfig) -> SiliconModel { + let mut m = SiliconModel::new(config).unwrap(); + m.run_to_completion(20_000).unwrap(); + m + } + #[test] + fn all_modes_layouts_and_filters_compute_exact_same_matrix() { + for mode in [Mode::Cpu, Mode::Gpu, Mode::Tensor] { + for layout in [Layout::Packed, Layout::Strided] { + for filter in [Filter::Difference, Filter::Identity, Filter::Mix] { + let config = SiliconConfig { + mode, + layout, + filter, + input: (0..64).map(|i| i % 17 - 8).collect(), + ..Default::default() + }; + let expected = reference(&config); + let m = run(config); + assert_eq!(m.output, expected); + assert!(m.valid.iter().all(|v| *v)); + assert_eq!(m.counters.macs, 512); + assert_eq!(m.counters.completed_outputs, 64); + } + } + } + } + #[test] + fn semantic_filters_have_independently_checked_results() { + let input: Vec = (0..64).map(|i| i % 8).collect(); + let m = run(SiliconConfig { + filter: Filter::Difference, + input: input.clone(), + ..Default::default() + }); + for row in 0..8 { + assert_eq!(&m.output[row * 8..row * 8 + 8], &[0, 1, 1, 1, 1, 1, 1, 1]); + } + let mixed = run(SiliconConfig { + filter: Filter::Mix, + input: vec![1; 64], + ..Default::default() + }); + for row in 0..8 { + assert_eq!( + &mixed.output[row * 8..row * 8 + 8], + &[3, 4, 4, 4, 4, 4, 4, 3] + ); + } + assert_eq!( + run(SiliconConfig { + filter: Filter::Identity, + input: input.clone(), + ..Default::default() + }) + .output, + input + ); + } + #[test] + fn coalescing_uses_addresses_masks_and_access_ranges() { + let on = vec![true; 32]; + let addresses: Vec<_> = (0..32).map(|i| i * 4).collect(); + assert_eq!(coalesced_sectors(&addresses, &on, 4).unwrap().len(), 4); + assert_eq!( + coalesced_sectors(&addresses.iter().map(|a| a + 4).collect::>(), &on, 4) + .unwrap() + .len(), + 5 + ); + assert_eq!( + coalesced_sectors(&(0..32).map(|i| i * 32).collect::>(), &on, 4) + .unwrap() + .len(), + 32 + ); + assert_eq!(coalesced_sectors(&[0; 32], &on, 4).unwrap().len(), 1); + assert!(coalesced_sectors(&addresses, &[false; 32], 4) + .unwrap() + .is_empty()); + assert_eq!(coalesced_sectors(&[30], &[true], 4).unwrap(), vec![0, 1]); + assert!(coalesced_sectors(&[u64::MAX], &[true], 4).is_err()); + assert!(coalesced_sectors(&[0], &[], 4).is_err()); + } + #[test] + fn gpu_layout_changes_real_traffic_but_not_result() { + let packed = run(SiliconConfig { + mode: Mode::Gpu, + ..Default::default() + }); + let strided = run(SiliconConfig { + mode: Mode::Gpu, + layout: Layout::Strided, + ..Default::default() + }); + assert_eq!(packed.output, strided.output); + assert!(strided.counters.sectors > packed.counters.sectors); + assert!(strided.tick > packed.tick); + assert_eq!(packed.counters.active_lane_ops, 512); + assert_eq!(packed.counters.issued_lane_slots, 512); + } + #[test] + fn cache_hits_and_pending_merges_are_real_and_retain_results() { + let cached = run(SiliconConfig::default()); + let uncached = run(SiliconConfig { + cache_enabled: false, + ..Default::default() + }); + assert_eq!(cached.output, uncached.output); + assert!(cached.counters.cache_hits > 0); + assert!(cached.counters.merged_misses > 0); + assert_eq!( + cached.counters.loads, + cached.counters.cache_hits + + cached.counters.cache_misses + + cached.counters.merged_misses + ); + assert_eq!(uncached.counters.cache_hits, 0); + assert!(cached.tick < uncached.tick); + } + #[test] + fn tile_reuse_reduces_operand_loads() { + let gpu = run(SiliconConfig { + mode: Mode::Gpu, + ..Default::default() + }); + let tensor = run(SiliconConfig { + mode: Mode::Tensor, + ..Default::default() + }); + assert_eq!(gpu.output, tensor.output); + assert_eq!(gpu.counters.loads, 1024); + assert_eq!(tensor.counters.loads, 256); + } + #[test] + fn completed_output_arrives_after_transfer_and_nodes_obey_capacities() { + for mode in [Mode::Cpu, Mode::Gpu, Mode::Tensor] { + let mut m = SiliconModel::new(SiliconConfig { + mode, + ..Default::default() + }) + .unwrap(); + while m.status != RunStatus::Complete { + m.step(1).unwrap(); + let s = m.snapshot(); + for n in &s.nodes { + assert!(n.occupancy <= n.capacity, "{} occupancy", n.id); + } + for p in &m.pending { + if let Completion::Store { values } = &p.action { + for (id, _) in values { + assert!(!s.valid[*id]); + } + } + } + if mode == Mode::Gpu && m.compute_inflight() > 0 { + assert!(s.group_index.is_some()); + assert!(s.lanes.iter().any(|l| l.active)); + } + } + assert!(m.transfers.iter().all(|t| t.end_tick <= m.tick)); + assert!(m.snapshot().nodes.iter().all(|n| n.state == "idle")); + } + } + #[test] + fn issue_width_changes_scheduling_not_math() { + let narrow = run(SiliconConfig { + issue_width: 1, + ..Default::default() + }); + let wide = run(SiliconConfig { + issue_width: 2, + ..Default::default() + }); + assert_eq!(narrow.output, wide.output); + assert_ne!(narrow.tick, wide.tick); + } + #[test] + fn systolic_wave_moves_matching_operands_only_between_neighbors() { + let mut m = SiliconModel::new(SiliconConfig { + mode: Mode::Tensor, + ..Default::default() + }) + .unwrap(); + let mut wave_counts = Vec::new(); + let mut last_event = 0; + let mut neighbor_moves = 0; + while m.status != RunStatus::Complete { + m.step(1).unwrap(); + for p in &m.pending { + if let Completion::TileMac { tile, a, b } = &p.action { + let wave = m.tiles[*tile].k; + let mut count = 0; + for pe in 0..16 { + let row = pe / 4; + let col = pe % 4; + let expected = wave.checked_sub(row + col).filter(|k| *k < 8); + assert_eq!(a[pe].map(|v| v.0), expected); + assert_eq!(b[pe].map(|v| v.0), expected); + if col > 0 { + assert_eq!(a[pe], m.pe_a[pe - 1]); + } + if row > 0 { + assert_eq!(b[pe], m.pe_b[pe - 4]); + } + if expected.is_some() { + count += 1; + } + } + wave_counts.push(count); + } + } + for event in m.transfers.iter().filter(|t| t.id > last_event) { + if event.from.starts_with("pe") && event.to.starts_with("pe") { + let from: usize = event.from[2..].parse().unwrap(); + let to: usize = event.to[2..].parse().unwrap(); + assert!((to == from + 1 && from / 4 == to / 4) || to == from + 4); + assert_eq!(event.end_tick - event.start_tick, 1); + assert!(event.value.is_some()); + neighbor_moves += 1; + } + } + last_event = m.next_transfer; + } + assert_eq!( + wave_counts, + [1, 3, 6, 10, 13, 15, 16, 16, 15, 13, 10, 6, 3, 1].repeat(4) + ); + assert_eq!(neighbor_moves, 4 * (12 + 12) * 8); + assert_eq!(m.counters.macs, 512); + assert_eq!(m.counters.loads, 256); + } + #[test] + fn cache_is_bounded_lru_not_fifo() { + let mut m = SiliconModel::new(Default::default()).unwrap(); + for line in 0..4 { + m.fill_cache(line); + } + assert!(m.cache_hit(0)); + m.fill_cache(4); + assert!(!m.cache_hit(1)); + assert!(m.cache_hit(0)); + assert_eq!(m.cache.len(), 4); + let cache = m.snapshot().detail.cache; + assert_eq!( + cache + .lru_to_mru + .iter() + .map(|line| line.tag) + .collect::>(), + vec![2, 3, 4, 0] + ); + assert!(cache + .lru_to_mru + .iter() + .all(|line| line.base_address == line.tag * 64)); + } + #[test] + fn cpu_detail_exposes_operands_only_after_matching_load_completes() { + let mut model = SiliconModel::new(Default::default()).unwrap(); + model.step(1).unwrap(); + let first = model.snapshot(); + let job = &first.detail.cpu_jobs[0]; + assert_eq!(job.stage, "waiting_a"); + assert_eq!((job.a, job.b), (None, None)); + assert_eq!(job.ready_tick, Some(8)); + assert_eq!(job.a_address, Some(0)); + assert_eq!(job.b_address, Some(4096)); + assert!(first.detail.cache.lru_to_mru.is_empty()); + assert_eq!(first.detail.cache.pending_fills.len(), 1); + assert_eq!(first.detail.cache.pending_fills[0].arrival_tick, 8); + model.step(6).unwrap(); + assert_eq!(model.snapshot().detail.cpu_jobs[0].a, None); + model.step(1).unwrap(); + let arrived = model.snapshot(); + assert_eq!(arrived.detail.cpu_jobs[0].a, Some(model.config.input[0])); + assert_eq!(arrived.detail.cpu_jobs[0].b, None); + assert_eq!(arrived.detail.cache.lru_to_mru[0].tag, 0); + assert!(arrived.detail.outputs[0].terms.is_empty()); + // A/B registers retain old bits internally, but the next k must not + // present them as its operands before new loads complete. + loop { + model.step(1).unwrap(); + let detail = model.snapshot().detail; + if detail.cpu_jobs[0].k == 1 { + assert_eq!(detail.cpu_jobs[0].a, None); + assert_eq!(detail.cpu_jobs[0].b, None); + assert_eq!(detail.outputs[0].terms.len(), 1); + assert_eq!(detail.outputs[0].terms[0].retired_tick, model.tick); + break; + } + assert!(model.tick < 1000); + } + } + #[test] + fn gpu_detail_reports_requested_sectors_and_actual_arrival_not_future_values() { + for layout in [Layout::Packed, Layout::Strided] { + let mut model = SiliconModel::new(SiliconConfig { + mode: Mode::Gpu, + layout, + ..Default::default() + }) + .unwrap(); + let initial = model.snapshot(); + assert!(initial + .detail + .warps + .iter() + .all(|warp| warp.requested_a_sectors.is_empty() + && warp.requested_b_sectors.is_empty())); + model.step(1).unwrap(); + let sent = model.snapshot(); + let warp = &sent.detail.warps[0]; + assert_eq!(warp.stage, "waiting_operands"); + assert_eq!(warp.requested_a_sectors, vec![0, 1, 2, 3]); + assert_eq!( + warp.requested_b_sectors, + if layout == Layout::Packed { + vec![128] + } else { + (128..136).collect() + } + ); + assert_eq!( + sent.counters.sectors as usize, + warp.requested_a_sectors.len() + warp.requested_b_sectors.len() + ); + assert_eq!(sent.transfers[0].bytes, sent.counters.sectors as u32 * 32); + for lane in &warp.lanes { + assert_eq!(lane.a, None); + assert_eq!(lane.b, None); + assert_eq!(lane.a_address, Some((lane.output_index / 8 * 32) as u64)); + assert_eq!( + lane.b_address, + Some( + 4096 + (lane.output_index % 8 + * if layout == Layout::Packed { 4 } else { 32 }) + as u64 + ) + ); + } + let arrival = warp.ready_tick.unwrap(); + model.step((arrival - model.tick - 1) as u32).unwrap(); + assert!(model.snapshot().detail.warps[0] + .lanes + .iter() + .all(|lane| lane.a.is_none() && lane.b.is_none())); + model.step(1).unwrap(); + let ready = model.snapshot(); + for lane in &ready.detail.warps[0].lanes { + assert_eq!(lane.a, Some(model.config.input[lane.output_index / 8 * 8])); + assert_eq!(lane.b, Some(model.weights[lane.output_index % 8])); + assert_eq!(lane.accumulated_sum, 0); + assert_eq!(lane.retired_terms, 0); + } + assert_eq!(ready.counters.macs, 0); + } + } + #[test] + fn retired_provenance_is_bounded_monotonic_and_tracks_completed_macs_in_every_model() { + for mode in [Mode::Cpu, Mode::Gpu, Mode::Tensor] { + let mut model = SiliconModel::new(SiliconConfig { + mode, + input: (0..64).map(|i| (i * 7) % 17 - 8).collect(), + filter: Filter::Mix, + ..Default::default() + }) + .unwrap(); + let expected = reference(model.config()); + let mut previous: Vec> = vec![Vec::new(); 64]; + loop { + let snapshot = model.snapshot(); + assert_eq!(snapshot.detail.outputs.len(), 64); + assert_eq!( + snapshot + .detail + .outputs + .iter() + .map(|output| output.terms.len()) + .sum::() as u64, + snapshot.counters.macs + ); + for output in &snapshot.detail.outputs { + let id = output.output_index; + assert!(output.terms.len() <= 8); + assert_eq!(output.retired_terms, output.terms.len()); + assert_eq!(output.reference_sum, expected[id]); + assert!(output.terms.starts_with(&previous[id])); + let mut sum = 0; + for (k, term) in output.terms.iter().enumerate() { + assert_eq!(term.k, k); + assert_eq!(term.a, snapshot.input[id / 8 * 8 + k]); + assert_eq!(term.b, snapshot.weights[k * 8 + id % 8]); + assert_eq!(term.product, term.a * term.b); + sum += term.product; + assert_eq!(term.sum_after, sum); + assert!(term.retired_tick > 0 && term.retired_tick <= snapshot.tick); + } + assert_eq!(output.accumulated_sum, sum); + assert_eq!(output.stored, snapshot.valid[id]); + if output.stored { + assert_eq!(output.retired_terms, 8); + assert_eq!(sum, snapshot.output[id]); + assert!( + output.terms.last().unwrap().retired_tick < snapshot.tick, + "Output cannot be stored in the same tick as its last MAC" + ); + } + previous[id] = output.terms.clone(); + } + if snapshot.status == RunStatus::Complete { + break; + } + model.step(1).unwrap(); + assert!(model.tick < 20_000); + } + model.reset(); + assert!(model + .snapshot() + .detail + .outputs + .iter() + .all(|output| output.terms.is_empty() + && output.accumulated_sum == 0 + && !output.stored)); + } + } + #[test] + fn inputs_and_tick_batches_reject_without_mutating() { + assert!(SiliconModel::new(SiliconConfig { + input: vec![0; 63], + ..Default::default() + }) + .is_err()); + assert!(SiliconModel::new(SiliconConfig { + input: vec![9; 64], + ..Default::default() + }) + .is_err()); + for issue_width in [0, 3, 255] { + assert!(SiliconModel::new(SiliconConfig { + issue_width, + ..Default::default() + }) + .is_err()); + } + let mut m = SiliconModel::new(Default::default()).unwrap(); + assert!(m.step(257).is_err()); + assert_eq!(m.tick, 0); + assert_eq!(m.status, RunStatus::Ready); + m.step(0).unwrap(); + assert_eq!(m.tick, 0); + assert!(m.run_to_completion(100_001).is_err()); + assert_eq!(m.tick, 0); + } + #[test] + fn batches_reset_and_finished_steps_are_deterministic() { + for mode in [Mode::Cpu, Mode::Gpu, Mode::Tensor] { + let mut a = SiliconModel::new(SiliconConfig { + mode, + ..Default::default() + }) + .unwrap(); + let mut b = a.clone(); + a.step(256).unwrap(); + for _ in 0..256 { + b.step(1).unwrap(); + } + assert_eq!(a.tick, b.tick); + assert_eq!(a.counters, b.counters); + assert_eq!(a.output, b.output); + assert_eq!(a.transfers, b.transfers); + a.reset(); + let c = run(a.config.clone()); + a.run_to_completion(20_000).unwrap(); + assert_eq!(a.tick, c.tick); + assert_eq!(a.counters, c.counters); + assert_eq!(a.output, c.output); + let tick = a.tick; + a.step(256).unwrap(); + assert_eq!(a.tick, tick); + } + } + #[test] + fn all_public_activity_is_bounded_and_dependencies_are_respected() { + let mut m = SiliconModel::new(Default::default()).unwrap(); + while m.status != RunStatus::Complete { + m.step(1).unwrap(); + assert!(m.memory_inflight() <= MEMORY_SLOTS); + assert!(m.compute_inflight() <= m.config.issue_width as usize); + assert!(m.transfers.len() <= TRANSFER_HISTORY); + for job in &m.jobs { + assert!(job.k <= 8); + if job.stage == Stage::Done { + assert_eq!(job.k, 8); + } + } + for t in &m.transfers { + assert!(t.end_tick > t.start_tick); + } + assert!(m.tick < 20_000); + } + } +} diff --git a/crates/doke-web/src/brain.wgsl b/crates/doke-web/src/brain.wgsl index db2d359..2b3bf34 100644 --- a/crates/doke-web/src/brain.wgsl +++ b/crates/doke-web/src/brain.wgsl @@ -1,4 +1,4 @@ -struct Uniforms { vp:mat4x4,eye:vec4,params:vec4,flags:vec4,activity:array,64>,detail:vec4,style:vec4 }; +struct Uniforms { vp:mat4x4,eye:vec4,params:vec4,flags:vec4,activity:array,256>,detail:vec4,style:vec4,flow_from:array,32>,flow_to:array,32> }; @group(0) @binding(0) var u:Uniforms; struct VertexOut { @builtin(position) position:vec4, @location(0) world:vec3,@location(1) normal:vec3,@location(2) @interpolate(flat) region:u32,@location(3) @interpolate(flat) kind:u32 }; fn neuron(i:u32)->vec3{let a=f32(i%6u)*6.283185/6.;return vec3(select(0.78,-0.78,i<6u),cos(a)*0.65,sin(a)*0.65);} @@ -23,6 +23,7 @@ fn lens()->vec3{ if(k==6.){let t=fract(time*0.7+r*0.13);pos+=vec3((f32(id%4u)-1.5)*0.38,0.68-t*0.22,0.18);} if(k==8.){let t=fract(time*0.35+r*0.17);let x=(f32(id%6u)-2.5)*0.27;pos+=vec3(x+select(0.,sin(t*4.+r)*0.1,u.activity[60].w==0.),-0.42-t*0.24,0.47);} if(k==11.){let t=fract(time*0.65+f32(id%3u)/3.);let drift=vec3(0.012*sin(t*3.14159),-t*0.065,t*0.070);pos+=vec3(-drift.z,drift.y,drift.x);} + if(k==16.){pos+=mix(u.flow_from[min(id,31u)].xyz,u.flow_to[min(id,31u)].xyz,u.flow_to[min(id,31u)].w);} var o:VertexOut;o.position=u.vp*vec4(pos,1.);o.world=pos;o.normal=n;o.region=id;o.kind=u32(k); if(k==14.){let aperture=lens();o.position=vec4(aperture.xy+p.xy*vec2(aperture.z/u.style.y,aperture.z),0.99999,1.);o.world=u.detail.xyz;o.normal=p;} return o; @@ -35,6 +36,7 @@ fn shade(v:VertexOut)->FragmentOut { } if(v.kind==14u){var o:FragmentOut;let edge=smoothstep(0.975,0.997,length(v.normal.xy));o.color=vec4(vec3(0.018+edge*0.12),1.);o.glow=vec4(vec3(edge*0.02),1.);return o;} if(v.kind==2u){discard;} + if(v.kind==16u&&u.flow_from[min(v.region,31u)].w<0.5){discard;} if(v.kind==0u&&u.flags.y==1.){discard;} if((u.flags.y==2.||u.params.y==2.)&&v.world.x>0.){discard;} if((u.flags.y==3.||u.params.y==3.)&&v.world.x<0.){discard;} @@ -47,7 +49,7 @@ fn shade(v:VertexOut)->FragmentOut { let rim=pow(1.-abs(dot(n,view)),2.8); let spec=pow(max(dot(n,normalize(view+normalize(vec3(-0.4,0.85,0.7)))),0.),48.); let selected=select(0.,1.,abs(f32(v.region)-u.flags.x)<0.1); - var a=u.activity[min(v.region,63u)].x; + var a=u.activity[min(v.region,255u)].x; if(u.params.z==1.){a=select(0.06,0.55,v.region==u32(u.params.w)+2u);} if(u.params.z==3.){a=0.22;} let particle=(v.kind>=4u&&v.kind<=8u)||v.kind==11u; @@ -66,7 +68,12 @@ fn shade(v:VertexOut)->FragmentOut { if(v.kind==13u){tint=vec3(0.42,0.65,1.);} if(u.params.z==1.&&(v.kind==4u||v.kind==7u)){tint=vec3(1.,0.79,0.50);if(u.activity[60].w==1.){tint=vec3(0.58,0.90,0.78);}if(u.activity[60].w==2.){tint=vec3(0.79,0.69,1.);}} } + if(v.kind==15u){light=0.09+lambert*0.26+rim*0.15+spec*0.10+a*0.65+selected*0.2;emissive=a*0.7+selected*0.18;if(u.style.x>0.5){tint=mix(vec3(0.9,0.96,1.),vec3(0.56,0.9,0.92),a);}} + if(v.kind==16u){light=1.8;emissive=2.6;tint=vec3(0.72,0.94,1.);} + if(v.kind==17u){light=0.14+lambert*0.09;emissive=0.012;} + if(v.kind==19u){light=0.15+lambert*0.28+fill*0.08+spec*0.28+selected*0.08;emissive=0.;tint=vec3(0.72,0.82,0.86);} + if(v.kind==18u){light=0.018+lambert*0.03+rim*0.065;emissive=0.;} var out:FragmentOut;out.color=vec4(tint*light,1.);out.glow=vec4(tint*emissive,1.);return out; } @fragment fn fs(v:VertexOut)->FragmentOut{return shade(v);} -@fragment fn fs_focus(v:VertexOut)->FragmentOut{if(u.activity[min(v.region,63u)].x<=0.01&&abs(f32(v.region)-u.flags.x)>0.1){discard;}return shade(v);} +@fragment fn fs_focus(v:VertexOut)->FragmentOut{if(u.activity[min(v.region,255u)].x<=0.01&&abs(f32(v.region)-u.flags.x)>0.1){discard;}return shade(v);} diff --git a/crates/doke-web/src/chip.rs b/crates/doke-web/src/chip.rs new file mode 100644 index 0000000..f049b42 --- /dev/null +++ b/crates/doke-web/src/chip.rs @@ -0,0 +1,343 @@ +//! Original schematic hardware geometry; never a vendor floorplan. +use crate::{scenes::Mesh, Vertex}; +use glam::Vec3; +use serde_json::Value; + +const BOARD: u32 = 255; +const DETAIL: u32 = 19; + +fn vector(v: &Value) -> Result { + let a = v.as_array().ok_or("Expected vector")?; + if a.len() != 3 { + return Err("Expected three coordinates"); + } + let mut p = [0.; 3]; + for (i, x) in a.iter().enumerate() { + let n = x.as_f64().ok_or("Expected finite coordinate")?; + if !n.is_finite() || n.abs() > 8. { + return Err("Coordinate outside schematic"); + } + p[i] = n as f32; + } + Ok(Vec3::from(p)) +} + +fn dimensions(v: &Value) -> Result { + let size = vector(v)?; + if size.min_element() < 0.0001 { + return Err("Size must be positive and nondegenerate"); + } + Ok(size) +} + +fn face(m: &mut Mesh, points: &[Vec3], region: u32, kind: u32) { + let normal = (points[1] - points[0]) + .cross(points[2] - points[0]) + .normalize(); + let base = m.v.len() as u32; + for p in points { + m.v.push(Vertex { + p: p.to_array(), + n: normal.to_array(), + region: region as f32, + kind: kind as f32, + }); + } + for i in 1..points.len() - 1 { + m.i.extend_from_slice(&[base, base + i as u32, base + i as u32 + 1]); + } +} + +fn cuboid(m: &mut Mesh, c: Vec3, size: Vec3, region: u32, kind: u32) { + for (normal, u, v) in [ + (Vec3::X, Vec3::Y, Vec3::Z), + (-Vec3::X, Vec3::Z, Vec3::Y), + (Vec3::Y, Vec3::Z, Vec3::X), + (-Vec3::Y, Vec3::X, Vec3::Z), + (Vec3::Z, Vec3::X, Vec3::Y), + (-Vec3::Z, Vec3::Y, Vec3::X), + ] { + let p: Vec<_> = [(-1., -1.), (1., -1.), (1., 1.), (-1., 1.)] + .iter() + .map(|(a, b)| c + (normal + u * *a + v * *b) * size * 0.5) + .collect(); + face(m, &p, region, kind); + } +} + +/// Eight clipped corners and a true sloped top lip catch light at every scale. +fn die(m: &mut Mesh, c: Vec3, size: Vec3, region: u32, kind: u32) { + let hx = size.x * 0.5; + let hz = size.z * 0.5; + let bevel = size.x.min(size.z).min(size.y * 2.) * 0.09; + let corner = hx.min(hz) * 0.16; + let ring = |inset: f32, y: f32| { + let x = hx - inset; + let z = hz - inset; + let cut = corner * (1. - inset / hx.min(hz)); + [ + (-x + cut, -z), + (x - cut, -z), + (x, -z + cut), + (x, z - cut), + (x - cut, z), + (-x + cut, z), + (-x, z - cut), + (-x, -z + cut), + ] + .map(|(x, z)| c + Vec3::new(x, y, z)) + }; + let bottom = ring(0., -size.y * 0.5); + let shoulder = ring(0., size.y * 0.5 - bevel); + let top = ring(bevel, size.y * 0.5); + for i in 0..8 { + let j = (i + 1) % 8; + face( + m, + &[bottom[i], shoulder[i], shoulder[j], bottom[j]], + region, + kind, + ); + face(m, &[shoulder[i], top[i], top[j], shoulder[j]], region, kind); + } + let reverse: Vec<_> = top.iter().rev().copied().collect(); + face(m, &reverse, region, kind); + face(m, &bottom, region, kind); +} + +/// Raised physical traces, with no activity or time animation of their own. +fn etch(m: &mut Mesh, p: Vec3, size: Vec3, region: u32, style: &str) { + let stroke = size.x.min(size.z).min(0.5) * 0.012; + let top = p.y + size.y * 0.5 + stroke * 1.5; + let rows = if style == "memory" { 9 } else { 5 }; + for row in 0..rows { + let z = p.z + ((row as f32 + 0.5) / rows as f32 - 0.5) * size.z * 0.76; + if style == "compute" { + for col in 0..4 { + let x = p.x + (col as f32 - 1.5) * size.x * 0.19; + cuboid( + m, + Vec3::new(x, top, z), + Vec3::new(size.x * 0.12, stroke, stroke), + region, + DETAIL, + ); + } + } else { + let width = size.x + * if style == "control" && row % 2 == 0 { + 0.45 + } else { + 0.76 + }; + cuboid( + m, + Vec3::new(p.x, top, z), + Vec3::new(width, stroke, stroke), + region, + DETAIL, + ); + } + } + if style == "control" || style == "io" { + cuboid( + m, + Vec3::new(p.x + size.x * 0.3, top, p.z), + Vec3::new(stroke, stroke, size.z * 0.78), + region, + DETAIL, + ); + } +} + +fn path(v: &Value) -> Result, &'static str> { + let values = v.as_array().ok_or("Missing route points")?; + if !(2..=32).contains(&values.len()) { + return Err("Route needs 2 to 32 points"); + } + let points: Vec<_> = values.iter().map(vector).collect::>()?; + if points.windows(2).any(|p| p[0].distance(p[1]) < 0.0001) { + return Err("Degenerate route segment"); + } + Ok(points) +} + +pub fn mesh(json: &str) -> Result { + if json.len() > 512 * 1024 { + return Err("Schematic too large"); + } + let data: Value = serde_json::from_str(json).map_err(|_| "Invalid schematic")?; + let blocks = data["blocks"].as_array().ok_or("Missing blocks")?; + let links = data["links"].as_array().ok_or("Missing links")?; + let parts = data + .get("parts") + .map(|p| p.as_array().ok_or("Invalid parts")) + .transpose()?; + if blocks.len() > 240 || links.len() > 512 || parts.is_some_and(|p| p.len() > 2048) { + return Err("Schematic exceeds bounds"); + } + let mut regions = [false; 240]; + let mut m = Mesh::new(); + let (board, size) = if let Some(b) = data.get("board") { + let p = b + .get("position") + .map(vector) + .transpose()? + .unwrap_or(Vec3::new(0., -0.14, 0.)); + (p, dimensions(&b["size"])?) + } else { + (Vec3::new(0., -0.14, 0.), Vec3::new(4.1, 0.12, 3.7)) + }; + die(&mut m, board, size, BOARD, 18); + die( + &mut m, + board - Vec3::Y * (size.y * 0.5 + 0.04), + Vec3::new(size.x + 0.22, 0.06, size.z + 0.22), + BOARD, + 18, + ); + // Original package contacts frame the object; this is not a physical pin count. + for n in 0..32 { + let t = (n as f32 + 0.5) / 32. - 0.5; + for sign in [-1., 1.] { + cuboid( + &mut m, + board + + Vec3::new( + t * size.x * 0.94, + -size.y * 0.5 - 0.045, + sign * (size.z * 0.5 + 0.09), + ), + Vec3::new(size.x * 0.012, 0.025, 0.12), + BOARD, + 17, + ); + cuboid( + &mut m, + board + + Vec3::new( + sign * (size.x * 0.5 + 0.09), + -size.y * 0.5 - 0.045, + t * size.z * 0.94, + ), + Vec3::new(0.12, 0.025, size.z * 0.012), + BOARD, + 17, + ); + } + } + for b in blocks { + let r = b["region"].as_u64().ok_or("Missing region")?; + if r >= 240 || regions[r as usize] { + return Err("Block regions must be unique and below 240"); + } + regions[r as usize] = true; + let p = vector(&b["position"])?; + let size = dimensions(&b["size"])?; + let style = b + .get("style") + .map(|v| v.as_str().ok_or("Invalid block style")) + .transpose()? + .unwrap_or("control"); + if !["compute", "memory", "control", "io"].contains(&style) { + return Err("Unknown block style"); + } + die(&mut m, p, size, r as u32, 15); + etch(&mut m, p, size, r as u32, style); + } + if let Some(parts) = parts { + for part in parts { + let r = part["parent"].as_u64().ok_or("Missing part parent")?; + if r >= 240 || !regions[r as usize] { + return Err("Part parent must identify a block"); + } + match part["type"].as_str() { + Some("cuboid") => cuboid( + &mut m, + vector(&part["position"])?, + dimensions(&part["size"])?, + r as u32, + DETAIL, + ), + Some("tube") => { + let radius = part["radius"].as_f64().ok_or("Missing part radius")?; + if !radius.is_finite() || !(0.0001..=0.08).contains(&radius) { + return Err("Part radius outside bounds"); + } + m.tube(&path(&part["points"])?, radius as f32, r as u32, DETAIL); + } + _ => return Err("Unknown part type"), + } + } + } + for link in links { + let points = if let Some(p) = link.get("points") { + path(p)? + } else { + let a = vector(&link["from"])?; + let b = vector(&link["to"])?; + if a.distance(b) < 0.0001 { + continue; + } + // Keep routes in the endpoint plane instead of diving through the substrate. + let mid = Vec3::new(a.x, (a.y + b.y) * 0.5, b.z); + if a.distance(mid) < 0.0001 || b.distance(mid) < 0.0001 { + vec![a, b] + } else { + vec![a, mid, b] + } + }; + m.tube(&points, 0.004, BOARD, 17); + } + // Only emitted model events position these markers through uniforms. + for i in 0..32 { + m.sphere(Vec3::ZERO, Vec3::splat(0.025), i, 16, 5); + } + Ok(m) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + fn block(r: usize) -> Value { + json!({"region":r,"position":[0,0.1,0],"size":[0.3,0.1,0.3],"style":"compute"}) + } + #[test] + fn full_region_capacity_and_finite_indexed_geometry() { + let blocks: Vec<_> = (0..240).map(block).collect(); + let m = mesh(&json!({"blocks":blocks,"links":[]}).to_string()).unwrap(); + assert!(m.v.iter().any(|v| v.region == 239.)); + assert!(m.v.iter().any(|v| v.region == BOARD as f32)); + assert!(m.i.iter().all(|i| (*i as usize) < m.v.len())); + assert!(m + .v + .iter() + .all(|v| v.p.iter().chain(v.n.iter()).all(|n| n.is_finite()))); + assert!(m.v.len() < 200_000); + } + #[test] + fn decorative_parts_keep_parent_and_static_material() { + let data = json!({"blocks":[block(170)],"links":[],"parts":[{"type":"cuboid","parent":170,"position":[0,0.3,0],"size":[0.1,0.1,0.1]},{"type":"tube","parent":170,"points":[[0,0.4,0],[1,0.4,0]],"radius":0.01}]}); + let m = mesh(&data.to_string()).unwrap(); + assert!(m + .v + .iter() + .filter(|v| v.kind == DETAIL as f32) + .all(|v| v.region == 170.)); + } + #[test] + fn rejects_ambiguous_or_unbounded_geometry() { + for value in [ + json!({"blocks":[block(240)],"links":[]}), + json!({"blocks":[block(0),block(0)],"links":[]}), + json!({"blocks":[block(0)],"links":[],"parts":[{"parent":1,"type":"cuboid","position":[0,0,0],"size":[1,1,1]}]}), + json!({"blocks":[block(0)],"links":[{"points":[[0,0,0],[0,0,0]]}]}), + json!({"blocks":[{"region":0,"position":[9,0,0],"size":[1,1,1]}],"links":[]}), + json!({"blocks":[{"region":0,"position":[0,0,0],"size":[0,1,1]}],"links":[]}), + ] { + assert!(mesh(&value.to_string()).is_err()); + } + assert!(mesh(&" ".repeat(512 * 1024 + 1)).is_err()); + } +} diff --git a/crates/doke-web/src/dataflow.rs b/crates/doke-web/src/dataflow.rs new file mode 100644 index 0000000..4b05221 --- /dev/null +++ b/crates/doke-web/src/dataflow.rs @@ -0,0 +1,99 @@ +//! JSON snapshot boundary for the bounded educational models. +use doke_core::{ + fabric::{FabricConfig, FabricModel, QueueMode}, + silicon::{SiliconConfig, SiliconModel}, +}; +use wasm_bindgen::prelude::*; + +fn error(message: &str) -> JsValue { + JsValue::from_str(message) +} +fn integer(value: f64, max: u32) -> Result { + if !value.is_finite() || value.fract() != 0. || value < 0. || value > max as f64 { + return Err(error("Expected an integer inside the supported range")); + } + Ok(value as u32) +} +fn mode(value: &str) -> Result { + match value { + "fifo" => Ok(QueueMode::Fifo), + "voq" => Ok(QueueMode::Voq), + _ => Err(error("Unknown queue mode")), + } +} +#[wasm_bindgen] +pub struct SiliconSimulation { + model: SiliconModel, +} +#[wasm_bindgen] +impl SiliconSimulation { + #[wasm_bindgen(constructor)] + pub fn new(json: &str) -> Result { + if json.len() > 8192 { + return Err(error("Configuration too large")); + } + let value: serde_json::Value = + serde_json::from_str(json).map_err(|e| error(&e.to_string()))?; + if !value.is_object() { + return Err(error("Configuration must be a JSON object")); + } + let config: SiliconConfig = + serde_json::from_value(value).map_err(|e| error(&e.to_string()))?; + Ok(Self { + model: SiliconModel::new(config).map_err(error)?, + }) + } + pub fn advance(&mut self, ticks: f64) -> Result { + self.model.step(integer(ticks, 256)?).map_err(error)?; + self.snapshot() + } + pub fn snapshot(&self) -> Result { + serde_json::to_string(&self.model.snapshot()).map_err(|e| error(&e.to_string())) + } + pub fn reset(&mut self) -> Result { + self.model.reset(); + self.snapshot() + } +} +#[wasm_bindgen] +pub struct FabricSimulation { + model: FabricModel, +} +#[wasm_bindgen] +impl FabricSimulation { + #[wasm_bindgen(constructor)] + pub fn new(queue_mode: &str) -> Result { + Ok(Self { + model: FabricModel::with_config(FabricConfig { + queue_mode: mode(queue_mode)?, + }), + }) + } + pub fn advance(&mut self, ticks: f64) -> Result { + self.model.advance(integer(ticks, 4096)?).map_err(error)?; + self.snapshot() + } + pub fn inject(&mut self, egress: f64, count: f64) -> Result { + self.model + .inject_burst(integer(egress, 2)? as usize, integer(count, 64)?) + .map_err(error)?; + self.snapshot() + } + pub fn block(&mut self, egress: f64, blocked: bool) -> Result { + self.model + .set_blocked(integer(egress, 2)? as usize, blocked) + .map_err(error)?; + self.snapshot() + } + pub fn queue_mode(&mut self, value: &str) -> Result { + self.model.set_mode(mode(value)?); + self.snapshot() + } + pub fn snapshot(&self) -> Result { + serde_json::to_string(&self.model.snapshot()).map_err(|e| error(&e.to_string())) + } + pub fn reset(&mut self) -> Result { + self.model.reset(); + self.snapshot() + } +} diff --git a/crates/doke-web/src/lib.rs b/crates/doke-web/src/lib.rs index 9d3e727..0d88747 100644 --- a/crates/doke-web/src/lib.rs +++ b/crates/doke-web/src/lib.rs @@ -1,3 +1,5 @@ +mod chip; +mod dataflow; mod scenes; use bytemuck::{Pod, Zeroable}; use glam::{Mat4, Vec3, Vec4}; @@ -19,9 +21,11 @@ struct Uniform { eye: [f32; 4], params: [f32; 4], flags: [f32; 4], - activity: [[f32; 4]; 64], + activity: [[f32; 4]; 256], detail: [f32; 4], style: [f32; 4], + flow_from: [[f32; 4]; 32], + flow_to: [[f32; 4]; 32], } struct Targets { scene: wgpu::TextureView, @@ -66,6 +70,8 @@ pub struct BrainRenderer { hdr: bool, target_format: wgpu::TextureFormat, } +// This resource factory receives the shared pipelines and bindings together. +#[allow(clippy::too_many_arguments)] fn targets( d: &wgpu::Device, p: &wgpu::RenderPipeline, @@ -192,8 +198,13 @@ impl BrainRenderer { &wgpu::DeviceDescriptor { label: Some("Brain renderer"), required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()), + // The scene writes color and glow; asking for the default eight + // attachments rejects valid WebGL adapters that expose fewer. + required_limits: wgpu::Limits { + max_color_attachments: 2, + ..wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()) + }, memory_hints: wgpu::MemoryHints::MemoryUsage, }, None, @@ -495,6 +506,75 @@ impl BrainRenderer { self.load_mesh(&mesh.bytes())?; Ok(()) } + /// Original functional schematic, sharing the atlas camera and HDR pipeline. + pub fn chip_scene(&mut self, json: &str) -> Result<(), JsValue> { + let mesh = chip::mesh(json).map_err(JsValue::from_str)?; + self.state.params = [0., 0., 4., 0.]; + self.state.flags = [-1., 0., 2., 0.]; + self.state.detail = [0.; 4]; + self.state.activity = [[0.; 4]; 256]; + self.state.flow_from = [[0.; 4]; 32]; + self.state.flow_to = [[0.; 4]; 32]; + self.tissue_buffers = None; + self.target = Vec3::ZERO; + self.yaw = -0.25; + self.pitch = 0.82; + self.distance = 9.5; + self.load_mesh(&mesh.bytes()) + } + /// Camera values for UI-owned easing: distance, yaw, pitch, target x/y/z. + pub fn camera_state(&self) -> Vec { + vec![ + self.distance, + self.yaw, + self.pitch, + self.target.x, + self.target.y, + self.target.z, + ] + } + /// Validated chip camera pose. Call render() after setting a pose before picking. + pub fn chip_camera( + &mut self, + distance: f32, + yaw: f32, + pitch: f32, + x: f32, + y: f32, + z: f32, + ) -> Result<(), JsValue> { + if self.state.params[2] != 4. + || ![distance, yaw, pitch, x, y, z] + .iter() + .all(|v| v.is_finite()) + || !(0.25..=24.).contains(&distance) + || pitch.abs() > 1.45 + || x.abs().max(y.abs()).max(z.abs()) > 8. + { + return Err(JsValue::from_str("Invalid chip camera pose")); + } + self.distance = distance; + self.yaw = yaw; + self.pitch = pitch; + self.target = Vec3::new(x, y, z); + Ok(()) + } + /// Up to 32 real transfer markers: start xyz, end xyz, interpolation fraction. + pub fn chip_flows(&mut self, data: &[f32]) -> Result<(), JsValue> { + if data.len() > 224 + || !data.len().is_multiple_of(7) + || data.iter().any(|x| !x.is_finite() || x.abs() > 8.) + { + return Err(JsValue::from_str("Invalid transfer positions")); + } + self.state.flow_from = [[0.; 4]; 32]; + self.state.flow_to = [[0.; 4]; 32]; + for (i, f) in data.chunks_exact(7).enumerate() { + self.state.flow_from[i] = [f[0], f[1], f[2], 1.]; + self.state.flow_to[i] = [f[3], f[4], f[5], f[6].clamp(0., 1.)]; + } + Ok(()) + } pub fn anatomy(&mut self) { self.state.params[2] = 0.; } @@ -541,7 +621,8 @@ impl BrainRenderer { if i.iter().any(|&x| x as usize >= nv) || v.iter().any(|x| { x.p.iter().chain(x.n.iter()).any(|v| !v.is_finite()) - || !(0.0..64.0).contains(&x.region) + || !(0.0..256.0).contains(&x.region) + || x.region.fract() != 0. }) { return Err(JsValue::from_str("Invalid anatomy geometry")); @@ -591,13 +672,27 @@ impl BrainRenderer { self.pitch = (self.pitch + dy).clamp(-1.45, 1.45); } pub fn zoom(&mut self, delta: f32) { - self.distance = (self.distance * delta.exp()).clamp(0.65, 7.); + self.distance = (self.distance * delta.exp()).clamp( + if self.state.params[2] == 4. { + 0.25 + } else { + 0.65 + }, + if self.state.params[2] == 4. { 24. } else { 7. }, + ); } pub fn pan(&mut self, dx: f32, dy: f32) { let right = Vec3::new(self.yaw.cos(), 0., -self.yaw.sin()); self.target += right * (-dx * self.distance) + Vec3::Y * (dy * self.distance); } pub fn view(&mut self, preset: u32) { + if self.state.params[2] == 4. { + self.target = Vec3::ZERO; + self.distance = 9.5; + self.yaw = -0.25; + self.pitch = 0.82; + return; + } if self.state.params[2] > 0. { let synapse = self.state.params[2] == 1.; self.target = Vec3::new(0., if synapse { 0.4 } else { 0. }, 0.); @@ -665,7 +760,7 @@ impl BrainRenderer { self.bloom = v.clamp(0., 3.); } pub fn activity(&mut self, values: &[f32]) { - for n in 0..64 { + for n in 0..256 { self.state.activity[n][0] = values.get(n).copied().unwrap_or(0.).clamp(0., 1.); } } @@ -690,7 +785,12 @@ impl BrainRenderer { let mut focused_hit = false; for tri in self.cpu_i.chunks_exact(3) { let a = self.cpu_v[tri[0] as usize]; - if a.kind == 2. || (a.kind == 0. && self.state.flags[1] == 1.) { + if a.kind == 2. + || a.kind == 16. + || a.kind == 17. + || a.kind == 18. + || (a.kind == 0. && self.state.flags[1] == 1.) + { continue; } let v0 = Vec3::from(a.p); @@ -751,7 +851,10 @@ impl BrainRenderer { 0.02, 30., ) * Mat4::look_at_rh(self.eye, self.target, Vec3::Y); - if portrait_micro { + if self.state.params[2] == 4. { + let shift = if portrait_micro { -0.03 } else { 0.10 }; + self.matrix = Mat4::from_translation(Vec3::new(0., shift, 0.)) * self.matrix; + } else if portrait_micro { self.matrix = Mat4::from_translation(Vec3::new(0., 0.14, 0.)) * self.matrix; } self.state.vp = self.matrix.to_cols_array_2d(); @@ -915,6 +1018,7 @@ pub fn envelope(time: f32, start: f32, end: f32) -> f32 { doke_core::envelope(time, start, end) } #[wasm_bindgen] +#[derive(Default)] pub struct Circuit { state: doke_core::Model, } diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 7428ba4..06b9e91 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -1,43 +1,30 @@ -# The doke guide - -This is the guide to using, developing and hosting doke. +# doke guide ## Explore -The top navigation chooses **Brain**, **Synapse**, **Neural circuit** or **Messengers**. Each scene has its own lower dock. Drag to orbit, scroll or pinch to zoom, Shift-drag or right-drag to pan. Keyboard arrows rotate, +/− zoom, and Home resets the view. The ⋯ button opens display and camera tools. - -| Scene | Lower controls | What to look for | -| -------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Brain | Anatomy, Activities, Neurons | Select a region or a study-led story. Neurons opens a small teaching window; Focus the cells zooms into it. Toggle Neurons again to restore the surface. | -| Synapse | Play/pause, four stages, Focus cleft, Inspect messenger, explanation | Follow packaging, release, receptor action and clearance. Selecting a stage starts that stage's animation clock at zero. Pausing freezes it; playing resumes immediately. | -| Neural circuit | Play/pause, Send a signal, Tune the network, model evidence | See the interaction of excitatory and inhibitory populations. Drive and coupling controls change the numerical model. | -| Messengers | Dopamine, glutamate's conjugate acid, GABA, explanation | Rotate sourced molecular conformers. Oxygen has a muted warm accent; nitrogen a blue accent. Atom letters remain available without color. | - -All explanations use the floating panel on the **left**. About, source records, anatomy regions, activity stories, messenger explanations and model evidence open as named tabs. Reopening an item reuses its tab. Select an earlier tab to return; its reading position is preserved. The × button closes the current tab and returns to the document that opened it; the downward chevron hides the panel without discarding its tabs. Arrow keys, Home and End navigate the tab strip, which scrolls horizontally when needed. Only one document is visible, and tools automatically dismiss the reader on narrow screens. Scene changes hide the reader; its open tabs remain available. Animation updates do not replace a source being read. - -## Neurons and scientific interpretation - -The optional tissue window contains four procedural branching neurons, dendritic spines, axonal terminals, receiving processes and visible contact gaps. It occupies a localized area of the reference brain and is off initially. The cells are deliberately enlarged; **their positions, numbers, shapes and connectivity do not come from the atlas**. Flashing particles illustrate signaling and are not computed action potentials. This presentation draws on the structural distinctions discussed in [Spruston's review of pyramidal neurons](https://pubmed.ncbi.nlm.nih.gov/18270515/). - -Brain surfaces derive from the Allen Human Reference Atlas 2020. Overview has 270,212 triangles; Fine has 2,100,796; Native uses original 0.5 mm annotations and has 5,080,888. These remain grouped, smoothed anatomical teaching surfaces. Native detail downloads about 79 MB on demand; it is not the default. +Choose a product from the **hardware selector**, or open **Human brain**. Drag the scene to orbit, scroll to zoom, or use the camera buttons. Select a component to read its explanation and sources in the floating panel. Related explanations open as tabs, so a source does not replace the page that led to it. -Four activities retain their study-linked explanations: music/anticipation, reward prediction, visual recognition and finger movement. Highlights and connecting traces are editorial teaching emphasis. No activity predicts a visitor's neurotransmitter levels. +Every hardware family has three depths: -The synapse distinguishes calcium-associated release, receptor action and clearance. Dopamine uses a metabotropic illustration; glutamate and GABA show representative ionotropic mechanisms without claiming those are their only receptor families. Particle trajectories and teaching time are authored. PubChem coordinates represent individual molecular conformers, not molecular dynamics. The glutamate-related structure is **L-glutamic acid**, explicitly labeled as the conjugate acid rather than physiological glutamate protonation. +| View | What it shows | +| --------------------------------- | --------------------------------------------------------------------------------- | +| Processor | Documented product structure, arranged as a functional map; sampled where needed. | +| Inside a core / SM / array / path | Scheduling, storage and execution resources, with explanations on demand. | +| Follow a value / packet | Actual model state: operands, waiting work and sums, or packet IDs and queues. | -The neural circuit is a Wilson–Cowan-inspired two-population rate model, integrated with a fixed 1 ms Heun step. Its twelve visible cell symbols stand for two populations, not twelve individually simulated neurons. Parameters are pedagogical, not fitted to human recordings. All 20 source records are available in the interface. Scientific material has editorial source review, not independent expert validation. +Open **Input** to change the 8 × 8 image. Follow a value to reveal playback and single-step controls. Pin an output to inspect only the products that have actually completed. CPU cache controls expose reuse. GPU packed/strided storage changes requested sectors while preserving the mathematical output. The tensor model passes operands through a 4 × 4 multiply-accumulate array. The matrix operation selector offers edges, weighted neighbors or identity. Chip selection changes architectural context, not undocumented model timing. -## Display and motion +On **Cisco Silicon One G200**, open **Traffic** to send packets, block outputs and compare FIFO with virtual output queues. The geometry changes with the queue discipline. Packet identities, drops and delivery counts come from the bounded three-port model. It illustrates mechanisms without claiming to reproduce Cisco’s internal implementation. -**Adaptive/HDR** retains floating-point render targets, extended-range WebGPU presentation and emissive heading overlays. The color accents are preserved through RGB glow passes rather than converted to grayscale. Eligible browser/OS/display combinations and localhost or HTTPS are required. Ordinary HTTP origins other than localhost use WebGL2 and SDR. +**Human brain** keeps anatomy, a cutaway, and the sourced visual-processing guide. **Compare brain** places that atlas beside the chip. The shared image is a comparison prompt: the numerical image transform does not model a person's perception. The guide's clock sets reading pace; it is separate from execution ticks and biological measurement. **Timing & parts** compares sourced chip clock periods and human synaptic, spike and visual-response durations on a logarithmic seconds axis. Expand a measurement for its preparation and primary paper. H100 memory bandwidth and G200 switching capacity remain separate rate references; neither is treated as a clock or cognition score. -**SDR glow** uses contrast and bloom within the display's existing range. **Quiet light** reduces bloom. **Subtle color accents** can be disabled for an entirely grayscale rendering. The atom labels do not rely on color alone. +Display settings offer Balanced, Sharp and Ultra render resolution, HDR/SDR light, reduced motion, and the three anatomy mesh tiers. Overview has 270,212 triangles; Fine has 2,100,796; Native has 5,080,888 from 0.5 mm reference annotations. Higher tiers load on demand. More mesh detail does not add cellular or functional resolution to the atlas. -Reduced motion disables automatic orbit and initial playback. Explicitly pressing Play starts the chosen synapse or circuit animation; pausing is immediate. The synapse cycles through five-second teaching stages, with visible movement in each stage. These timings are not biological kinetics. +HDR requires a compatible browser, OS and display, with localhost or HTTPS for WebGPU. SDR glow stays within ordinary display range. Screenshots and GIFs do not establish physical HDR luminance. -## Building from a fresh clone +## Build and run -From the repository root, use Rust (tested with 1.92), Node 18 or newer (tested with 23), Python (tested with 3.10) and a browser supporting WebGL2/WebGPU: +Use Rust, Node 18 or newer, Python 3, and a WebGPU/WebGL2 browser. The pinned `wasm-bindgen` CLI must match the Rust dependency: ```sh rustup target add wasm32-unknown-unknown @@ -50,82 +37,49 @@ bash tools/build.sh python3 tools/serve.py ``` -`tools/build_assets.sh` downloads the source annotations, generates all three detail tiers and molecules, then validates the results. Native atlas conversion is a substantial one-time data operation requiring several GB of available memory. It is separate from routine Rust/interface builds. Generated anatomy binaries are ignored by Git; manifests, processing scripts and provenance are retained. `tools/build.sh` regenerates source records, dependency notices, Wasm and the release manifest. Its `wasm-bindgen` CLI version must match the pinned library. Rendering and integration run in each visitor's browser, not on the server. +Open `http://localhost:4173`. Anatomy generation is a separate, substantial one-time download and conversion; allow several GB of memory. It generates and validates three mesh tiers. Routine builds compile Wasm, validate source records, bundle dependency notices and create a release manifest. Source records are maintained directly in `public/architecture.json`, `public/hardware.json`, `public/brain.json` `public/comparison.json` and `public/timescales.json`. -## Verify changes +For hardware development without generated anatomy, build `doke-web` for `wasm32-unknown-unknown`, run the matching `wasm-bindgen` command from `tools/build.sh`, and serve `public/`. Brain views require the anatomy assets. -Browser checks use Chrome on macOS by default. On another installation, set `CHROME_PATH` to the Chrome/Chromium executable. Confirm `node --version` is at least 18 in the same shell used for `npm`. Set `DOKE_URL` to override the local URL for the main browser and refinement checks. +## Verify -With the local server running: +On Linux, install the browser and its system dependencies once with `npx playwright install --with-deps chromium`. On macOS the checks use installed Google Chrome by default. + +With the built app running: ```sh cargo test -p doke-core --locked +python3 tools/check_content.py +DOKE_URL=http://127.0.0.1:4173 npm test .venv/bin/python tools/validate_assets.py -npm test -node tests/refinement.mjs -node tests/panels.mjs -node tests/hdr.mjs -npm run test:performance python3 tools/package_demo.py -node tests/portable.mjs +npm run test:portable ``` -`npm test` exercises anatomy, activities, sources, chemistry, numerical controls, detail tiers, mobile navigation and fallbacks. Refinement checks cover exclusive left panels, contextual docks, rendered-frame pause/resume in all four synapse stages, tissue geometry, color switching and mobile panel separation. HDR tests exercise an actual float16 GPU pipeline with the display-capability signal emulated. They do not measure physical display luminance. - -`tests/panels.mjs` covers About/source return paths, reading position, tab reuse, anatomy history, keyboard navigation, playback while reading evidence, and narrow-screen layout. +The checks cover source references and geometry, exact arithmetic and provenance, rejected inputs, actual rendering, navigation, playback and controls. The portable test serves the compiled artifact under `/doke/` with the packaged security headers. `CHROME_PATH` can select Chrome/Chromium; macOS defaults to installed Chrome, while Linux uses Playwright Chromium. -Generated JSON reports and screenshots live in `test-results/`. Performance numbers from any test are specific to its machine, viewport and browser. No universal frame-rate guarantee is implied. +CI runs source/model checks and the focused browser checks without downloaded anatomy. Its `DOKE_SKIP_BRAIN=1` explicitly skips atlas-dependent checks. Complete local review must also exercise the actual brain and the portable artifact. Reports and screenshots belong in ignored `test-results/`. -## Optional server deployment +## Host a demo -The app needs only static hosting. `tools/deploy.sh` supports an existing dedicated Incus container with Nginx on an SSH host. Provision the container, install Nginx and configure its port forwarding separately; the script does not provision infrastructure or expose ports. +Doke needs static hosting, not an application server. Package the build: ```sh -cp ops/deploy.env.example ops/deploy.local.env -# Edit deploy.local.env to identify your SSH host, container and root directory. -bash tools/build.sh -bash tools/deploy.sh -DOKE_URL=https://your-demo.example npm run test:deployed +python3 tools/package_demo.py ``` -`ops/deploy.local.env` is ignored by Git. Keep addresses and machine-specific settings there, and authenticate through SSH keys/your SSH agent. The default container and installation directory are `doke` and `/opt/doke`; existing installations can override both without renaming a running container. `ops/nginx.conf` is the generic Nginx template. - -The script uploads a timestamped static release, switches the `current` symlink, validates Nginx, restarts the dedicated container and checks health through the configured loopback port. It retains older releases. For rollback, use SSH and Incus to repoint `/current` to a retained `/releases/` directory, then restart that container and verify health. The template serves HTTP; arrange HTTPS at your reverse proxy for public use and WebGPU/HDR eligibility. - -## Public sharing +The result is `dist/doke/`, including runtime assets, provenance, notices and security headers. It excludes the redundant large anatomy file and uses chunked downloads. Publish that artifact, not the repository root. A Cloudflare Pages project can serve it over HTTPS with a `pages.dev` address; a custom domain is optional. GitHub Pages can serve the same artifact but does not apply Cloudflare's `_headers` file. Check [Pages limits](https://developers.cloudflare.com/pages/platform/limits/) before publishing a large new asset. -The recommended public demo is **Cloudflare Pages**, with source on GitHub and a compiled static artifact uploaded to Pages. It can use a free HTTPS `pages.dev` address; no domain purchase or public access to the home server is necessary. The Mac can be off. [Current static-request pricing](https://developers.cloudflare.com/pages/functions/pricing/) and [platform limits](https://developers.cloudflare.com/pages/platform/limits/) apply. +After selecting the Cloudflare account and project, the publication command is: ```sh -python3 tools/package_demo.py -# After selecting a Cloudflare account/project for public publication: npx wrangler pages deploy dist/doke --project-name doke ``` -The portable artifact includes all runtime assets, source attribution and a compatible security-header policy, and excludes redundant oversized meshes. Anatomy downloads are split below the 25 MiB per-file Pages limit. `tests/portable.mjs` verifies a project subdirectory and the intended CSP locally. GitHub Pages is another option; it does not apply Cloudflare's `_headers` file. Accounts or leaderboards would require a separate backend and security design; they are outside the current static application. - -The current application is stateless and has no accounts, analytics or personal brain data. GitHub stores the source; a runnable demo needs the generated `dist/doke/` artifact. Do not commit that artifact or serve the repository root. - -## Git hygiene - -Commit source code, lockfiles, this guide, asset metadata, the fallback image, fonts and third-party notices. `.gitignore` excludes generated meshes and Wasm, source downloads, dependencies, local environments, packaged builds, test output, historical notes and private deployment configuration. Historical research reports and screenshots may remain locally in `docs/archive/`; they are not needed to build or understand the current application. Scientific references remain in `public/content.json` and the interface. - -Before a first push, review `git status --short` and the files selected by `git ls-files --cached --others --exclude-standard`. After staging, inspect `git diff --cached --stat` and `git diff --cached`. Ignore rules do not remove files already committed, so recheck credentials and private addresses whenever adding configuration or reports. A source-code license still needs to be chosen if the project is intended to be open source; asset and dependency licenses remain separate. - -## Recording a demo - -The README's curated tour is `docs/media/doke-demo.gif`. It records real interactions at native anatomy detail, then trims loading and setup pauses into nine short chapters. Capture uses SDR glow because GIF cannot preserve HDR luminance. - -To recreate it on macOS, run the app on port 4173 with all anatomy and molecule assets generated. Use Node >=18, the project's Playwright dependency, Google Chrome, FFmpeg and gifsicle (`brew install ffmpeg gifsicle`). Then: - -```sh -npx playwright install ffmpeg -node tools/record_demo.mjs -python3 tools/render_demo.py -``` +An existing home-server deployment remains supported by `tools/deploy.sh`. Its template is `ops/nginx.conf`; private host/container settings belong in ignored `ops/deploy.local.env`, copied from `ops/deploy.env.example`. The script expects an already provisioned Incus container and port forwarding. It uploads a static release, switches `current`, validates Nginx, restarts the container and checks health. Configure HTTPS separately for public use and WebGPU. No hosting change occurs merely by building or opening the demo. -`CHROME_PATH` and `DOKE_URL` can override the browser executable and local URL. Raw footage, chapter screenshots, edit timestamps and the higher-quality MP4 remain in the ignored `test-results/demo/` directory. Only the selected GIF belongs in Git; the existing [atlas attribution](../public/assets/NOTICE.md) also applies to the anatomy shown in it. For manual recording, [Kap](https://getkap.co/) can record a selected macOS screen area and export GIF or MP4. +## Scope and attribution -## Scientific credits and notices +The [model notes](SILICON.md) distinguish documented hardware from educational execution. Anatomy derives from the Allen Human Reference Atlas 2020, not a scan of the visitor. Region highlights are authored cues, not measured firing. The app has no accounts, analytics or private brain data. -About focuses on the scientific sources, interpretation and compact Allen atlas attribution. The separate dependency-license and anatomy-notice links are omitted from that interface. Their files remain in the repository and packaged website: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) requires attribution, a license link and an indication of adaptations, and dependency licenses such as [MIT](https://opensource.org/license/mit) require their notices to accompany distributed copies. These conditions apply to free projects too. Sharing controls have been removed; existing bookmarked query URLs can still be opened. +Keep the atlas attribution/license/adaptation notice, font licenses and dependency notices in distributed copies. Generated assets, raw capture files and machine-local configuration stay out of Git. Record a new tour from the current interface; the old chemistry/synapse recording no longer describes this version. diff --git a/docs/SILICON.md b/docs/SILICON.md new file mode 100644 index 0000000..1730002 --- /dev/null +++ b/docs/SILICON.md @@ -0,0 +1,60 @@ +# Inside a chip, alongside a brain + +Current scope, 15 October 2026. Doke centers hardware architecture alongside the human brain. One product selector covers CPU, GPU, tensor/NPU and network ASIC references. The chemistry, standalone synapse and circuit experiences are removed from navigation. A single scene, three depths and on-demand explanation tabs carry the experience. + +## Three depths, three kinds of evidence + +The **processor** view shows documented product structure. **One core/SM/array/path** exposes functional resources and their roles. **Execution** follows the actual values computed by a deliberately small Rust model. A selected block opens its explanation and source; the interface does not require reading everything before exploring. + +| Reference | Visible structure | Primary evidence | +| ---------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ryzen 9 9950X | 16 cores; functional groups of eight; 64 MB aggregate L3 | [AMD product specifications](https://www.amd.com/en/products/processors/desktops/ryzen/9000-series/amd-ryzen-9-9950x.html), [Zen 5 core diagram](https://www.amd.com/content/dam/amd/en/documents/epyc-business-docs/white-papers/amd-epyc-smt-technology-brief.pdf), [classic Zen 5 complex](https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/user-guides/58462_amd-epyc-9005-tg-architecture-overview.pdf) | +| RTX 5090 | 170 enabled SMs, four execution partitions per SM, 96 MB L2 | [RTX Blackwell architecture](https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf), Figures 4–6 and product appendix | +| H100 SXM5 | 132 enabled SMs, 50 MB L2, 256 KB combined L1/shared per SM | [Hopper architecture](https://developer.nvidia.com/blog/nvidia-hopper-architecture-in-depth/), [Hopper tuning guide](https://docs.nvidia.com/cuda/hopper-tuning-guide/index.html) | +| Google TPU v1 | Documented 256 × 256 matrix unit, 24 MiB unified buffer; sampled array windows | [TPU paper](https://arxiv.org/abs/1704.04760), [Google architecture explanation](https://cloud.google.com/blog/products/ai-machine-learning/an-in-depth-look-at-googles-first-tensor-processing-unit-tpu) | +| Cisco Silicon One G200 | Public ingress/egress, shared-memory and queueing functions; original functional arrangement | [G200 data sheet](https://www.cisco.com/c/en/us/solutions/collateral/silicon-one/silicon-one-g200-ds.html) | + +The two CPU core groups are an interpretation combining product totals with the documented classic Zen 5 complex; no photographed die placement is asserted. Full GB202 has more SMs than RTX 5090, and full GH100 has more than H100 SXM5. Product variants are kept separate. Positions, areas and trace lengths are original functional schematics, not physical floorplans. + +Physical GPU arithmetic resources are distinct from logical warp lanes. A warp contains 32 threads; its lanes are not 32 entire cores. Architectural landmarks that the running model does not implement remain explanatory. Selecting H100 instead of RTX changes the reference structure and facts; it does not supply a calibrated performance emulator. + +## What the execution model calculates + +The editable 8 × 8 input is matrix A. Choose a matrix B for differences (each column subtracts its left neighbor, with a zero boundary), weighted neighbors (diagonal weight two and adjacent weights one), or identity. All compute C = A × B. CPU, GPU and tensor schedules must produce the same exact 64 outputs and 512 integer multiply-adds. + +The CPU model schedules independent dot products, issues addressed reads, models a fully associative four-line cache with true LRU replacement and merged misses, and waits for operands before arithmetic. It does not decode x86 or emulate branch prediction, speculative execution or reorder-buffer retirement. The surrounding core view explains those mechanisms without inventing activity for them. [BOOM's open CPU documentation](https://docs.boom-core.org/en/latest/sections/issue-units.html) provides mechanism context, separately from AMD-specific facts. + +The GPU model has two logical 32-thread warps. It derives 32-byte sector requests from the active operand addresses. Packed versus strided storage changes request traffic while keeping the mathematical B unchanged. Requested sectors are not claimed to equal DRAM transactions after caches. The [CUDA memory guidance](https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#coalesced-access-to-global-memory) explains that distinction. + +The tensor model uses 16 processing elements arranged as a 4 × 4 output-stationary systolic array. Neighboring cells reuse operands; four tiles cover the 8 × 8 output. The processor view is inspired by the documented TPU v1, whose 65,536 MACs use a weight-stationary organization. The execution model is a deliberately different teaching schedule, not a reproduction of TPU v1’s dataflow. Array windows and decorative cells are explicitly sampled, not invented physical subdivisions. + +The packet model admits packets to a 32-slot, three-port switch. FIFO can block traffic behind a packet whose destination is unavailable; virtual output queues separate those destinations. Each tick delivers previous transfers and arbitrates new ones. Injection, admission, queue occupancy, delivery and drops obey conservation. These capacities and arbitration rules illustrate published queueing mechanisms; no G200 clock, physical port layout or internal latency is inferred. + +Every matrix output retains up to eight actually completed products. Current operands are absent until their loads arrive; accumulated sums contain only retired work; output validity waits for its store. Pause, step and reset operate on the model, and playback interpolates emitted transfers rather than manufacturing completion. + +**Model ticks are authored teaching time.** They are not vendor clock cycles, nanoseconds, FPS, energy or benchmark predictions. Product MHz, bandwidth and core counts do not silently parameterize undisclosed execution rules. The same controls operate all four model families; a product selection changes the architecture and available workload. H100 and RTX share the GPU teaching schedule. + +## The human brain remains part of doke + +The existing Allen atlas provides anatomy, internal structures and a visual-processing guide, both alone and beside the processor. The same image is an invitation to compare mechanisms, not an assertion that an edge transform performs human recognition. + +[Haxby et al.](https://pubmed.ncbi.nlm.nih.gov/11577229/) support distributed visual representations. [Logothetis et al.](https://pubmed.ncbi.nlm.nih.gov/11449264/) distinguish the measured fMRI signal from individual neuronal output. The guide highlights broad atlas landmarks at an authored reading pace; it does not replay those experiments or predict the visitor's neural activity. No universal brain-versus-chip speed or intelligence score is presented. + +## Timing and parts comparison + +`public/timescales.json` stores durations in seconds with explicit statistic type, source and preparation. The panel places chip clock periods (`1 / frequency`) beside human synaptic-response onset, action-potential half-width, local event sequences and visual-task ERP onset on a logarithmic duration axis. These are different events, not equivalent units of computation. The axis does not divide them into a speed or intelligence ranking. Means and approximate onsets are points; only a reported observed interval is a range. + +The human values come from [Molnár et al. (2008)](https://journals.plos.org/plosbiology/article?id=10.1371/journal.pbio.0060222), [Testa-Silva et al. (2010)](https://www.frontiersin.org/journals/synaptic-neuroscience/articles/10.3389/fnsyn.2010.00012/full) and [Thorpe et al. (1996)](https://www.nature.com/articles/381520a0). Clinical tissue provenance, temperature, SD/SEM and the distinction between EEG onset and reaction time remain available beside the measurements. + +Ryzen’s documented base clock, RTX’s documented base clock and TPU v1’s reported clock provide the silicon periods. No verified final H100 core clock is supplied: its documented 3.35 TB/s memory bandwidth is shown separately. G200’s 51.2 Tbps full-duplex switching capacity is also a rate, never converted to a clock period. Parts comparisons describe specific similarities and their limits, not one-to-one neuron/transistor equivalences. + +## Next iterations + +1. Let users preserve an output while reducing memory traffic, with scores scoped to one model version and input. +2. Add a small, specified instruction workload or measured trace before claiming instruction-level execution on a particular device. +3. Follow a real tiny neural-network operator graph into hardware work; an LLM topology alone is not inference. +4. Add licensed recorded neural trials with species, modality, units and preprocessing kept visible. + +Further complexity must earn its place through a useful interaction and inspectable evidence. Hardware depth, visual clarity and the brain comparison take priority over adding more modes. + +Validation checks exact results, causal operand arrival, bounded provenance, real cache/sector changes, source and geometry consistency, rendering, controls and portable hosting. It does not establish vendor-cycle or biological accuracy. Source records and specific limits live beside each explanation in `public/architecture.json`, `public/brain.json` `public/comparison.json` and `public/timescales.json`. diff --git a/docs/media/brain-comparison.webp b/docs/media/brain-comparison.webp new file mode 100644 index 0000000..3358195 Binary files /dev/null and b/docs/media/brain-comparison.webp differ diff --git a/docs/media/compute-explorer.webp b/docs/media/compute-explorer.webp new file mode 100644 index 0000000..e74f640 Binary files /dev/null and b/docs/media/compute-explorer.webp differ diff --git a/docs/media/cpu-core.webp b/docs/media/cpu-core.webp new file mode 100644 index 0000000..f64bef4 Binary files /dev/null and b/docs/media/cpu-core.webp differ diff --git a/docs/media/doke-demo.gif b/docs/media/doke-demo.gif deleted file mode 100644 index ae8b798..0000000 Binary files a/docs/media/doke-demo.gif and /dev/null differ diff --git a/docs/media/follow-a-value.webp b/docs/media/follow-a-value.webp new file mode 100644 index 0000000..b20787e Binary files /dev/null and b/docs/media/follow-a-value.webp differ diff --git a/docs/media/packet-queues.webp b/docs/media/packet-queues.webp new file mode 100644 index 0000000..7842f48 Binary files /dev/null and b/docs/media/packet-queues.webp differ diff --git a/docs/media/tensor-array.webp b/docs/media/tensor-array.webp new file mode 100644 index 0000000..5576aa9 Binary files /dev/null and b/docs/media/tensor-array.webp differ diff --git a/package.json b/package.json index 33a74b4..8d7d079 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "private": true, "type": "module", "scripts": { - "test": "node tests/browser.mjs", + "test": "node tests/hardware-catalog.mjs && node tests/architecture.mjs && node tests/timescales.mjs && node tests/silicon-wasm.mjs && node tests/focused-browser.mjs && node tests/hardware-expansion.mjs && node tests/interaction-review.mjs", "build": "bash tools/build.sh", - "test:deployed": "node tests/deployed.mjs", - "test:performance": "node tests/performance.mjs", - "format": "prettier --write public/app.js public/style.css public/index.html tests/*.mjs" + "test:browser": "node tests/focused-browser.mjs && node tests/hardware-expansion.mjs && node tests/interaction-review.mjs", + "test:portable": "node tests/portable-demo.mjs", + "format": "prettier --write public/silicon.js public/silicon-layouts.js public/timescales-view.js public/brain-comparison.js public/silicon.css public/index.html tests/*.mjs", + "test:silicon": "node tests/hardware-catalog.mjs && node tests/architecture.mjs && node tests/timescales.mjs && node tests/silicon-wasm.mjs" }, "devDependencies": { "playwright": "1.58.2", diff --git a/public/app.js b/public/app.js deleted file mode 100644 index c8db5fe..0000000 --- a/public/app.js +++ /dev/null @@ -1,2019 +0,0 @@ -import { hdrCapable, configureHDR, HDRText, loadNative } from "./display.js"; -const $ = (s) => document.querySelector(s), - $$ = (s) => [...document.querySelectorAll(s)]; -const esc = (s) => - String(s ?? "").replace( - /[&<>"']/g, - (c) => - ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ - c - ], - ); -const state = { - tab: "explore", - view: "whole", - region: null, - activity: null, - chemical: null, - time: 0, - playing: false, - chapter: -1, - reduced: matchMedia("(prefers-reduced-motion: reduce)").matches, - paths: true, - mode: "atlas", - step: 0, - circuitPlaying: false, - drive: 2.5, - inhibition: 8, - quality: "auto", - camera: 0, - parent: null, - microPlaying: !matchMedia("(prefers-reduced-motion: reduce)").matches, - microClock: 0, - microElapsed: 0, - graph: false, - pulse: 0, - light: "auto", - neurons: false, - neuronAnchor: null, - color: true, -}; -let atlas, - content, - renderer, - wasm, - circuit, - dirty = true, - last = 0, - lastUI = 0, - clock = 0, - trace = [], - gpuError = "", - resizeView = () => {}, - loadToken = 0; -const canvas = () => $("#brain-canvas"), - overlay = $("#overlay-canvas"), - ctx = overlay.getContext("2d"); -let atlasBytes = null, - sceneLoaded = "atlas", - hdrText = null, - hdrActive = false, - detailAbort = null, - moleculeData = null, - sceneSerial = 0, - textDirty = true, - interactionTime = 0; -const moleculeCache = new Map(); -const initialDetails = $("#details").innerHTML; -const regions = () => atlas.regions.filter((r) => r.kind !== 2); -const regionBy = (id) => atlas.regions.find((r) => r.id === id); -const fmt = (n) => - `${Math.floor(n / 60) - .toString() - .padStart(2, "0")}:${Math.floor(n % 60) - .toString() - .padStart(2, "0")}`; -function toast(s) { - $("#toast").textContent = s; - $("#toast").classList.add("show"); - clearTimeout(toast.timer); - toast.timer = setTimeout(() => $("#toast").classList.remove("show"), 2800); -} -function evidence(keys) { - return `
FOLLOW THE EVIDENCE
${[...new Set(keys)].map((k) => ``).join("")}
`; -} -function showSource(id) { - const s = content.sources[id]; - if (!s) return; - writeDetails( - `source-${id}`, - `${s.authors} · ${s.year}`, - `
SOURCE RECORD · ${s.year}

${esc(s.title)}

${esc(s.authors)}

${esc(s.method)} · ${esc(s.species)}

What it supports

${esc(s.finding)}

What it does not establish

${esc(s.limit)}

Read the source ↗

Evidence is linked to specific explanations. Use the findings and limitations together when interpreting an explanation.

`, - { reference: true, reveal: true }, - ); -} -function about() { - writeDetails( - "about", - "About", - `
ABOUT DOKE

A window into the brain.
With the evidence in view.

Explore reference anatomy, follow everyday activities, and discover how cells and chemical messengers work together.

Anatomy, activity and signaling

Anatomy: the Allen Human Reference Atlas 2020 provides a shared reference for the brain’s structures. The displayed regions group atlas labels; their boundaries are simplified teaching surfaces, not a scan of an individual.

Activities and synapses: research findings guide the explanations of music, reward, vision, movement and chemical signaling. Glowing regions and traveling particles illustrate those explanations; they do not measure firing rates or chemical concentrations.

Neural circuit: an excitatory–inhibitory population model inspired by Wilson–Cowan explores how interacting populations influence one another. Its parameters are educational, not fitted to a person’s brain.

Follow a chemical messenger

Synthesis, release, receptor action and clearance are distinct processes. A messenger’s effect depends on the receiving cell and receptor. Molecular structures come from PubChem; they represent individual conformers rather than the molecules’ motion inside a living synapse.

Reference anatomy

Allen Human Reference Atlas – 3D, 2020. Allen Institute for Brain Science; Ding et al. RRID:SCR_017764. Original atlas · CC BY 4.0. Adapted through teaching-region grouping, resampling, smoothing and surface simplification.

${evidence(["allen", "wilson", "bold", "neurotransmitters"])}`, - { reference: true, reveal: true }, - ); -} -function renderLeft() { - if (state.tab === "explore") { - $("#left-content").innerHTML = - `
ANATOMY${regions().length} REGIONS
`; - renderRegions(""); - $("#region-search").oninput = (e) => renderRegions(e.target.value); - } else if (state.tab === "activities") { - $("#left-content").innerHTML = - `
EVERYDAY EXPERIENCE04 STORIES
${content.activities.map((a) => ``).join("")}

Study findings become a guided story. The animation’s timing and intensity are illustrative.

`; - } else if (state.tab === "chemicals") { - $("#left-content").innerHTML = - `
CHEMICAL MESSENGERS03 SIGNALS
${content.chemicals.map((c) => ``).join("")}
FOLLOW A MOLECULE

Across the
synaptic cleft.

Step from synthesis to release, receptor action and clearance.

A chemical can have different effects across cells, receptors and circuits. There is no one-chemical explanation for a feeling.

`; - } else { - $("#left-content").innerHTML = - `
EXCITATION & INHIBITIONTOY MODEL

Two interacting populations. Change their inputs and watch the system evolve.

${circuit ? "Integrated in Rust at a fixed 1 ms step." : "The Rust model is unavailable on this device."} Parameters are pedagogical, not fitted to human recordings.

`; - $("#drive").oninput = (e) => { - state.drive = +e.target.value; - $("#drive-output").value = state.drive.toFixed(1); - dirty = true; - }; - $("#inhibition").oninput = (e) => { - state.inhibition = +e.target.value; - $("#inhibition-output").value = state.inhibition.toFixed(1); - dirty = true; - }; - $("#circuit-play").onclick = () => { - state.circuitPlaying = !state.circuitPlaying; - renderLeft(); - refreshMicro(); - dirty = true; - }; - $("#circuit-reset").onclick = () => { - circuit?.reset(); - trace = []; - clock = 0; - state.pulse = 0; - state.microClock = 0; - if ($("#model-readout")) - $("#model-readout").textContent = - "Model time 0.000 s · E 0.100 · I 0.100"; - dirty = true; - toast("Model reset to E = I = 0.1"); - }; - } -} -function renderRegions(q) { - const found = regions().filter((r) => - `${r.id} ${r.name} ${r.role} ${r.atlasNames.join(" ")}` - .toLowerCase() - .includes(q.toLowerCase()), - ); - $("#region-count").textContent = `${found.length} REGIONS`; - $("#region-list").innerHTML = found.length - ? found - .map( - (r) => - ``, - ) - .join("") - : '

No matching region. Try “motor” or “thalamus”.

'; -} -function setTab(tab) { - if (!["explore", "activities", "chemicals", "circuit"].includes(tab)) return; - state.tab = tab; - $("#drawer-title").textContent = { - explore: "Anatomy", - activities: "Everyday experiences", - chemicals: "Chemical messengers", - circuit: "Tune the conversation", - }[tab]; - state.mode = tab === "circuit" ? "circuit" : "atlas"; - state.playing = false; - state.circuitPlaying = false; - setMode(); - $$("[data-tab]").forEach((b) => { - b.classList.toggle("active", b.dataset.tab === tab); - b.setAttribute("aria-current", b.dataset.tab === tab ? "page" : "false"); - }); - renderLeft(); - if (tab === "explore") { - state.activity = null; - state.chemical = null; - state.region = null; - renderer?.select(-1); - writeDetails("overview", "Brain", initialDetails, { activate: false }); - } - if (tab === "activities" && !state.activity) { - writeDetails( - "activities", - "Activities", - `
A BRAIN IN CONTEXT

One experience.
Many contributors.

Choose a study-led story. Pause at any point, select a region, and open the supporting evidence.

Start with music.

A study of intensely pleasurable music offers a window into anticipation and reward.

These lessons highlight selected findings. Unlit areas are not inactive, and the story is not an exhaustive map of an activity.

`, - { activate: false }, - ); - } - if (tab === "activities" && state.activity) showChapter(true); - if (tab === "chemicals") selectChemical(state.chemical?.id || "dopamine"); - if (tab === "circuit") { - writeDetails( - "circuit", - "Neural circuit", - `
A NUMERICAL EXPERIMENT

A balance
in motion.

Each group represents a normalized population state. Excitation drives activity; inhibition feeds back onto the excitatory population.

— Excitatory (E)┄ Inhibitory (I)
Model time 0.000 s · E 0.100 · I 0.100

Try raising external drive, then increase inhibitory coupling. The resulting equilibrium depends on both interactions.

WHAT THIS MODEL MEANS

These are dimensionless population variables—not individual spikes or chemical concentrations. They are not mapped to the atlas.

${evidence(["wilson"])}`, - { activate: false }, - ); - } - updateActivity(); - updateTimeline(); - dirty = true; -} -const panelPages = new Map(); -let activePanelPage = null, - latestDetailPage = null, - nextPanelId = 0; -function rememberPanelScroll() { - const page = panelPages.get(activePanelPage); - if (page) { - page.scroll = $("#details").scrollTop; - if (page.key === `activity-${state.activity?.id}`) - page.activityTime = state.time; - } -} -function renderPanelTabs() { - const list = $("#panel-tabs"); - for (const page of panelPages.values()) { - if (!page.opened) continue; - if (!page.button) { - const button = document.createElement("button"); - button.id = `document-tab-${nextPanelId++}`; - button.setAttribute("role", "tab"); - button.setAttribute("aria-controls", "details"); - button.dataset.panelPage = page.key; - button.onclick = () => revisitPanelPage(page.key); - page.button = button; - list.append(button); - } - page.button.textContent = page.title; - page.button.title = page.title; - const selected = - document.body.dataset.info === "open" && activePanelPage === page.key; - page.button.setAttribute("aria-selected", String(selected)); - page.button.tabIndex = selected ? 0 : -1; - } - const browse = document.body.dataset.explorer === "open"; - $("#panel-browse").setAttribute("aria-selected", String(browse)); - $("#panel-browse").tabIndex = browse ? 0 : -1; - $("#close-tab").hidden = browse || !activePanelPage; - $("#close-tab").title = - `Close ${panelPages.get(activePanelPage)?.title || "current"} tab`; -} -function revisitPanelPage(key) { - const page = panelPages.get(key); - if (!page) return; - if (key === "circuit" && state.mode !== "circuit") setTab("circuit"); - if ( - key.startsWith("molecule-") && - (state.mode !== "molecule" || state.chemical?.id !== key.slice(9)) - ) - openMolecule(key.slice(9)); - if ( - key.startsWith("region-") && - (state.region !== key.slice(7) || state.mode !== "atlas") - ) - selectRegion(key.slice(7)); - else if ( - key.startsWith("activity-") && - (state.activity?.id !== key.slice(9) || state.mode !== "atlas") - ) - selectActivity(key.slice(9), page.activityTime || 0); - else if ( - key.startsWith("chemical-") && - (state.chemical?.id !== key.slice(9) || state.mode !== "synapse") - ) - selectChemical(key.slice(9)); - else if ( - key.startsWith("synapse-") && - (state.chemical?.id !== key.slice(8) || state.mode !== "synapse") - ) { - state.chemical = content.chemicals.find((c) => c.id === key.slice(8)); - openSynapse(); - } - selectPanelPage(key, true); -} -function selectPanelPage(key, reveal = false) { - const page = panelPages.get(key); - if (!page) return; - if (activePanelPage !== key) { - if (reveal) page.returnTo = activePanelPage; - rememberPanelScroll(); - activePanelPage = key; - $("#details").replaceChildren(...page.nodes); - $("#details").scrollTop = page.scroll; - } - if (reveal || document.body.dataset.info === "open") page.opened = true; - if (reveal) { - document.body.dataset.info = "open"; - document.body.dataset.explorer = "closed"; - if (innerWidth < 821) $("#view-settings").hidden = true; - } - if (key === "circuit") { - const value = trace.at(-1) || [0, 0.1, 0.1]; - const readout = $("#model-readout"); - if (readout) - readout.textContent = `Model time ${value[0].toFixed(3)} s · E ${value[1].toFixed(3)} · I ${value[2].toFixed(3)}`; - } - syncPanel(); - if (!page.button || !page.opened) return; - $("#details").setAttribute("aria-labelledby", page.button.id); - page.button.scrollIntoView({ block: "nearest", inline: "nearest" }); -} -function writeDetails( - key, - title, - html, - { activate = true, reference = false, reveal = false } = {}, -) { - let page = panelPages.get(key); - if (!page) { - page = { key, title, nodes: [], scroll: 0, reference }; - panelPages.set(key, page); - } - // Reopening a reference keeps its reading position and does not duplicate the tab. - if (!reference || !page.nodes.length) { - const template = document.createElement("template"); - template.innerHTML = html; - page.nodes = [...template.content.childNodes]; - if (activePanelPage === key) { - rememberPanelScroll(); - $("#details").replaceChildren(...page.nodes); - $("#details").scrollTop = page.scroll; - } - } - if (!reference) latestDetailPage = key; - if (activate || reveal || !activePanelPage) selectPanelPage(key, reveal); - else syncPanel(); -} -function closePanelTab() { - const keys = [...panelPages.values()] - .filter((p) => p.opened) - .map((p) => p.key), - index = keys.indexOf(activePanelPage); - const page = panelPages.get(activePanelPage); - if (!page) return; - page.button.remove(); - panelPages.delete(activePanelPage); - if (latestDetailPage === activePanelPage) - latestDetailPage = [...panelPages.values()] - .reverse() - .find((p) => !p.reference)?.key; - activePanelPage = null; - const next = panelPages.get(page.returnTo)?.opened - ? page.returnTo - : keys[index - 1] || keys[index + 1]; - if (next) selectPanelPage(next, true); - else { - $("#details").replaceChildren(); - openExplorer(); - } - $("#panel-tabs [aria-selected=true]")?.focus(); -} -function syncPanel() { - const browse = document.body.dataset.explorer === "open", - details = document.body.dataset.info === "open"; - $("#info-panel").hidden = !browse && !details; - $("#panel-browse").textContent = - state.mode === "circuit" - ? "Controls" - : state.mode !== "atlas" - ? "Messengers" - : state.tab === "activities" - ? "Activities" - : "Anatomy"; - $("#browse-page").hidden = !browse; - $("#details").hidden = !details; - renderPanelTabs(); - textDirty = true; - dirty = true; -} -function openExplorer(on = true) { - document.body.dataset.explorer = on ? "open" : "closed"; - if (on) { - document.body.dataset.info = "closed"; - if (innerWidth < 821) $("#view-settings").hidden = true; - } - syncPanel(); -} -function openInfo(on = true) { - if (on && latestDetailPage) selectPanelPage(latestDetailPage, true); - document.body.dataset.info = on ? "open" : "closed"; - if (on) { - document.body.dataset.explorer = "closed"; - if (innerWidth < 821) $("#view-settings").hidden = true; - } - syncPanel(); -} -function setMode() { - sceneSerial++; - const anatomy = state.mode === "atlas"; - document.body.dataset.scene = state.mode; - canvas().style.visibility = "visible"; - $(".stage-tools").hidden = false; - $(".scene-hint").hidden = !anatomy; - $(".segmented").hidden = !anatomy; - $("#view-settings").hidden = true; - $("#cut-control").hidden = !(anatomy && state.view === "cut"); - $(".explorer-dock").hidden = !anatomy; - $$(".atlas-setting").forEach((el) => (el.hidden = !anatomy)); - canvas().setAttribute( - "aria-label", - `${anatomy ? "Brain atlas" : state.mode === "molecule" ? "Chemical messenger structure" : state.mode === "synapse" ? "Chemical synapse" : "Neural circuit"}. Drag to orbit, scroll to zoom; arrow keys rotate, plus and minus zoom.`, - ); - $(".scale-portal").hidden = !anatomy; - $("#micro-controls").hidden = anatomy; - $(".micro-caption").hidden = anatomy; - $("#scene-breadcrumb").hidden = anatomy; - $("#scene-mode").textContent = - state.mode === "circuit" - ? "A NETWORK OF SIGNALS · TEACHING MODEL" - : state.mode === "synapse" - ? "ACROSS THE SYNAPTIC CLEFT" - : state.mode === "molecule" - ? "MEET THE MESSENGER · PUBCHEM CONFORMER" - : state.activity && state.tab === "activities" - ? "NARRATIVE FLOW · NOT MEASURED AXONS" - : "THE HUMAN BRAIN"; - $("#scale-tag").textContent = anatomy - ? viewName() - : state.mode === "molecule" - ? "Structure, not dynamics" - : "Illustrative · not to scale"; - $("#scene-labels").innerHTML = ""; - delete $("#scene-labels").dataset.signature; - $("#focus-cleft").dataset.focused = "false"; - $("#focus-cleft").textContent = "Focus cleft ⊕"; - $("#scene-loading").hidden = !!renderer || (!anatomy && !!wasm); - $$("[data-scene]").forEach((b) => - b.classList.toggle("active", b.dataset.scene === state.mode), - ); - $("#quality").disabled = !anatomy; - syncScene().catch((e) => { - toast(e.message); - }); - refreshMicro(); - syncPanel(); - updateTimeline(); - dirty = true; -} -async function syncScene() { - if (!renderer) return; - const key = - state.mode + - (["molecule", "synapse"].includes(state.mode) ? state.chemical?.id : ""); - if (key === sceneLoaded) { - if (state.mode === "molecule" && moleculeData) showMoleculeDetails(); - return; - } - const ticket = ++sceneSerial; - detailAbort?.abort(); - if (state.mode === "atlas") { - if (atlasBytes) renderer.load_mesh(atlasBytes); - renderer.anatomy(); - applyCamera(state.camera); - setView(state.view); - updateActivity(); - if (state.neurons && state.neuronAnchor) - renderer.neuron_detail(...state.neuronAnchor, true); - } else if (state.mode === "synapse") { - renderer.scene( - 1, - content.chemicals.findIndex((c) => c.id === state.chemical?.id), - ); - renderer.micro_state(state.step, 0, 0); - } else if (state.mode === "circuit") { - renderer.scene(2, 0); - renderer.micro_state(0, 0.1, 0.1); - } else { - const chem = state.chemical || content.chemicals[0]; - const data = - moleculeCache.get(chem.id) || - (await fetch(`assets/molecules/${chem.id}.json`).then((r) => { - if (!r.ok) throw Error("Molecular structure unavailable"); - return r.json(); - })); - moleculeCache.set(chem.id, data); - if (ticket !== sceneSerial) return; - moleculeData = data; - renderer.molecule(JSON.stringify(data)); - renderer.micro_state(0, 0, 0); - showMoleculeDetails(); - } - sceneLoaded = key; - $("#render-stats").textContent = - `${renderer.backend()} · Rust · ${renderer.triangle_count().toLocaleString()} triangles`; - canvas().classList.remove("scene-transition"); - void canvas().offsetWidth; - canvas().classList.add("scene-transition"); - dirty = true; -} -function refreshMicro() { - if (state.mode === "atlas") return; - const syn = state.mode === "synapse", - mol = state.mode === "molecule"; - const stepsHTML = syn - ? ["Synthesis", "Release", "Receptors", "Clearance"] - .map( - (name, i) => - ``, - ) - .join("") - : mol - ? content.chemicals - .map( - (c) => - ``, - ) - .join("") - : ''; - if ($("#micro-steps").dataset.content !== stepsHTML) { - $("#micro-steps").dataset.content = stepsHTML; - $("#micro-steps").innerHTML = stepsHTML; - } - $("#inspect-molecule").hidden = !syn; - $("#focus-cleft").hidden = !syn; - $("#micro-play").hidden = mol; - $("#micro-play").textContent = ( - syn ? state.microPlaying : state.circuitPlaying - ) - ? "Ⅱ" - : "▶"; - $("#micro-play").setAttribute( - "aria-pressed", - String(syn ? state.microPlaying : state.circuitPlaying), - ); - $("#micro-play").setAttribute( - "aria-label", - (syn ? state.microPlaying : state.circuitPlaying) - ? "Pause animation" - : "Play animation", - ); - $("#micro-kicker").textContent = syn - ? `${state.chemical?.name || "Dopamine"} · STEP ${state.step + 1} OF 4` - : mol - ? "ATOM BY ATOM" - : "EXCITATION ↔ INHIBITION"; - $("#micro-title").textContent = syn - ? [ - "Make the messenger.", - "Cross the cleft.", - "Receive the signal.", - "Clear and recycle.", - ][state.step] - : mol - ? state.chemical.id === "glutamate" - ? "L-glutamic acid" - : state.chemical.name - : "How neurons talk."; - $("#micro-description").textContent = syn - ? [ - "Vesicles package chemical messengers. The windowed vesicles bring their contents into sight.", - "Calcium entry helps trigger release. Follow messenger symbols toward the receiving membrane.", - state.chemical?.id === "dopamine" - ? "Binding activates intracellular signaling. Dopamine receptors are not ion-channel pores." - : "Receptor binding changes the receiving cell’s signaling. Receptor family and ionic gradients matter.", - "Transport and metabolism regulate the signal. Trails illustrate clearance without assigning a rate.", - ][state.step] - : mol - ? state.chemical.id === "glutamate" - ? "Glutamate’s conjugate acid, from PubChem. This structure is not a physiological protonation model." - : "Rotate a real molecular conformer. Atom letters identify each element; restrained color distinguishes oxygen and nitrogen." - : "Two groups influence each other. The Rust model changes their activity; traveling pulses illustrate signaling, rather than computed individual spikes."; - const crumb = $("#scene-breadcrumb button:nth-of-type(2)"); - crumb.textContent = state.mode === "circuit" ? "Neural circuit" : "Synapse"; - crumb.dataset.scene = state.mode === "circuit" ? "circuit" : "synapse"; - $("#molecule-crumb").textContent = mol ? ` / ${state.chemical.name}` : ""; -} -async function openMolecule(id) { - if (!state.chemical) state.chemical = content.chemicals[0]; - if (id) - state.chemical = - content.chemicals.find((c) => c.id === id) || state.chemical; - state.mode = "molecule"; - state.tab = "chemicals"; - renderLeft(); - state.microPlaying = false; - state.circuitPlaying = false; - setMode(); - openExplorer(false); - openInfo(false); -} -function showMoleculeDetails() { - if (moleculeData) - writeDetails( - `molecule-${state.chemical.id}`, - state.chemical.name, - `
PUBCHEM · CID ${moleculeData.cid}

${esc(state.chemical.name)}

${esc(moleculeData.note)}

C · HNO

C: carbon · H: hydrogen · N: nitrogen · O: oxygen. Ball sizes and bonds are simplified for legibility; no motion or energy is calculated.

Inspect source structure ↗`, - { activate: false }, - ); -} -function drawHotspots() { - if (!renderer) return; - let marks = []; - if ( - state.mode === "atlas" && - !state.region && - !state.activity && - state.tab !== "chemicals" - ) - marks = [ - { - p: regionBy("motor").leftCentroid, - label: "Cerebral cortex", - region: "motor", - }, - { - p: regionBy("cerebellum").leftCentroid, - label: "Cerebellum", - region: "cerebellum", - }, - { p: regionBy("pons").centroid, label: "Brainstem", region: "pons" }, - ]; - if (state.mode === "atlas" && state.neurons && state.neuronAnchor) - marks = [ - { - p: state.neuronAnchor.map((v, axis) => v - (axis === 1 ? 0.29 : 0)), - label: "Neural tissue · enlarged", - neuron: true, - }, - ]; - if (state.mode === "synapse") - marks = [ - { p: [-0.62, 0.97, 0.63], label: "Vesicles", step: 0 }, - { p: [0.55, 0.02, 0.35], label: "Messenger flow", step: 1 }, - { p: [0.6, -0.38, 0.15], label: "Receptors", step: 2 }, - ]; - if (state.mode === "circuit") - marks = [ - { p: [-0.8, 0.85, 0], label: "Excitatory cells · E", info: true }, - { p: [0.8, 0.85, 0], label: "Inhibitory cells · I", info: true }, - ]; - if (state.mode === "molecule" && moleculeData?.id === state.chemical?.id) - marks = moleculeData.atoms - .map((a, i) => ({ - p: [a.x * 0.3, a.y * 0.3, a.z * 0.3], - label: a.element, - element: a.element, - atom: i, - })) - .filter((a) => a.label !== "H"); - if (state.mode === "atlas" && state.view === "deep") - marks = marks.filter((m) => m.region !== "motor"); - const d = canvas().width / canvas().clientWidth; - const html = marks - .map((m) => { - const p = renderer.project(...m.p); - if (p[2] < 0 || p[2] > 1) return ""; - const x = p[0] / d, - y = p[1] / d; - if (x < 20 || x > innerWidth - 20 || y < 120 || y > innerHeight - 110) - return ""; - return ``; - }) - .join(""); - const holder = $("#scene-labels"); - const signature = html.replace(/style="[^"]*"/g, ""); - if (holder.dataset.signature !== signature) { - holder.dataset.signature = signature; - holder.innerHTML = html; - } else { - const temp = document.createElement("div"); - temp.innerHTML = html; - const next = [...temp.children]; - [...holder.children].forEach((el, i) => { - if (next[i]) el.style.cssText = next[i].style.cssText; - else el.hidden = true; - }); - } -} -function neuronInfo() { - writeDetails( - "neurons", - "Neurons", - `
NEURAL TISSUE · ENLARGED SCHEMATIC

Cells beneath
the surface.

Branching dendrites receive inputs. A cell body integrates them, and an axon carries signals toward synaptic contacts.

The small heads on dendrites are spines. Nearby axon terminals form representative contacts, with a visible gap between cells.

These procedural neurons are enlarged inside a local atlas window. Their positions, shapes, counts and flashes are illustrative, not measured cells or reconstructed connectivity. The surrounding brain remains reference anatomy.

${evidence(["neuron-morphology", "allen"])}`, - {}, - ); - openInfo(); -} -function toggleNeurons() { - state.neurons = !state.neurons; - const r = regionBy(state.region); - const source = r && r.index < 18 ? r : regionBy("motor"); - state.neuronAnchor = source.leftCentroid.slice(); - renderer?.neuron_detail(...state.neuronAnchor, state.neurons); - $("#neuron-layer").setAttribute("aria-pressed", String(state.neurons)); - if (state.neurons) { - $("#hemisphere").value = "0"; - setView("whole"); - neuronInfo(); - } else { - applyCamera(0); - openInfo(false); - } - dirty = true; -} -function applyCamera(n) { - state.camera = n; - renderer?.view(n); - $("#camera-preset").value = String(n); - $("#orientation-label").textContent = [ - "PERSPECTIVE", - "LEFT LATERAL", - "RIGHT LATERAL", - "SUPERIOR", - "POSTERIOR", - "DEEP PERSPECTIVE", - ][n]; - dirty = true; -} -function viewName() { - return state.view === "deep" - ? "Inside · x-ray highlights" - : state.view === "cut" - ? "Cutaway" - : "Whole brain"; -} -function setView(view) { - state.view = view; - if (state.mode !== "atlas") return; - renderer?.xray(view === "deep"); - renderer?.hemisphere(+$("#hemisphere").value); - renderer?.reveal( - view === "deep" ? 1 : +$("#hemisphere").value, - view === "cut" ? +$("#cut-depth").value : 2, - ); - $$(".segmented button").forEach((b) => - b.classList.toggle( - "active", - b.id === - `view-${view === "deep" ? "deep" : view === "cut" ? "cut" : "whole"}`, - ), - ); - $("#cut-control").hidden = view !== "cut" || state.mode !== "atlas"; - if (state.mode === "atlas") $("#scale-tag").textContent = viewName(); - dirty = true; -} -function selectRegion(id) { - openInfo(true); - const r = regionBy(id); - if (!r || r.kind === 2) return; - state.region = id; - renderer?.select(r.index); - if (state.mode !== "atlas") { - state.mode = "atlas"; - setMode(); - } - if (r.kind === 1 && state.view === "whole") setView("deep"); - const info = content.regions[id]; - writeDetails( - `region-${id}`, - r.name, - `
ANATOMY / ${String(r.index + 1).padStart(2, "0")}

${esc(r.name)}

${esc(r.role)}

${esc(info.summary)}

ATLAS MAPPING

${r.labels.length} reference labels grouped for teaching. Includes ${esc(r.atlasNames.join("; "))}.

This is an anatomical grouping, not an independently mapped functional parcel. A region contributes to multiple processes.

${evidence(info.sources)}${state.activity ? '' : ""}`, - {}, - ); - if (state.tab === "explore") renderRegions($("#region-search")?.value || ""); - dirty = true; -} -function selectActivity(id, time = 0) { - openInfo(true); - openExplorer(false); - const a = content.activities.find((a) => a.id === id); - if (!a) return; - state.activity = a; - state.region = null; - state.chemical = null; - state.time = Math.max(0, Math.min(a.duration, time)); - state.chapter = -1; - state.tab = "activities"; - state.mode = "atlas"; - renderer?.select(-1); - setTab("activities"); - setView(["music", "reward"].includes(id) ? "deep" : "whole"); - renderer?.view( - ["music", "reward"].includes(id) ? 5 : id === "vision" ? 4 : 0, - ); - $("#orientation-label").textContent = - id === "vision" ? "POSTERIOR" : "PERSPECTIVE"; - showChapter(true); - updateActivity(); - updateTimeline(); - dirty = true; -} -function showChapter(force = false) { - const a = state.activity; - if (!a) return; - const n = Math.min( - a.chapters.length - 1, - Math.floor(state.time / (a.duration / a.chapters.length)), - ); - if (n === state.chapter && !force) return; - state.chapter = n; - const c = a.chapters[n]; - writeDetails( - `activity-${a.id}`, - a.short, - `
${a.number} / ${esc(a.short).toUpperCase()}

${esc(c.title)}.

CHAPTER ${n + 1} OF ${a.chapters.length}

${esc(c.text)}

${c.regions.map((id) => ``).join("")}

Highlight = focus of this explanation. Brightness, duration and connecting marks are authored. Unlit regions are not inactive. Connections are schematic and do not represent measured axons. Inside view shows highlighted structures through their neighbors.

${evidence([c.source])}`, - { activate: force }, - ); -} -function selectChemical(id) { - openInfo(true); - const c = content.chemicals.find((c) => c.id === id); - if (!c) return; - state.chemical = c; - state.region = null; - state.activity = null; - state.playing = false; - state.tab = "chemicals"; - state.mode = "synapse"; - state.step = 1; - state.microPlaying = !state.reduced; - renderer?.select(-1); - $$("[data-tab]").forEach((b) => - b.classList.toggle("active", b.dataset.tab === "chemicals"), - ); - setMode(); - if (state.mode === "atlas") setView(c.regions.length ? "deep" : "whole"); - renderLeft(); - writeDetails( - `chemical-${c.id}`, - c.name, - `
${esc(c.className)}

${esc(c.name)}.

${esc(c.summary)}

${["synthesis", "release", "receptors", "clearance"].map((k, i) => `
0${i + 1} / ${k.toUpperCase()}

${esc(c[k])}

`).join("")}

${c.regions.length ? "Highlighted regions provide pathway context, not a map of synthesis rates or concentrations. This selection is incomplete; the ventral tegmental area is not separately segmented in this teaching atlas." : "This messenger is broadly distributed. No activity-specific release field is inferred or shown."}

${evidence(c.sources)}
RELATED STORIES
${c.activities.map((id) => ``).join("")}`, - {}, - ); - updateActivity(); - updateTimeline(); - dirty = true; -} -function updateActivity() { - const values = new Float32Array(64); - if (state.mode === "atlas") { - if (state.tab === "activities" && state.activity) { - const a = state.activity; - const span = a.duration / a.chapters.length; - a.chapters.forEach((c, i) => { - const local = state.time - i * span; - let v = wasm - ? wasm.envelope(state.time, i * span, (i + 1) * span) - : Math.max( - 0, - Math.min(1, local / (span * 0.2), (span - local) / (span * 0.2)), - ); - if (state.reduced && state.chapter === i) v = 0.8; - c.regions.forEach((id) => { - const r = regionBy(id); - if (r) values[r.index] = Math.max(values[r.index], v * 0.8); - }); - }); - } - if (state.tab === "chemicals" && state.chemical) - state.chemical.regions.forEach((id) => { - const r = regionBy(id); - if (r) values[r.index] = 0.55; - }); - } - renderer?.activity(values); -} -function updateTimeline() { - document.body.dataset.story = - state.mode === "atlas" && state.tab === "activities" && state.activity - ? "on" - : "off"; - const a = - state.mode === "atlas" && state.tab === "activities" - ? state.activity - : null; - $("#play").disabled = !a; - $("#restart").disabled = !a; - $("#speed").disabled = !a; - $("#playhead").disabled = !a; - $("#play").textContent = state.playing ? "Ⅱ" : "▶"; - $("#play").setAttribute( - "aria-label", - state.playing ? "Pause activity" : "Play activity", - ); - $("#timeline-kicker").textContent = - state.mode === "synapse" - ? "A SMALLER SCALE" - : state.mode === "circuit" - ? "THE NEURAL CIRCUIT" - : a - ? "GUIDED STORY · ILLUSTRATIVE" - : "FOLLOW A SIGNAL"; - $("#timeline-name").textContent = - state.mode === "synapse" - ? "A chemical signal, step by step" - : state.mode === "circuit" - ? "Two populations. A changing balance." - : a - ? a.title - : "Choose an activity to bring the atlas to life"; - $("#playhead").max = a?.duration || 60; - $("#playhead").value = a ? state.time : 0; - $("#elapsed").textContent = fmt(a ? state.time : 0); - $("#duration").textContent = a ? fmt(a.duration) : "—"; - $("#chapter-caption").textContent = a - ? a.chapters[state.chapter]?.title || "Ready to explore" - : state.mode === "circuit" - ? "Use the controls to run the Rust model" - : state.mode === "synapse" - ? "Use Previous and Next to follow the mechanism" - : "Explore at your own pace"; - $("#chapter-ticks").innerHTML = a - ? a.chapters - .map( - (c, i) => - ``, - ) - .join("") - : ""; -} -function seek(t) { - if (!state.activity) return; - state.time = Math.min(state.activity.duration, Math.max(0, t)); - showChapter(); - updateActivity(); - updateTimeline(); - dirty = true; -} -function openSynapse() { - state.parent = state.activity - ? { - id: state.activity.id, - time: state.time, - camera: state.camera, - view: state.view, - } - : null; - state.chemical = - state.chemical || - content.chemicals.find( - (c) => c.id === (state.activity?.chemical || "dopamine"), - ); - state.mode = "synapse"; - state.tab = "chemicals"; - renderLeft(); - state.playing = false; - state.circuitPlaying = false; - state.step = 1; - state.microPlaying = !state.reduced; - state.microElapsed = 0; - state.microClock = 0; - openExplorer(false); - openInfo(false); - setMode(); - showSynapse(); - updateTimeline(); -} -function showSynapse() { - refreshMicro(); - renderer?.micro_state(state.step, 0, 0); - const c = state.chemical, - keys = ["synthesis", "release", "receptors", "clearance"], - k = keys[state.step]; - writeDetails( - `synapse-${c.id}`, - `${c.name} synapse`, - `
${c.name.toUpperCase()} / SYNAPSE

${["Make the
messenger.", "Release the
signal.", "Meet the
receptor.", "End and
recycle."][state.step]}

STEP ${state.step + 1} OF 4 · ${k.toUpperCase()}

${esc(c[k])}

${state.step === 1 ? "In this conventional chemical-synapse schematic, an arriving electrical signal triggers calcium-dependent vesicle release. Actual release can also occur outside a neatly bounded synaptic cleft." : state.step === 2 ? (c.id === "dopamine" ? "Metabotropic receptors act through intracellular signaling; these symbols are not open ion channels." : c.id === "glutamate" ? "Glutamate has both ionotropic and metabotropic receptor families. The pore here illustrates the ionotropic case." : "GABA has ionotropic and metabotropic receptors. This pore represents an ionotropic receptor; the effect depends on ionic gradients.") : ""}

${state.parent ? '' : ""}

A generic teaching diagram. Dots represent messenger molecules without implying counts, concentrations, speed or scale. This is an authored mechanism lesson.

${evidence(c.sources.slice(0, 2).concat(["neurotransmitters"]))}`, - { activate: false }, - ); - dirty = true; -} -function chooseSynapseStep(step) { - state.step = Math.max(0, Math.min(3, step)); - state.microElapsed = 0; - state.microClock = 0; - showSynapse(); - dirty = true; -} -function equations() { - writeDetails( - "equations", - "Model evidence", - `
RUST MODEL / PEDAGOGICAL PARAMETERS

Two populations.
One feedback system.

dE/dt = (−E + S(10E − gI + P − 2.5)) / 0.08\ndI/dt = (−I + S(10E − 3I − 2)) / 0.12\nS(x)  = 1 / (1 + exp(−x))

E and I are normalized excitatory and inhibitory population states. P is external drive (0–5). g is inhibitory coupling (3–15). Initial E = I = 0.1. Time is a model time coordinate in seconds, not a fitted biological timescale.

Integration uses fixed-step Heun at 0.001 s, independent of display frame rate. Long background pauses do not fast-forward the model. The plot keeps the last 15 model seconds. This two-population teaching adaptation is inspired by Wilson–Cowan; it omits refractory terms and is not a reproduction of a fitted system.

Send a signal adds a teaching input of 2 drive units with a 0.3-model-second exponential decay, capped at drive 5. The traveling pulses visualize population activity; this model does not compute individual action potentials. No neurotransmitter concentrations, synaptic plasticity, or atlas coupling are modeled.

${evidence(["wilson"])}`, - { reference: true, reveal: true }, - ); -} -function drawOverlay(t) { - const w = overlay.width, - h = overlay.height, - d = w / Math.max(1, overlay.clientWidth); - ctx.clearRect(0, 0, w, h); - ctx.save(); - ctx.scale(d, d); - const W = w / d, - H = h / d; - ctx.font = '10px "IBM Plex Mono", monospace'; - ctx.lineWidth = 1; - if (state.mode === "synapse" && !renderer) drawSynapse(W, H, t); - else if (state.mode === "circuit" && !renderer) drawCircuit(W, H); - else if (state.mode !== "atlas") { - } else if (renderer) { - let ids = state.region - ? [state.region] - : state.tab === "activities" && state.activity - ? state.activity.chapters[state.chapter]?.regions || [] - : state.tab === "chemicals" - ? state.chemical?.regions || [] - : []; - const points = ids - .map((id) => { - const r = regionBy(id); - if (!r) return null; - const p = renderer.project(...(r.leftCentroid || r.centroid)); - return { r, x: p[0] / d, y: p[1] / d, z: p[2] }; - }) - .filter( - (p) => - p && - p.z > 0 && - p.z < 1 && - p.x > 10 && - p.x < W - 10 && - p.y > 50 && - p.y < H - 90, - ); - if (state.paths && !state.region && points.length > 1) { - ctx.strokeStyle = "#aaaaaa55"; - ctx.setLineDash([3, 6]); - ctx.beginPath(); - points.forEach((p, i) => - i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y), - ); - ctx.stroke(); - ctx.setLineDash([]); - } - const labelY = new Map(); - for (const side of [-1, 1]) { - const group = points - .filter((p) => (p.x < W / 2 ? -1 : 1) === side) - .sort((a, b) => a.y - b.y); - let previous = 65; - for (const p of group) { - const y = Math.max(previous + 28, p.y - 24); - labelY.set(p, y); - previous = y; - } - const overflow = Math.max(0, previous - (H - 120)); - for (const p of group) labelY.set(p, labelY.get(p) - overflow); - } - points.forEach((p, i) => { - ctx.fillStyle = "#eee"; - ctx.beginPath(); - ctx.arc(p.x, p.y, 2, 0, Math.PI * 2); - ctx.fill(); - const dir = p.x < W / 2 ? -1 : 1, - len = Math.min(60, W * 0.1), - y = labelY.get(p); - ctx.strokeStyle = "#ffffff66"; - ctx.beginPath(); - ctx.moveTo(p.x, p.y); - ctx.lineTo(p.x + dir * 20, y); - ctx.lineTo(p.x + dir * len, y); - ctx.stroke(); - ctx.font = '10px "DM Sans",sans-serif'; - const name = p.r.name; - const tw = ctx.measureText(name).width; - const x = Math.min( - W - tw - 15, - Math.max(15, dir < 0 ? p.x - len - tw - 6 : p.x + len + 6), - ); - ctx.fillStyle = "#090909dc"; - ctx.fillRect(x - 5, y - 9, tw + 10, 20); - ctx.fillStyle = "#ddd"; - ctx.fillText(name, x, y + 4); - }); - } - ctx.restore(); - if (state.mode === "atlas" && state.activity && state.paths) - drawNarrativeFlow(); -} -function drawNarrativeFlow() { - if (!renderer) return; - const ids = [...new Set(state.activity.chapters.flatMap((ch) => ch.regions))]; - if (ids.length < 2) return; - const d = overlay.width / overlay.clientWidth; - const points = ids - .map((id) => renderer.project(...regionBy(id).leftCentroid)) - .map((p) => [p[0] / d, p[1] / d]); - ctx.save(); - ctx.scale(d, d); - ctx.lineWidth = 1; - ctx.strokeStyle = "#ffffff3a"; - for (let i = 0; i < points.length - 1; i++) { - const [x, y] = points[i], - [a, b] = points[i + 1]; - const mx = (x + a) / 2 + 45, - my = (y + b) / 2 - 35; - ctx.setLineDash([2, 5]); - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.quadraticCurveTo(mx, my, a, b); - ctx.stroke(); - ctx.setLineDash([]); - if (!state.reduced) { - const t = (state.time * 0.22 + i * 0.3) % 1, - u = 1 - t; - dot( - u * u * x + 2 * u * t * mx + t * t * a, - u * u * y + 2 * u * t * my + t * t * b, - 2.1, - true, - ); - } - } - ctx.restore(); -} -function drawSynapse(W, H, t) { - const center = W * 0.5, - top = H * 0.31, - bottom = H * 0.62, - left = W * 0.14, - right = W * 0.86; - ctx.strokeStyle = "#8b8b8b"; - ctx.fillStyle = "#171717"; - ctx.lineWidth = 1.2; - const terminal = () => { - ctx.beginPath(); - ctx.moveTo(left, 100); - ctx.bezierCurveTo( - left - 15, - top + 10, - left + 15, - top + 35, - center, - top + 35, - ); - ctx.bezierCurveTo(right - 15, top + 35, right + 15, top + 10, right, 100); - ctx.stroke(); - }; - terminal(); - ctx.strokeStyle = "#333"; - ctx.beginPath(); - ctx.moveTo(left, 105); - ctx.bezierCurveTo(left - 8, top + 4, left + 20, top + 29, center, top + 29); - ctx.bezierCurveTo(right - 20, top + 29, right + 8, top + 4, right, 105); - ctx.stroke(); - ctx.fillStyle = "#101010"; - ctx.fillRect(0, bottom, W, H - bottom); - ctx.strokeStyle = "#999"; - ctx.beginPath(); - ctx.moveTo(0, bottom); - ctx.bezierCurveTo(W * 0.3, bottom - 12, W * 0.7, bottom + 12, W, bottom); - ctx.stroke(); - ctx.strokeStyle = "#333"; - ctx.beginPath(); - ctx.moveTo(0, bottom + 6); - ctx.bezierCurveTo(W * 0.3, bottom - 6, W * 0.7, bottom + 18, W, bottom + 6); - ctx.stroke(); - ctx.fillStyle = "#777"; - ctx.font = '9px "IBM Plex Mono",monospace'; - ctx.fillText("PRESYNAPTIC TERMINAL", left, 85); - ctx.fillText("POSTSYNAPTIC MEMBRANE", left, bottom + 65); - ctx.fillStyle = "#555"; - ctx.textAlign = "center"; - ctx.fillText("SYNAPTIC CLEFT", center, (top + 35 + bottom) / 2); - ctx.textAlign = "left"; - for (let i = 0; i < 5; i++) { - let x = left + 30 + (i * (right - left - 60)) / 4, - y = top - 38 + Math.sin(i * 2) * 18; - ctx.strokeStyle = i === 2 && state.step === 1 ? "#fff" : "#555"; - ctx.beginPath(); - ctx.arc(x, y, 18, 0, Math.PI * 2); - ctx.stroke(); - for (let j = 0; j < 5; j++) { - const a = j * 2.4; - dot(x + Math.cos(a) * 8, y + Math.sin(a) * 8, 1.8, state.step === 0); - } - } - for (let i = 0; i < 4; i++) { - const x = W * 0.28 + i * W * 0.15; - ctx.fillStyle = "#222"; - ctx.strokeStyle = state.step === 2 ? "#eee" : "#666"; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.roundRect(x - 7, bottom - 13, 14, 27, 4); - ctx.fill(); - ctx.stroke(); - if (state.chemical.id !== "dopamine") { - ctx.strokeStyle = "#090909"; - ctx.beginPath(); - ctx.moveTo(x, bottom - 13); - ctx.lineTo(x, bottom + 14); - ctx.stroke(); - } - if (state.step === 2) { - dot(x, bottom - 21, 3, true); - ctx.strokeStyle = "#888"; - ctx.setLineDash([2, 4]); - ctx.beginPath(); - ctx.moveTo(x, bottom + 16); - ctx.lineTo(x, bottom + 43); - ctx.stroke(); - ctx.setLineDash([]); - } - } - if (state.step === 1 || state.step === 3) { - for (let i = 0; i < 15; i++) { - const phase = state.reduced ? 0.5 : (t * 0.24 + i * 0.067) % 1, - x = center + Math.sin(i * 13) * W * 0.2, - y = - state.step === 1 - ? top + 40 + phase * (bottom - top - 65) - : bottom - 20 - phase * (bottom - top - 55); - dot(x + (state.step === 3 ? phase * 35 : 0), y, 2.3, true); - } - } - ctx.fillStyle = "#aaa"; - ctx.font = '11px "DM Sans",sans-serif'; - ctx.fillText( - state.step === 0 - ? "Vesicles package the messenger" - : state.step === 1 - ? "Release into extracellular space" - : state.step === 2 - ? state.chemical.id === "dopamine" - ? "Receptor → intracellular signaling" - : "Receptor binding changes signaling" - : "Transporters and metabolism regulate the signal", - left, - H - 80, - ); - ctx.fillStyle = "#666"; - ctx.font = '8px "IBM Plex Mono",monospace'; - ctx.fillText( - `${state.chemical.symbol} · ${["SYNTHESIS", "RELEASE", "RECEPTOR ACTION", "CLEARANCE"][state.step]} · ILLUSTRATIVE`, - left, - H - 57, - ); -} -function dot(x, y, r, glow) { - ctx.save(); - ctx.fillStyle = glow ? "#fff" : "#aaa"; - if (glow) { - ctx.shadowColor = "#fff"; - ctx.shadowBlur = 12; - } - ctx.beginPath(); - ctx.arc(x, y, r, 0, Math.PI * 2); - ctx.fill(); - ctx.restore(); -} -function drawCircuit(W, H) { - const a = trace.at(-1) || [0, 0.1, 0.1], - cy = H * 0.28, - r = Math.min(W * 0.115, 62), - xs = [W * 0.3, W * 0.7]; - ctx.textAlign = "center"; - xs.forEach((x, n) => { - const v = a[n + 1]; - ctx.strokeStyle = n ? "#777" : "#ccc"; - ctx.setLineDash(n ? [4, 4] : []); - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.arc(x, cy, r, 0, Math.PI * 2); - ctx.stroke(); - ctx.setLineDash([]); - for (let i = 0; i < 22; i++) { - const angle = i * 2.399, - rr = Math.sqrt((i + 0.5) / 22) * r * 0.78; - dot( - x + Math.cos(angle) * rr, - cy + Math.sin(angle) * rr, - 1.3 + v * 1.3, - !state.reduced && v > 0.4, - ); - } - ctx.fillStyle = "#ddd"; - ctx.font = "24px Georgia,serif"; - ctx.fillText(n ? "I" : "E", x, cy + r + 38); - ctx.font = '9px "IBM Plex Mono",monospace'; - ctx.fillStyle = "#888"; - ctx.fillText( - `${n ? "INHIBITORY" : "EXCITATORY"} · ${v.toFixed(3)}`, - x, - cy + r + 62, - ); - }); - ctx.strokeStyle = "#777"; - ctx.beginPath(); - ctx.moveTo(xs[0] + r + 10, cy - 12); - ctx.quadraticCurveTo(W * 0.5, cy - 48, xs[1] - r - 10, cy - 12); - ctx.stroke(); - ctx.fillStyle = "#aaa"; - ctx.font = "11px monospace"; - ctx.fillText("+", W * 0.5, cy - 36); - ctx.setLineDash([4, 4]); - ctx.beginPath(); - ctx.moveTo(xs[1] - r - 10, cy + 12); - ctx.quadraticCurveTo(W * 0.5, cy + 48, xs[0] + r + 10, cy + 12); - ctx.stroke(); - ctx.setLineDash([]); - ctx.fillText("−", W * 0.5, cy + 42); - const x0 = 55, - y0 = H * 0.6, - pw = W - 95, - ph = H * 0.23; - ctx.textAlign = "right"; - ctx.font = '8px "IBM Plex Mono",monospace'; - for (let i = 0; i <= 4; i++) { - const y = y0 + (i * ph) / 4; - ctx.strokeStyle = "#242424"; - ctx.beginPath(); - ctx.moveTo(x0, y); - ctx.lineTo(x0 + pw, y); - ctx.stroke(); - ctx.fillStyle = "#777"; - ctx.fillText((1 - i / 4).toFixed(2), x0 - 10, y + 3); - } - const end = Math.max(15, a[0]), - start = end - 15; - [1, 2].forEach((k) => { - ctx.strokeStyle = k === 1 ? "#eee" : "#888"; - ctx.setLineDash(k === 1 ? [] : [5, 4]); - ctx.lineWidth = 1.6; - ctx.beginPath(); - trace.forEach((p, i) => { - const x = x0 + ((p[0] - start) / 15) * pw, - y = y0 + (1 - p[k]) * ph; - i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); - }); - ctx.stroke(); - ctx.setLineDash([]); - }); - ctx.textAlign = "left"; - ctx.fillStyle = "#aaa"; - ctx.font = '9px "IBM Plex Mono",monospace'; - ctx.fillText("NORMALIZED POPULATION STATE", x0, y0 - 20); - ctx.fillStyle = "#666"; - ctx.fillText(`${start.toFixed(1)} s`, x0, y0 + ph + 20); - ctx.textAlign = "right"; - ctx.fillText(`${end.toFixed(1)} s`, x0 + pw, y0 + ph + 20); - ctx.textAlign = "left"; - ctx.fillText("— E ┄ I · MODEL TIME", x0, H - 48); -} -function bind() { - document.addEventListener("click", (e) => { - let b = e.target.closest("button"); - if (!b) return; - if (b.dataset.tab) setTab(b.dataset.tab); - else if (b.dataset.region) selectRegion(b.dataset.region); - else if (b.dataset.source) showSource(b.dataset.source); - else if (b.dataset.activity) selectActivity(b.dataset.activity); - else if (b.dataset.chemical) { - if (state.mode === "molecule") openMolecule(b.dataset.chemical); - else selectChemical(b.dataset.chemical); - } else if (b.hasAttribute("data-synapse")) openSynapse(); - else if (b.hasAttribute("data-back-story")) showChapter(true); - else if (b.hasAttribute("data-return-activity") && state.parent) { - const parent = state.parent; - selectActivity(parent.id, parent.time); - setView(parent.view); - applyCamera(parent.camera); - } else if (b.dataset.focus) { - const r = regionBy(b.dataset.focus); - renderer?.focus(...(r.leftCentroid || r.centroid)); - dirty = true; - } else if (b.hasAttribute("data-chapter")) { - seek( - (+b.dataset.chapter * state.activity.duration) / - state.activity.chapters.length + - 0.05, - ); - } else if (b.dataset.pan) { - renderer?.pan(...b.dataset.pan.split(",").map((n) => +n * 0.04)); - dirty = true; - } else if (b.id === "start-music") selectActivity("music"); - else if (b.id === "equations") equations(); - else if (b.id === "syn-prev") chooseSynapseStep(state.step - 1); - else if (b.id === "syn-next") chooseSynapseStep(state.step + 1); - }); - $("#about-button").onclick = about; - $(".left-foot").onclick = () => showSource("allen"); - $("#view-whole").onclick = () => setView("whole"); - $("#view-deep").onclick = () => setView("deep"); - $("#view-cut").onclick = () => setView("cut"); - $("#hemisphere").onchange = () => setView(state.view); - $("#cut-depth").oninput = () => setView("cut"); - $("#camera-preset").onchange = (e) => applyCamera(+e.target.value); - $("#reset-view").onclick = () => applyCamera(0); - $("#zoom-in").onclick = () => { - renderer?.zoom(-0.15); - dirty = true; - }; - $("#zoom-out").onclick = () => { - renderer?.zoom(0.15); - dirty = true; - }; - $("#settings-button").onclick = () => { - const opening = $("#view-settings").hidden; - if (opening && innerWidth < 821) { - openExplorer(false); - openInfo(false); - } - $("#view-settings").hidden = !opening; - }; - $("#glow-control").oninput = (e) => { - renderer?.glow(+e.target.value); - dirty = true; - }; - $("#subtle-colors").onchange = (e) => { - state.color = e.target.checked; - renderer?.accents(state.color); - document.body.dataset.color = state.color ? "on" : "off"; - dirty = true; - }; - $("#reduced-motion").checked = state.reduced; - $("#reduced-motion").onchange = (e) => { - state.reduced = e.target.checked; - if (state.reduced) { - state.microPlaying = false; - state.circuitPlaying = false; - } - refreshMicro(); - updateActivity(); - dirty = true; - }; - $("#show-paths").onchange = (e) => { - state.paths = e.target.checked; - dirty = true; - }; - $("#quality").onchange = (e) => changeQuality(e.target.value); - $("#play").onclick = () => { - if (!state.activity) return; - if (state.time >= state.activity.duration) seek(0); - state.playing = !state.playing; - updateTimeline(); - }; - $("#playhead").oninput = (e) => seek(+e.target.value); - $("#restart").onclick = () => seek(0); - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") $("#view-settings").hidden = true; - if (e.key === "/" && !["INPUT", "TEXTAREA"].includes(e.target.tagName)) { - $("#region-search")?.focus(); - e.preventDefault(); - } - }); - document.addEventListener("visibilitychange", () => { - last = 0; - }); - window.addEventListener("popstate", restoreRoute); -} -function bindCanvas() { - const c = canvas(), - pointers = new Map(); - let start = null, - moved = 0; - c.onpointerdown = (e) => { - interactionTime = performance.now(); - pointers.set(e.pointerId, [e.clientX, e.clientY]); - start = [e.clientX, e.clientY]; - moved = 0; - c.setPointerCapture(e.pointerId); - }; - c.onpointermove = (e) => { - const prev = pointers.get(e.pointerId); - if (!prev) return; - const dx = e.clientX - prev[0], - dy = e.clientY - prev[1]; - moved += Math.abs(dx) + Math.abs(dy); - if (pointers.size === 2) { - const other = [...pointers.entries()].find( - ([id]) => id !== e.pointerId, - )[1]; - const old = Math.hypot(prev[0] - other[0], prev[1] - other[1]), - now = Math.hypot(e.clientX - other[0], e.clientY - other[1]); - if (old > 0 && now > 0) renderer?.zoom(Math.log(old / now)); - renderer?.pan(dx * 0.0007, -dy * 0.0007); - } else if (e.shiftKey || e.buttons === 2) - renderer?.pan(dx * 0.001, -dy * 0.001); - else renderer?.orbit(-dx * 0.005, dy * 0.005); - pointers.set(e.pointerId, [e.clientX, e.clientY]); - dirty = true; - }; - c.onpointerup = (e) => { - if (pointers.size === 1 && moved < 6 && start && renderer) { - const r = c.getBoundingClientRect(), - n = renderer.pick( - (e.clientX - r.left) / r.width, - (e.clientY - r.top) / r.height, - ); - const region = atlas.regions.find((r) => r.index === n); - if (state.mode === "atlas" && region) selectRegion(region.id); - else if (state.mode !== "atlas") openInfo(true); - } - pointers.delete(e.pointerId); - }; - c.onpointercancel = (e) => pointers.delete(e.pointerId); - c.oncontextmenu = (e) => e.preventDefault(); - c.onwheel = (e) => { - interactionTime = performance.now(); - e.preventDefault(); - renderer?.zoom(e.deltaY * 0.001); - dirty = true; - }; - c.onkeydown = (e) => { - const k = e.key; - if ( - [ - "ArrowLeft", - "ArrowRight", - "ArrowUp", - "ArrowDown", - "+", - "-", - "Home", - ].includes(k) - ) { - e.preventDefault(); - if (k === "Home") applyCamera(0); - else if (k === "+" || k === "-") renderer?.zoom(k === "+" ? -0.12 : 0.12); - else { - const dx = k === "ArrowLeft" ? -0.08 : k === "ArrowRight" ? 0.08 : 0, - dy = k === "ArrowUp" ? -0.08 : k === "ArrowDown" ? 0.08 : 0; - e.shiftKey ? renderer?.pan(dx, dy) : renderer?.orbit(dx, dy); - } - dirty = true; - } - }; -} -async function changeQuality(q) { - if (state.mode !== "atlas") { - toast("Detail levels apply to the brain atlas."); - return; - } - const token = ++loadToken, - previous = state.quality; - detailAbort?.abort(); - detailAbort = new AbortController(); - $("#load-progress").hidden = false; - $("#load-progress span").textContent = - q === "high" ? "Native anatomy · preparing…" : "Loading anatomy…"; - $("#load-progress progress").value = 0; - try { - let bytes, count; - if (q === "high" || q === "fine") { - const data = await loadNative( - (progress, m) => { - $("#load-progress progress").value = progress; - $("#load-progress span").textContent = - `${q === "high" ? "Native 0.5 mm" : "Fine detail"} · ${Math.round(progress * 100)}% of ${Math.round(m.compressedBytes / 1e6)} MB`; - }, - detailAbort.signal, - q === "high" ? "native" : "fine", - ); - bytes = data.bytes; - count = data.meta.triangles; - } else { - const res = await fetch("assets/brain.bin", { - signal: detailAbort.signal, - }); - if (!res.ok) throw Error("Anatomy unavailable"); - bytes = new Uint8Array(await res.arrayBuffer()); - count = atlas.triangles; - } - if (token !== loadToken || state.mode !== "atlas") return; - renderer?.load_mesh(bytes); - atlasBytes = bytes; - state.quality = q; - resizeView(); - $("#render-stats").textContent = - `${renderer?.backend()} · Rust · ${count.toLocaleString()} triangles`; - toast(q === "high" ? "Native-resolution anatomy ready" : "Detail updated"); - dirty = true; - } catch (e) { - $("#quality").value = previous; - toast(e.name === "AbortError" ? "Detail download canceled" : e.message); - } finally { - if (token === loadToken) $("#load-progress").hidden = true; - } -} -function restoreRoute() { - const p = new URLSearchParams(location.search); - if (p.has("v") && p.get("v") !== content.version) { - toast( - "This link uses another content version; showing the current edition.", - ); - } - const tab = p.get("tab") || "explore"; - setTab(tab); - if (p.has("activity")) - selectActivity( - p.get("activity"), - Number.isFinite(+p.get("t")) ? +p.get("t") : 0, - ); - if (p.has("chemical")) selectChemical(p.get("chemical")); - if (p.has("region")) selectRegion(p.get("region")); - if (["whole", "deep", "cut"].includes(p.get("view"))) setView(p.get("view")); - if (p.get("mode") === "molecule") openMolecule(p.get("chemical")); - if (p.get("mode") === "synapse") { - openSynapse(); - state.step = Math.max(0, Math.min(3, parseInt(p.get("step")) || 0)); - if ( - p.has("parent") && - content.activities.some((a) => a.id === p.get("parent")) - ) - state.parent = { - id: p.get("parent"), - time: Math.max(0, Math.min(60, +p.get("parentTime") || 0)), - camera: 5, - view: "deep", - }; - showSynapse(); - } - if ( - p.has("camera") && - Number.isInteger(+p.get("camera")) && - +p.get("camera") >= 0 && - +p.get("camera") <= 5 - ) - applyCamera(+p.get("camera")); - if (["0", "2", "3"].includes(p.get("hemisphere"))) - $("#hemisphere").value = p.get("hemisphere"); - if (p.has("cut") && Number.isFinite(+p.get("cut"))) - $("#cut-depth").value = Math.max(-0.8, Math.min(0.8, +p.get("cut"))); - setView(state.view); - if (tab === "circuit") { - const drive = +p.get("drive"), - inh = +p.get("inhibition"); - if (p.has("drive") && Number.isFinite(drive)) - state.drive = Math.max(0, Math.min(5, drive)); - if (p.has("inhibition") && Number.isFinite(inh)) - state.inhibition = Math.max(3, Math.min(15, inh)); - renderLeft(); - } - state.microPlaying = false; - state.circuitPlaying = false; - refreshMicro(); - dirty = true; -} -async function boot() { - [atlas, content] = await Promise.all([ - fetch("assets/atlas.json").then((r) => { - if (!r.ok) throw Error("Atlas metadata"); - return r.json(); - }), - fetch("content.json").then((r) => { - if (!r.ok) throw Error("Content"); - return r.json(); - }), - ]); - bind(); - bindImmersive(); - renderLeft(); - restoreRoute(); - const resize = () => { - const c = canvas(), - r = c.getBoundingClientRect(), - d = - state.quality === "low" - ? 1 - : Math.min(devicePixelRatio, state.quality === "high" ? 2 : 1.5); - const w = Math.max(1, Math.min(4096, Math.round(r.width * d))), - h = Math.max(1, Math.min(4096, Math.round(r.height * d))); - c.width = w; - c.height = h; - overlay.width = w; - overlay.height = h; - renderer?.resize(w, h); - if (renderer?.hdr_enabled()) { - hdrActive = configureHDR(canvas(), renderer, state.light === "auto"); - renderer.hdr_output(hdrActive); - } - dirty = true; - }; - resizeView = resize; - new ResizeObserver(resize).observe($(".stage")); - resize(); - try { - wasm = await import("./pkg/doke.js"); - await wasm.default(); - circuit = new wasm.Circuit(); - if (state.tab === "circuit") renderLeft(); - const forceGL = - !navigator.gpu || - new URLSearchParams(location.search).get("backend") === "webgl"; - try { - renderer = await wasm.BrainRenderer.create( - canvas(), - forceGL, - hdrCapable() && state.light === "auto", - ); - } catch (e) { - if (forceGL) throw e; - canvas().replaceWith(canvas().cloneNode()); - renderer = await wasm.BrainRenderer.create(canvas(), true, false); - } - const res = await fetch("assets/brain.bin"); - if (!res.ok) throw Error("Anatomy asset unavailable"); - atlasBytes = new Uint8Array(await res.arrayBuffer()); - renderer.load_mesh(atlasBytes); - hdrActive = configureHDR(canvas(), renderer, state.light === "auto"); - renderer.hdr_output(hdrActive); - if (hdrActive) { - try { - hdrText = new HDRText( - canvas().getContext("webgpu").getConfiguration().device, - ); - } catch (e) { - console.warn("HDR text unavailable", e); - } - } - document.body.dataset.light = hdrActive ? "hdr" : "sdr"; - $("#hdr-status").textContent = hdrActive - ? "Extended-range canvas active. Brightness depends on your display and system settings." - : "SDR contrast + bloom. Your screen’s physical brightness range is unchanged."; - $("#render-stats").textContent = - `${renderer.backend()} · Rust · ${atlas.triangles.toLocaleString()} triangles`; - $("#scene-loading").hidden = true; - resize(); - bindCanvas(); - setView(state.view); - applyCamera(state.camera); - if (state.region) renderer.select(regionBy(state.region).index); - updateActivity(); - } catch (e) { - gpuError = String(e); - console.warn("3D view unavailable:", gpuError); - renderer = null; - $("#scene-loading").innerHTML = - 'Reference brain surface, static preview

Explore through the explanations.

LIVE 3D IS UNAVAILABLE ON THIS DEVICEThe anatomy list, activities, chemistry and sources remain available.
'; - $("#scene-loading").hidden = state.mode !== "atlas"; - $("#render-stats").textContent = "Text explorer · live 3D unavailable"; - bindCanvas(); - } - if (state.mode !== "atlas") { - setMode(); - } - updateTimeline(); - document.documentElement.dataset.ready = "true"; - requestAnimationFrame(frame); -} -function frame(t) { - const dt = last ? Math.min((t - last) / 1000, 0.1) : 0; - last = t; - if (!document.hidden) { - if (state.playing && state.activity && state.mode === "atlas") { - state.time = Math.min( - state.activity.duration, - state.time + dt * +$("#speed").value, - ); - showChapter(); - updateActivity(); - if (t - lastUI > 80) { - updateTimeline(); - lastUI = t; - } - if (state.time >= state.activity.duration) { - state.playing = false; - updateTimeline(); - } - dirty = true; - } - if (state.circuitPlaying && state.mode === "circuit" && circuit) { - clock += dt; - const n = Math.floor(clock / 0.001); - clock -= n * 0.001; - if (n) { - const p = circuit.advance( - n, - Math.min(5, state.drive + state.pulse), - state.inhibition, - ); - state.pulse *= Math.exp((-n * 0.001) / 0.3); - trace.push([...p]); - if (t - lastUI > 100 && $("#model-readout")) { - $("#model-readout").textContent = - `Model time ${p[0].toFixed(3)} s · E ${p[1].toFixed(3)} · I ${p[2].toFixed(3)}`; - lastUI = t; - } - while (trace.length && trace[0][0] < p[0] - 15) trace.shift(); - dirty = true; - } - } - if (state.mode === "synapse" && state.microPlaying) { - state.microClock += dt; - state.microElapsed += dt; - if (state.microElapsed >= 5) { - state.step = (state.step + 1) % 4; - state.microElapsed = 0; - showSynapse(); - } - renderer?.micro_state(state.step, 0, 0); - dirty = true; - } - if (state.mode === "circuit") { - const value = trace.at(-1) || [0, 0.1, 0.1]; - renderer?.micro_state(0, value[1], value[2]); - if (state.circuitPlaying) state.microClock += dt; - } - const animateMicro = - (state.mode === "synapse" && state.microPlaying) || - (state.mode === "circuit" && state.circuitPlaying); - if (state.mode === "atlas" && state.neurons && !state.reduced) dirty = true; - const floating = - state.mode === "atlas" && !state.reduced && t - interactionTime > 3500; - if (floating) { - renderer?.orbit(dt * 0.012, Math.cos(t * 0.0004) * dt * 0.003); - dirty = true; - } - if (dirty || animateMicro) { - if (renderer) { - try { - renderer.render( - state.mode === "atlas" - ? state.reduced - ? 0 - : t * 0.001 - : state.microClock, - ); - } catch (e) { - state.playing = false; - toast("The graphics device was interrupted. Reload to restore 3D."); - renderer = null; - } - } - drawOverlay(t * 0.001); - drawHotspots(); - if (hdrText && textDirty) { - hdrText.redraw(); - textDirty = false; - } - dirty = false; - } - } - requestAnimationFrame(frame); -} -function bindImmersive() { - document.body.dataset.explorer = "closed"; - document.body.dataset.info = "closed"; - document.body.dataset.color = "on"; - $("#info-panel").append($(".left-panel"), $("#details")); - $("#panel-browse").onclick = () => openExplorer(); - $("#close-tab").onclick = closePanelTab; - $("#panel-tabs").onkeydown = (e) => { - if ( - !e.target.matches('[role="tab"]') || - !["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key) - ) - return; - e.preventDefault(); - const tabs = [...$("#panel-tabs").querySelectorAll('[role="tab"]')]; - const index = tabs.indexOf(e.target); - const next = - e.key === "Home" - ? tabs[0] - : e.key === "End" - ? tabs.at(-1) - : tabs[ - (index + (e.key === "ArrowRight" ? 1 : -1) + tabs.length) % - tabs.length - ]; - next.click(); - next.focus(); - }; - $("#close-panel").onclick = () => { - openExplorer(false); - openInfo(false); - }; - $("#neuron-layer").onclick = toggleNeurons; - syncPanel(); - $("#scene-labels").onpointerover = () => { - interactionTime = performance.now(); - }; - $("#close-explorer").onclick = () => openExplorer(false); - $("#close-info").onclick = () => openInfo(false); - $("#cancel-detail").onclick = () => detailAbort?.abort(); - $("#micro-play").onclick = () => { - if (state.mode === "circuit") { - state.circuitPlaying = !state.circuitPlaying; - renderLeft(); - } else state.microPlaying = !state.microPlaying; - last = performance.now(); - dirty = true; - refreshMicro(); - }; - $("#inspect-molecule").onclick = () => openMolecule(); - $("#focus-cleft").onclick = () => { - const button = $("#focus-cleft"); - if (button.dataset.focused === "true") { - renderer?.view(0); - button.dataset.focused = "false"; - button.textContent = "Focus cleft ⊕"; - } else { - renderer?.focus(0, 0.02, 0.15); - button.dataset.focused = "true"; - button.textContent = "Whole synapse ↺"; - } - dirty = true; - }; - $("#micro-info").onclick = () => openInfo(true); - $("#light-mode").onchange = (e) => { - state.light = e.target.value; - document.body.dataset.light = - state.light === "quiet" - ? "quiet" - : hdrActive && state.light === "auto" - ? "hdr" - : "sdr"; - renderer?.glow( - state.light === "quiet" ? 0.1 : state.light === "sdr" ? 1.6 : 2.2, - ); - if (hdrText) hdrText.canvas.hidden = state.light !== "auto"; - if (renderer?.hdr_enabled()) { - hdrActive = configureHDR(canvas(), renderer, state.light === "auto"); - renderer.hdr_output(hdrActive); - } - document.body.dataset.light = - state.light === "quiet" ? "quiet" : hdrActive ? "hdr" : "sdr"; - $("#hdr-status").textContent = hdrActive - ? "Extended-range canvas active. Brightness depends on your display and system settings." - : state.light === "quiet" - ? "Quiet light: reduced bloom, standard display range." - : "SDR contrast + bloom. Your screen’s physical brightness range is unchanged."; - dirty = true; - }; - document.addEventListener("click", (e) => { - const b = e.target.closest("button"); - if (!b) return; - if (b.dataset.tab) { - openExplorer(true); - if (b.dataset.tab !== "chemicals") openInfo(false); - } - if (b.dataset.scene) { - if (b.dataset.scene === "atlas") { - state.mode = "atlas"; - state.circuitPlaying = false; - state.microPlaying = false; - setTab("explore"); - openExplorer(false); - openInfo(false); - } else if (b.dataset.scene === "synapse") { - openSynapse(); - } else if (b.dataset.scene === "molecule") { - openMolecule(); - } else { - setTab("circuit"); - state.circuitPlaying = !state.reduced; - state.microClock = 0; - renderLeft(); - openExplorer(false); - openInfo(false); - refreshMicro(); - } - } - if (b.hasAttribute("data-step")) chooseSynapseStep(+b.dataset.step); - if (b.hasAttribute("data-micro-info") || b.hasAttribute("data-atom")) - openInfo(true); - if (b.hasAttribute("data-neuron-info")) neuronInfo(); - if (b.hasAttribute("data-neuron-focus")) { - renderer?.focus(...state.neuronAnchor); - renderer?.zoom(-0.2); - openInfo(false); - dirty = true; - } - if (b.id === "circuit-source") equations(); - if (b.id === "circuit-controls") openExplorer(true); - if (b.id === "stimulate-circuit") { - state.pulse = 2; - state.circuitPlaying = true; - refreshMicro(); - toast("A brief extra input enters the excitatory population."); - } - if ( - b.hasAttribute("data-inspect-molecule") || - b.hasAttribute("data-molecule") - ) - openMolecule(b.dataset.molecule); - if (b.hasAttribute("data-brain-map")) { - state.mode = "atlas"; - setMode(); - setView(state.chemical.regions.length ? "deep" : "whole"); - updateActivity(); - } - }); - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - openInfo(false); - openExplorer(false); - } - if (e.key === "/" && !["INPUT", "TEXTAREA"].includes(e.target.tagName)) { - setTab("explore"); - openExplorer(true); - $("#region-search")?.focus(); - } - }); - const invalidateText = () => { - textDirty = true; - dirty = true; - }; - for (const el of [$("#details"), $(".micro-caption")]) - new MutationObserver(invalidateText).observe(el, { - childList: true, - subtree: true, - characterData: true, - }); - new MutationObserver(invalidateText).observe(document.body, { - attributes: true, - attributeFilter: ["data-info", "data-explorer", "data-scene", "data-light"], - }); - $("#info-panel").addEventListener("scroll", invalidateText, true); - window.addEventListener("resize", invalidateText); - document.fonts.ready.then(invalidateText); -} -boot().catch((e) => { - $("#scene-loading").innerHTML = - "

Content could not load.

Please reload when your connection returns."; - $("#details").innerHTML = - "

Unable to load the atlas.

Please check your connection and reload.

"; - console.error(e); -}); diff --git a/public/architecture.json b/public/architecture.json new file mode 100644 index 0000000..47c043d --- /dev/null +++ b/public/architecture.json @@ -0,0 +1,1952 @@ +{ + "schema_version": 1, + "checked_on": "2026-10-15", + "scope": "Documented architectural structure alongside separately identified educational arithmetic and packet models. Original schematic positions do not represent physical floorplans. Published clocks and bandwidths are reference quantities; model ticks do not predict chip timing, and no neural equivalence is implied.", + "sources": { + "amd-9950x": { + "title": "AMD Ryzen 9 9950X specifications", + "url": "https://www.amd.com/en/products/processors/desktops/ryzen/9000-series/amd-ryzen-9-9950x.html", + "locator": "General specifications: cores, threads, caches, package die count" + }, + "amd-zen5-smt": { + "title": "AMD EPYC and SMT: Zen 5 core diagram", + "url": "https://www.amd.com/content/dam/amd/en/documents/epyc-business-docs/white-papers/amd-epyc-smt-technology-brief.pdf", + "locator": "April 2025, page 8: core front end, execution pipelines and memory subsystem", + "scope": "Zen 5 core mechanisms; server package details do not describe Ryzen." + }, + "amd-zen5-ccd": { + "title": "AMD EPYC 9005 architecture overview", + "url": "https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/user-guides/58462_amd-epyc-9005-tg-architecture-overview.pdf", + "locator": "April 2025, page 4, Figure 2-3: classic Zen 5 core complex", + "scope": "Documents the classic 8-core Zen 5 complex. EPYC package counts and I/O are not Ryzen specifications." + }, + "amd-small-gemm": { + "title": "AMD AOCL-BLAS: small matrix multiplication", + "url": "https://www.amd.com/en/developer/resources/technical-articles/2025/aocl-blas-boosting-gemm-performance-for-small-matrices-.html", + "locator": "Instruction-level parallelism, independent accumulators and instruction-cache pressure" + }, + "boom-fetch": { + "title": "Berkeley BOOM: instruction fetch", + "url": "https://docs.boom-core.org/en/latest/sections/instruction-fetch-stage.html", + "locator": "The front-end, fetch buffer and redirect behavior", + "scope": "Open CPU mechanism reference, not a description of proprietary Zen implementation." + }, + "boom-rename": { + "title": "Berkeley BOOM: register renaming", + "url": "https://docs.boom-core.org/en/latest/sections/rename-stage.html", + "locator": "The purpose of renaming; physical register file; busy table", + "scope": "Generic out-of-order mechanism reference, not Zen register-file implementation." + }, + "boom-issue": { + "title": "Berkeley BOOM: instruction issue", + "url": "https://docs.boom-core.org/en/latest/sections/issue-units.html", + "locator": "Issue slots, select logic and wake-up", + "scope": "Explains readiness and resource constraints. Its scheduling policy is not attributed to AMD." + }, + "boom-retire": { + "title": "Berkeley BOOM: reorder buffer and retirement", + "url": "https://docs.boom-core.org/en/latest/sections/reorder-buffer.html", + "locator": "Opening description and Commit Stage", + "scope": "Mechanism reference, not a Zen reorder-buffer simulation." + }, + "boom-loadstore": { + "title": "Berkeley BOOM: load/store unit", + "url": "https://docs.boom-core.org/en/latest/sections/load-store-unit.html", + "locator": "Load queue, store queue and memory ordering", + "scope": "Generic mechanism reference. Doke does not implement BOOM or Zen memory ordering." + }, + "nvidia-blackwell": { + "title": "NVIDIA RTX Blackwell GPU Architecture", + "url": "https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf", + "locator": "Pages 8–12, Figures 4–6; page 46, RTX 5090 product table", + "scope": "RTX GB20x graphics architecture, distinct from data-center Blackwell." + }, + "nvidia-5090": { + "title": "NVIDIA GeForce RTX 5090 specifications", + "url": "https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/", + "locator": "Full specs: GPU engine and memory specifications" + }, + "nvidia-hopper": { + "title": "NVIDIA Hopper Architecture In-Depth", + "url": "https://developer.nvidia.com/blog/nvidia-hopper-architecture-in-depth/", + "locator": "H100 SXM5 configuration, Figure 3 and Figure 4", + "scope": "SXM5 configuration: not the full GH100 diagram or PCIe variant. Preliminary performance tables are not used." + }, + "cuda-model": { + "title": "CUDA Programming Guide: programming model", + "url": "https://docs.nvidia.com/cuda/cuda-programming-guide/01-introduction/programming-model.html", + "locator": "Sections 1.2.2.1–1.2.2.2: blocks, grids, warps and SIMT" + }, + "cuda-hardware": { + "title": "CUDA Programming Guide: hardware execution", + "url": "https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/advanced-kernel-programming.html", + "locator": "Sections 3.2.2.1–3.2.2.2: SIMT and hardware multithreading" + }, + "cuda-memory": { + "title": "CUDA Best Practices Guide: memory optimization", + "url": "https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html", + "locator": "Sections 10.2.1 and 10.2.3: coalesced global access and shared memory" + }, + "cuda-registers": { + "title": "NVIDIA: register cache for warp-centric programs", + "url": "https://developer.nvidia.com/blog/register-cache-warp-cuda/", + "locator": "Memory hierarchy and register-cache design" + }, + "cuda-sectors": { + "title": "NVIDIA: global memory access in CUDA", + "url": "https://developer.nvidia.com/blog/unlock-gpu-performance-global-memory-access-in-cuda/", + "locator": "Global Memory Coalescing, Figures 1–2; transaction and sector discussion" + }, + "hopper-tuning": { + "title": "NVIDIA Hopper Tuning Guide", + "url": "https://docs.nvidia.com/cuda/hopper-tuning-guide/index.html", + "locator": "Sections 1.4.1.1 and 1.4.2: occupancy and memory hierarchy" + }, + "blackwell-tuning": { + "title": "NVIDIA Blackwell Tuning Guide", + "url": "https://docs.nvidia.com/cuda/blackwell-tuning-guide/index.html", + "locator": "Compute capability 12.0 limits, contrasted with 10.0", + "scope": "Only compute capability 12.0 limits apply to RTX 5090." + }, + "google-tpu-v1-paper": { + "title": "Jouppi et al.: In-Datacenter Performance Analysis of a Tensor Processing Unit", + "url": "https://arxiv.org/pdf/1704.04760", + "locator": "ISCA 2017, pages 3–4, Figures 1 and 4; page 5, Table 2; page 15, dataflow comparison", + "scope": "First-generation TPU used for inference. Its weight-stationary array differs from doke’s output-stationary teaching array." + }, + "google-tpu-v1-explained": { + "title": "Google: An in-depth look at the first TPU", + "url": "https://cloud.google.com/blog/products/ai-machine-learning/an-in-depth-look-at-googles-first-tensor-processing-unit-tpu?hl=en", + "locator": "Quantization, systolic array, CISC instructions and 700 MHz operation", + "scope": "First TPU generation, not TPU v4 or current Cloud TPU hardware." + }, + "cisco-g200": { + "title": "Cisco Silicon One G200 data sheet", + "url": "https://www.cisco.com/c/en/us/solutions/collateral/silicon-one/silicon-one-g200-ds.html", + "locator": "February 2026: Product overview; Table 1; Figure 2 functional block diagram; Features", + "scope": "Public G200 functions and interface totals. Does not disclose a die floorplan, internal stage count, clock rate or exact buffer capacity." + }, + "cisco-voq": { + "title": "Cisco Silicon One QoS: virtual output queues", + "url": "https://www.cisco.com/c/en/us/products/collateral/switches/catalyst-9500-series-switches/catalyst-9500x-9600x-qos-q200-wp.html", + "locator": "VOQ queuing model, credit requests and scheduling", + "scope": "Mechanism explanation on named Q200-based Catalyst platforms. Their capacity, buffering, arbitration and timing are not G200 specifications." + }, + "p4-psa": { + "title": "P4 Portable Switch Architecture, version 1.2", + "url": "https://p4.org/wp-content/uploads/sites/53/p4-spec/docs/PSA-v1.2.html", + "locator": "Section 1, Figure 1: parsing, ingress decisions, buffering and egress; sections 6.2–6.4: packet paths", + "scope": "Open functional description of a programmable switch. Does not assert G200 implements PSA or uses this physical pipeline." + }, + "nvidia-hgx-specifications": { + "title": "NVIDIA HGX reference architecture: GPU specifications", + "url": "https://docs.nvidia.com/enterprise-reference-architectures/hgx-ai-factory-h100-h200-b200/latest/components.html", + "locator": "Table 1: H100 SXM memory per GPU and GPU bandwidth", + "scope": "H100 SXM 80 GB variant. Memory bandwidth is not a compute clock or an end-to-end latency." + } + }, + "profiles": { + "ryzen-9950x": { + "family": "cpu", + "name": "AMD Ryzen 9 9950X", + "architecture": "Zen 5", + "counts": { + "cores": 16, + "threads": 32, + "package_dies": 3, + "logical_core_groups": 2, + "cores_per_logical_group": 8 + }, + "facts": [ + { + "label": "Cores / threads", + "value": "16 / 32", + "sources": [ + "amd-9950x" + ] + }, + { + "label": "L1 instruction / data per Zen 5 core", + "value": "32 KiB / 48 KiB", + "sources": [ + "amd-zen5-smt" + ] + }, + { + "label": "L2 per core / aggregate L3", + "value": "1 MiB / 64 MB", + "sources": [ + "amd-zen5-smt", + "amd-9950x" + ] + } + ], + "topology_note": "The two eight-core groups are a functional interpretation combining the 9950X product totals with AMD’s documented classic Zen 5 core complex. No physical die coordinates or interconnect distances are asserted.", + "sources": [ + "amd-9950x", + "amd-zen5-ccd", + "amd-zen5-smt" + ], + "hidden_blocks": [], + "timing": { + "kind": "clock", + "hz": 4300000000, + "label": "4.3 GHz base clock", + "sourceIds": [ + "amd-9950x" + ], + "scope": "Published base frequency. Actual core frequency varies with operating conditions. Its reciprocal is a reference clock period, not instruction latency or model-tick duration." + } + }, + "rtx-5090": { + "family": "gpu", + "name": "NVIDIA GeForce RTX 5090", + "architecture": "RTX Blackwell", + "counts": { + "sms": 170, + "partitions_per_sm": 4, + "cuda_cores_per_sm": 128, + "tensor_cores_per_sm": 4, + "rt_cores_per_sm": 1, + "warp_lanes": 32 + }, + "facts": [ + { + "label": "Enabled SMs", + "value": "170", + "sources": [ + "nvidia-blackwell" + ] + }, + { + "label": "L2 / combined L1 and shared per SM", + "value": "96 MB / 128 KB", + "sources": [ + "nvidia-blackwell" + ] + }, + { + "label": "Register storage per SM", + "value": "256 KB", + "sources": [ + "nvidia-blackwell" + ] + }, + { + "label": "Device memory", + "value": "32 GB GDDR7", + "sources": [ + "nvidia-5090" + ] + } + ], + "topology_note": "170 enabled SMs in a logical grid. Full GB202 has 192 SMs; the grid does not infer which physical units are enabled. The SM view is based on Figure 5, not a physical floorplan.", + "sources": [ + "nvidia-blackwell", + "nvidia-5090", + "blackwell-tuning" + ], + "hidden_blocks": [], + "timing": { + "kind": "clock", + "hz": 2010000000, + "label": "2.01 GHz base clock", + "sourceIds": [ + "nvidia-5090" + ], + "scope": "NVIDIA reference base frequency. Operating frequency varies; one cycle is not one completed CUDA instruction or one model tick." + } + }, + "h100-sxm": { + "family": "gpu", + "name": "NVIDIA H100 SXM5", + "architecture": "Hopper", + "counts": { + "sms": 132, + "partitions_per_sm": 4, + "cuda_cores_per_sm": 128, + "tensor_cores_per_sm": 4, + "rt_cores_per_sm": 0, + "warp_lanes": 32 + }, + "facts": [ + { + "label": "Enabled SMs", + "value": "132", + "sources": [ + "nvidia-hopper" + ] + }, + { + "label": "L2 / combined L1 and shared per SM", + "value": "50 MB / 256 KB", + "sources": [ + "nvidia-hopper", + "hopper-tuning" + ] + }, + { + "label": "Registers per SM", + "value": "65,536 × 32-bit", + "sources": [ + "hopper-tuning" + ] + }, + { + "label": "Device memory", + "value": "80 GB HBM3", + "sources": [ + "nvidia-hopper" + ] + } + ], + "topology_note": "132 enabled SMs for the SXM5 configuration. Full GH100 has 144 and the PCIe product differs. The SM structure is a functional diagram; device-specific instruction and memory behavior is not emulated.", + "sources": [ + "nvidia-hopper", + "hopper-tuning", + "nvidia-hgx-specifications" + ], + "hidden_blocks": [ + "ray" + ], + "timing": { + "kind": "bandwidth", + "bytesPerSecond": 3350000000000, + "label": "3.35 TB/s memory bandwidth", + "sourceIds": [ + "nvidia-hgx-specifications" + ], + "scope": "Published H100 SXM memory bandwidth, using decimal TB/s. This is aggregate data movement capacity, not a clock frequency, memory latency or measured application throughput." + } + }, + "tpu-v1": { + "family": "tensor", + "name": "Google TPU v1", + "shortName": "TPU v1", + "architecture": "First-generation Tensor Processing Unit", + "counts": { + "array_rows": 256, + "array_columns": 256, + "macs": 65536, + "model_rows": 4, + "model_columns": 4 + }, + "facts": [ + { + "label": "Documented matrix array", + "value": "256 × 256 · 65,536 8-bit MACs", + "sources": [ + "google-tpu-v1-paper" + ] + }, + { + "label": "Unified buffer / accumulators", + "value": "24 MiB / 4 MiB", + "sources": [ + "google-tpu-v1-paper" + ] + }, + { + "label": "External weight storage", + "value": "8 GiB DDR3", + "sources": [ + "google-tpu-v1-paper" + ] + }, + { + "label": "Original use", + "value": "Neural-network inference", + "sources": [ + "google-tpu-v1-paper" + ] + } + ], + "topology_note": "An original functional schematic of the published first-generation TPU. The visible cell texture and array window are representative, not a depiction of all 65,536 MACs or actual die coordinates. Execution opens a separate 4 × 4 output-stationary integer model; the documented TPU keeps weights in the array instead.", + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "hidden_blocks": [], + "timing": { + "kind": "clock", + "hz": 700000000, + "label": "700 MHz documented clock", + "sourceIds": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "scope": "The 2017 paper’s first-generation inference TPU. Its clock period is a hardware reference; doke’s 4 × 4 output-stationary model is not this 256 × 256 weight-stationary implementation and its ticks are not TPU cycles." + } + }, + "silicon-one-g200": { + "family": "switch", + "name": "Cisco Silicon One G200", + "shortName": "Silicon One G200", + "architecture": "Programmable network switching processor", + "counts": { + "serdes": 512, + "ethernet_macs": 512, + "model_ingresses": 3, + "model_egresses": 3 + }, + "facts": [ + { + "label": "Published switching bandwidth", + "value": "51.2 Tbps, full-duplex", + "sources": [ + "cisco-g200" + ] + }, + { + "label": "Network interfaces", + "value": "512 × 112G SerDes · 512 Ethernet MACs", + "sources": [ + "cisco-g200" + ] + }, + { + "label": "Example switch configurations", + "value": "64 × 800GE to 512 × 100GE", + "sources": [ + "cisco-g200" + ] + }, + { + "label": "Packet storage", + "value": "Fully shared on-chip buffer", + "sources": [ + "cisco-g200" + ] + } + ], + "topology_note": "Public G200 functions are arranged as an original functional schematic, not a physical die or a disclosed stage-by-stage G200 pipeline. Execution is a three-input, three-output teaching switch with a 32-packet shared capacity and rotating grants. These counts and rules are authored model parameters, not G200 specifications.", + "sources": [ + "cisco-g200", + "cisco-voq", + "p4-psa" + ], + "hidden_blocks": [], + "timing": { + "kind": "throughput", + "bitsPerSecond": 51200000000000, + "label": "51.2 Tbps, full-duplex", + "sourceIds": [ + "cisco-g200" + ], + "scope": "Cisco’s published aggregate switching bandwidth, preserving its full-duplex convention. Not a processor clock, packet latency or guarantee of application throughput; no conversion to clock period or model ticks is made." + } + } + }, + "families": { + "cpu": { + "name": "Inside a CPU", + "overview": "Follow instructions and their data through one core. Then pull back to see how cores share a chip.", + "levels": [ + { + "id": "processor", + "title": "Processor", + "description": "Sixteen cores, cache groups and the connection to memory." + }, + { + "id": "core", + "title": "One core", + "description": "The front end, execution units and nearby storage." + }, + { + "id": "execution", + "title": "Execution", + "description": "The actual operands, waiting requests and sums in this small computation." + } + ], + "blocks": { + "processor": { + "title": "Processor package", + "summary": "One package contains several cooperating pieces of silicon.", + "detail": [ + "The Ryzen 9 9950X specification lists 16 cores, 32 threads and three package dies. Its aggregate cache capacities are 16 MB L2 and 64 MB L3.", + "The scene is a navigable functional map. Tile size, position, trace length and glow do not measure physical die area or electrical delay." + ], + "path": [ + "CPU", + "Processor", + "Processor package" + ], + "sources": [ + "amd-9950x" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "ccd": { + "title": "Core complex", + "summary": "Nearby cores share their last-level cache.", + "detail": [ + "AMD documents a classic Zen 5 complex as eight cores sharing 32 MB of L3. The two groups here explain that cache locality.", + "This grouped arrangement is an interpretation of public architecture and product facts, not a reproduction of a photographed Ryzen floorplan. Open a core to explore its functional parts." + ], + "path": [ + "CPU", + "Processor", + "Core complex" + ], + "sources": [ + "amd-zen5-ccd", + "amd-9950x" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "core": { + "title": "CPU core", + "summary": "A core overlaps the work needed to execute a program.", + "detail": [ + "Fetching instructions, waiting for data and doing arithmetic can overlap. Having several execution units helps only when independent work is ready.", + "In the running example, each output is an eight-term dot product. Different outputs are independent; the next addition within one output must use that output’s previous sum." + ], + "path": [ + "CPU", + "Processor", + "CPU core" + ], + "sources": [ + "boom-issue", + "amd-small-gemm" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "io": { + "title": "I/O and memory connection", + "summary": "The processor connects its cores to the rest of the system.", + "detail": [ + "The package includes communication and memory interfaces alongside the compute cores. System memory stores far more than the small on-chip caches.", + "The visible connector is an orientation landmark. This model does not simulate DDR5 commands, PCIe traffic, inter-die arbitration or the integrated graphics engine." + ], + "path": [ + "CPU", + "Processor", + "I/O and memory connection" + ], + "sources": [ + "amd-9950x" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "memory": { + "title": "Main memory", + "summary": "The backing store for the input matrices and result.", + "detail": [ + "A load identifies a byte address. When the needed data is unavailable nearby, the processor must obtain it through the memory hierarchy.", + "Here, an input value occupies four modeled bytes. Follow a request to see its matrix coordinate, address and destination. Transfer times are authored model ticks." + ], + "path": [ + "CPU", + "Execution", + "Main memory" + ], + "sources": [ + "boom-loadstore" + ], + "level": "execution", + "modelNodes": [ + "memory" + ], + "modeled": true, + "modelNote": "Simulates addressed reads and completed transfers; no DRAM controller or measured nanoseconds." + }, + "l3": { + "title": "Shared L3 cache", + "summary": "The last on-chip cache before more distant storage.", + "detail": [ + "The 9950X has 64 MB of L3 in total. The grouped view illustrates separate sharing domains rather than one equally local pool for all cores.", + "L3 is architectural context here. The executable example has one deliberately small cache, so an animated teaching-cache request must not be read as measured Ryzen L3 activity." + ], + "path": [ + "CPU", + "Processor", + "Shared L3 cache" + ], + "sources": [ + "amd-9950x", + "amd-zen5-ccd" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "l2": { + "title": "Private L2 cache", + "summary": "More nearby capacity for one core.", + "detail": [ + "AMD’s Zen 5 diagram places a 1 MiB unified L2 behind the instruction and data caches. Unified means both kinds of content can use it.", + "This landmark lets you trace the hierarchy without filling the screen with every cache line. The running example does not implement a separate L2 replacement policy." + ], + "path": [ + "CPU", + "One core", + "Private L2 cache" + ], + "sources": [ + "amd-zen5-smt" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "l1i": { + "title": "Instruction cache", + "summary": "Instructions are data too, with their own nearby store.", + "detail": [ + "The core front end fetches instruction bytes from its L1 instruction cache. AMD’s Zen 5 diagram labels this store 32 KiB.", + "The image-processing example begins with already-defined operations. It does not fetch an x86 binary, so this block remains explanatory rather than producing invented fetch events." + ], + "path": [ + "CPU", + "One core", + "Instruction cache" + ], + "sources": [ + "amd-zen5-smt", + "boom-fetch" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "l1d": { + "title": "Data cache", + "summary": "Keep recently used data close to the arithmetic.", + "detail": [ + "The Zen 5 core diagram shows a 48 KiB L1 data cache. Cache storage is separate from the registers consumed directly by arithmetic.", + "Doke’s cache experiment is much smaller: four 64-byte lines. Repeating a resident access can hit; filling a fifth distinct line can evict an older one. This size makes reuse visible." + ], + "path": [ + "CPU", + "Execution", + "Data cache" + ], + "sources": [ + "amd-zen5-smt" + ], + "level": "execution", + "modelNodes": [ + "cache" + ], + "modeled": true, + "modelNote": "The active four-line teaching cache is not the physical 48 KiB L1D." + }, + "fetch": { + "title": "Instruction fetch", + "summary": "Bring the next part of the program into the core.", + "detail": [ + "The front end supplies instructions to later stages. A redirect changes where fetching continues, for example after resolving a branch.", + "Think of a loop as instructions plus the data those instructions touch. The present model follows the data and operation dependencies; it does not simulate instruction-fetch bandwidth." + ], + "path": [ + "CPU", + "One core", + "Instruction fetch" + ], + "sources": [ + "boom-fetch" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "branch": { + "title": "Branch prediction", + "summary": "Choose a likely next instruction path before it is known.", + "detail": [ + "Prediction keeps the front end supplied across branches. A wrong path must be discarded when the branch is resolved.", + "No random flashes stand in for predictions here. The matrix task has a fixed operation sequence and the simulator does not implement a branch predictor or recovery penalty." + ], + "path": [ + "CPU", + "One core", + "Branch prediction" + ], + "sources": [ + "boom-fetch", + "amd-zen5-smt" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "decode": { + "title": "Instruction decode", + "summary": "Turn encoded instructions into internal work.", + "detail": [ + "Zen 5 has two four-wide decode paths in AMD’s core diagram. These are instruction-processing resources, not eight independent CPU cores.", + "A matrix element is not an instruction: reading it, multiplying it and adding the product are distinct actions. This explorer exposes those actions without claiming to decode x86 bytes." + ], + "path": [ + "CPU", + "One core", + "Instruction decode" + ], + "sources": [ + "amd-zen5-smt" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "uop-cache": { + "title": "Operation cache", + "summary": "Reuse decoded work on a repeated instruction path.", + "detail": [ + "AMD’s core diagram includes an operation cache feeding the front end. It is distinct from caches that hold matrix values.", + "Changing input pixels changes data, not necessarily the loop’s instructions. The operation-cache landmark helps separate those concepts; its contents and hits are not modeled." + ], + "path": [ + "CPU", + "One core", + "Operation cache" + ], + "sources": [ + "amd-zen5-smt" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "rename": { + "title": "Register renaming", + "summary": "Separate a reused register name from its successive values.", + "detail": [ + "Renaming assigns different physical storage to different writes of a logical register. This removes false dependencies caused only by reusing a name.", + "It cannot remove a true dependency: if a sum needs a previous product, that product must still arrive. The example tracks such value readiness but does not implement a rename table." + ], + "path": [ + "CPU", + "One core", + "Register renaming" + ], + "sources": [ + "boom-rename" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "scheduler": { + "title": "Issue scheduler", + "summary": "Choose an operation whose inputs are ready.", + "detail": [ + "An issue queue holds waiting operations. Selection needs both ready operands and a suitable execution resource; merely having many operations queued is not enough.", + "Watch a dot product wait for its loads while another output progresses. The experiment’s bounded issue rule is visible in the live state and can be single-stepped." + ], + "path": [ + "CPU", + "Execution", + "Issue scheduler" + ], + "sources": [ + "boom-issue" + ], + "level": "execution", + "modelNodes": [ + "scheduler" + ], + "modeled": true, + "modelNote": "Simulates readiness and bounded issue; no claim to reproduce Zen’s scheduling policy." + }, + "registers": { + "title": "Operand registers", + "summary": "Values wait close to the units that consume them.", + "detail": [ + "Physical registers hold working values and supply execution units. They are not the same storage as cache lines.", + "The live view shows actual loaded operands and accumulated sums for the small task. It uses model slots rather than claiming each slot is a particular Ryzen hardware register." + ], + "path": [ + "CPU", + "Execution", + "Operand registers" + ], + "sources": [ + "boom-rename" + ], + "level": "execution", + "modelNodes": [ + "registers" + ], + "modeled": true, + "modelNote": "Displays actual operand state; register renaming and finite physical-register allocation are not modeled." + }, + "integer": { + "title": "Integer execution", + "summary": "Compute a result once its required inputs arrive.", + "detail": [ + "Arithmetic units perform operations selected by the scheduler. Different operation types can require different resources.", + "For one output, multiply A[row,k] by B[k,column], then add that product to the partial sum. Repeat for k = 0 through 7. Independent output chains can overlap." + ], + "path": [ + "CPU", + "Execution", + "Integer execution" + ], + "sources": [ + "boom-issue", + "amd-small-gemm" + ], + "level": "execution", + "modelNodes": [ + "compute" + ], + "modeled": true, + "modelNote": "Computes exact signed integer products and sums under educational timing rules." + }, + "vector": { + "title": "Vector and floating-point execution", + "summary": "One instruction can operate on several numeric elements.", + "detail": [ + "Vector instructions describe multiple elements together. Efficient matrix code also keeps independent accumulators to avoid one long dependency chain.", + "These units explain how optimized software can differ from the scalar teaching task. The current computation does not issue AVX-512 instructions or predict vector throughput." + ], + "path": [ + "CPU", + "One core", + "Vector and floating-point execution" + ], + "sources": [ + "amd-small-gemm", + "amd-zen5-smt" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "loadstore": { + "title": "Load/store machinery", + "summary": "Connect the program’s addresses to data movement.", + "detail": [ + "Loads retrieve values; stores make values available in memory. Real out-of-order processors track their ordering and possible address overlap.", + "The model follows read addresses and output transfers. It merges simultaneous misses for an outstanding cache line, but does not implement speculative memory ordering or store forwarding." + ], + "path": [ + "CPU", + "Execution", + "Load/store machinery" + ], + "sources": [ + "boom-loadstore" + ], + "level": "execution", + "modelNodes": [ + "memory", + "cache" + ], + "modeled": true, + "modelNote": "Actual model requests are visible; no vendor load/store queue or coherence simulation." + }, + "retire": { + "title": "Retirement", + "summary": "Finish architectural work in a controlled order.", + "detail": [ + "Execution completion and retirement are different. An out-of-order core can finish younger work early while waiting to commit older instructions safely.", + "Doke marks an output valid after its result transfer completes. That completion flag is not a simulated x86 reorder-buffer commit or an exception guarantee." + ], + "path": [ + "CPU", + "One core", + "Retirement" + ], + "sources": [ + "boom-retire" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "output": { + "title": "Output values", + "summary": "The computed image becomes available value by value.", + "detail": [ + "Each completed value is the exact result of the model’s matrix operation. The result view distinguishes available values from unfinished work.", + "For the adjacent-difference filter, the first column equals its input value. Each later column subtracts the input immediately to its left. Changing execution order must preserve those answers." + ], + "path": [ + "CPU", + "Execution", + "Output values" + ], + "sources": [ + "amd-small-gemm" + ], + "level": "execution", + "modelNodes": [ + "output" + ], + "modeled": true, + "modelNote": "Output validity follows completed model stores, never a decorative animation timer." + } + } + }, + "gpu": { + "name": "Inside a GPU", + "overview": "Find one multiprocessor among many, open its execution partitions, and follow the work of 32 threads.", + "levels": [ + { + "id": "processor", + "title": "Processor", + "description": "Many streaming multiprocessors share an L2 cache and device memory." + }, + { + "id": "core", + "title": "One SM", + "description": "Schedulers, registers, arithmetic units and shared storage." + }, + { + "id": "execution", + "title": "One warp", + "description": "Thirty-two logical lanes, their addresses and partial sums." + } + ], + "blocks": { + "processor": { + "title": "GPU processor", + "summary": "Many multiprocessors cooperate on a large pool of work.", + "detail": [ + "The reference view shows 170 enabled SMs for RTX 5090 or 132 for H100 SXM5. Full-die diagrams can show more units than the shipping product enables.", + "Each tile represents one documented SM, arranged for exploration. The grid is not a die photograph, and the small live task does not claim to occupy every tile." + ], + "path": [ + "GPU", + "Processor", + "GPU processor" + ], + "sources": [ + "nvidia-blackwell", + "nvidia-hopper" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "gpc": { + "title": "Processing cluster", + "summary": "SMs are organized into larger groups.", + "detail": [ + "NVIDIA’s architecture hierarchy groups streaming multiprocessors into graphics processing clusters. Program threads are separately organized into blocks and grids.", + "A logical grid of clickable SMs helps you find and enter a unit. Its rows do not claim the undocumented enabled-unit placement of a particular retail GPU." + ], + "path": [ + "GPU", + "Processor", + "Processing cluster" + ], + "sources": [ + "cuda-model" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "sm": { + "title": "Streaming multiprocessor", + "summary": "A local home for many resident threads.", + "detail": [ + "An SM combines scheduling, register storage, arithmetic and shared storage. Threads from one thread block run on one SM.", + "Open an SM to separate its structure from the 32 logical lanes of one warp. A CUDA core is an arithmetic resource; a lane is one thread’s position in a scheduled group." + ], + "path": [ + "GPU", + "Processor", + "Streaming multiprocessor" + ], + "sources": [ + "cuda-model" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "dispatch": { + "title": "Work distribution", + "summary": "Turn a launched grid into work assigned to SMs.", + "detail": [ + "A kernel launch defines a grid of thread blocks. Hardware can assign independent blocks to available SMs in different orders.", + "The eight-by-eight teaching output needs only 64 logical threads: two warps. Increasing the reference chip’s SM count does not magically turn that small task into a measured full-device benchmark." + ], + "path": [ + "GPU", + "Processor", + "Work distribution" + ], + "sources": [ + "cuda-model" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "instruction-cache": { + "title": "Instruction storage", + "summary": "Threads need the operations as well as the operands.", + "detail": [ + "The documented SM diagrams include instruction storage near the scheduler and dispatch resources.", + "The live model starts from a defined load–multiply–accumulate sequence. It does not run a compiled CUDA kernel or simulate instruction-cache hits and misses." + ], + "path": [ + "GPU", + "One SM", + "Instruction storage" + ], + "sources": [ + "nvidia-blackwell", + "nvidia-hopper" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "partition": { + "title": "SM execution partition", + "summary": "A repeating group of execution resources within an SM.", + "detail": [ + "The selected reference SM uses four execution partitions with local scheduling, registers and arithmetic, alongside shared resources.", + "Repeated geometry makes that organization visible at close range. A hardware partition is not the same thing as one permanent warp; scheduling can reuse its resources for many warps." + ], + "path": [ + "GPU", + "One SM", + "SM execution partition" + ], + "sources": [ + "nvidia-blackwell", + "nvidia-hopper", + "cuda-hardware" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "warp-scheduler": { + "title": "Warp scheduler", + "summary": "Issue a ready instruction for a group of threads.", + "detail": [ + "A scheduler selects a ready warp and issues its next instruction. Another resident warp can progress while the first waits for data.", + "Here two logical warps retain work in flight under a simple issue rule. Pausing stops their state. The highlighted group is the displayed warp, not a measurement of all real SM schedulers." + ], + "path": [ + "GPU", + "One warp", + "Warp scheduler" + ], + "sources": [ + "cuda-hardware" + ], + "level": "execution", + "modelNodes": [ + "scheduler" + ], + "modeled": true, + "modelNote": "Simulates two educational warps; vendor scheduling and occupancy are not reproduced." + }, + "registers": { + "title": "Register file", + "summary": "Each thread keeps its own working values.", + "detail": [ + "Thread-private register values remain on chip while warps wait. Register demand can constrain how many warps fit on an SM.", + "Inspect a lane to see its actual operands and partial sum. These model values are not a dump of physical GPU registers, and the example does not simulate register spilling." + ], + "path": [ + "GPU", + "One warp", + "Register file" + ], + "sources": [ + "cuda-registers", + "cuda-hardware" + ], + "level": "execution", + "modelNodes": [ + "registers" + ], + "modeled": true, + "modelNote": "Actual per-lane values; no physical allocation, bank conflict or spill model." + }, + "lanes": { + "title": "Warp lanes", + "summary": "Thirty-two logical positions execute a common operation.", + "detail": [ + "A warp contains 32 threads. Each lane retains its own values; active threads execute the selected instruction. Modern hardware also maintains per-thread execution state.", + "One lane owns one output element in this task. Each of its eight products uses a different k value. The same instruction therefore combines different values across the highlighted lanes." + ], + "path": [ + "GPU", + "One warp", + "Warp lanes" + ], + "sources": [ + "cuda-hardware" + ], + "level": "execution", + "modelNodes": [ + "compute" + ], + "modeled": true, + "modelNote": "The lane mask and values come from the executable model; lanes are not 32 complete CPU cores." + }, + "loadstore": { + "title": "Load/store units", + "summary": "Collect the addresses requested by active threads.", + "detail": [ + "Concurrent global-memory accesses are combined according to the addressed memory regions. Nearby addresses can share sectors; scattered addresses can require more.", + "Switch the storage layout while keeping the same mathematical matrix. The output should stay identical, while the model’s address-derived sector demand changes." + ], + "path": [ + "GPU", + "One warp", + "Load/store units" + ], + "sources": [ + "cuda-sectors" + ], + "level": "execution", + "modelNodes": [ + "memory", + "registers" + ], + "modeled": true, + "modelNote": "Counts actual modeled sectors; it does not equate every sector with a DRAM transaction." + }, + "l1shared": { + "title": "L1 cache and shared memory", + "summary": "A nearby store with two different uses.", + "detail": [ + "L1 caching is hardware managed. Shared memory is explicitly used by cooperating threads. Their capacity shares hardware resources, with architecture-specific limits.", + "The reference capacities are 128 KB combined for RTX Blackwell and 256 KB for Hopper. The current warp experiment bypasses a cache model; this landmark stays architectural context." + ], + "path": [ + "GPU", + "One SM", + "L1 cache and shared memory" + ], + "sources": [ + "cuda-memory", + "nvidia-blackwell", + "hopper-tuning" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "l2": { + "title": "Shared L2 cache", + "summary": "A cache between SM requests and device memory.", + "detail": [ + "RTX 5090 has 96 MB of L2; H100 SXM5 has 50 MB. These are product configurations, not the larger fully enabled die totals.", + "L2 can serve reusable data across requests. The teaching model counts sectors directly from addresses and does not claim those requests miss this cache or reach DRAM." + ], + "path": [ + "GPU", + "Processor", + "Shared L2 cache" + ], + "sources": [ + "nvidia-blackwell", + "nvidia-hopper" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "memory": { + "title": "Device memory", + "summary": "Large backing storage outside the SMs.", + "detail": [ + "RTX 5090 uses 32 GB of GDDR7; H100 SXM5 uses 80 GB of HBM3. These are different memory technologies and package arrangements.", + "The model keeps A, B and the output in a small addressed backing store. Glowing transfers represent requested model data, not measured bus utilization or physical wire travel." + ], + "path": [ + "GPU", + "One warp", + "Device memory" + ], + "sources": [ + "nvidia-5090", + "nvidia-hopper" + ], + "level": "execution", + "modelNodes": [ + "memory" + ], + "modeled": true, + "modelNote": "Actual modeled operand traffic, without a device-memory controller or SKU bandwidth prediction." + }, + "tensor": { + "title": "Tensor arithmetic", + "summary": "Specialized instructions compute matrix products.", + "detail": [ + "Tensor Cores perform matrix multiply–accumulate work. Their supported types and instruction shapes vary by GPU generation.", + "Ordinary multiply–add code does not automatically mean Tensor Cores were used. This warp experiment deliberately shows scalar integer arithmetic; tensor blocks remain inactive reference landmarks." + ], + "path": [ + "GPU", + "One SM", + "Tensor arithmetic" + ], + "sources": [ + "nvidia-hopper" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "ray": { + "title": "Ray traversal and intersection", + "summary": "Dedicated graphics work beside the general compute units.", + "detail": [ + "RTX Blackwell includes dedicated ray-tracing hardware. H100’s reference profile does not use that graphics block.", + "The input-tile task traces no rays. Leaving this unit inactive explains specialization more faithfully than lighting every block whenever any calculation runs." + ], + "path": [ + "GPU", + "One SM", + "Ray traversal and intersection" + ], + "sources": [ + "nvidia-blackwell" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "texture": { + "title": "Texture sampling", + "summary": "Fetch and filter sampled image data.", + "detail": [ + "Texture units support image sampling operations such as fetching texels and filtering neighboring samples.", + "Painting an image in this explorer does not cause real texture instructions in the educational model. Its matrix values are ordinary indexed operands, so this is a static architectural landmark." + ], + "path": [ + "GPU", + "One SM", + "Texture sampling" + ], + "sources": [ + "nvidia-blackwell" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "An architectural landmark. This part is explained here but is not separately simulated." + }, + "coalescer": { + "title": "Memory sectors", + "summary": "Addresses determine how much data must be requested.", + "detail": [ + "For four-byte words, 32 adjacent aligned lane addresses fit into four 32-byte sectors. Scattering the same number of words can touch many more sectors.", + "Inspect the addresses rather than judging brightness. Repeated addresses count once in the modeled sector union. A memory layout change can improve this count without changing any output values." + ], + "path": [ + "GPU", + "One warp", + "Memory sectors" + ], + "sources": [ + "cuda-sectors", + "cuda-memory" + ], + "level": "execution", + "modelNodes": [ + "memory" + ], + "modeled": true, + "modelNote": "Exact sector union for active addresses; cache behavior and measured memory efficiency are outside this model." + }, + "output": { + "title": "Output values", + "summary": "The same answers emerge through parallel work.", + "detail": [ + "Each thread contributes one output value after accumulating eight products. Values become valid when their modeled stores complete.", + "Compare the final matrix with CPU mode. Matching results demonstrate correctness of the two execution paths; differences in educational ticks do not establish which physical chip is faster." + ], + "path": [ + "GPU", + "One warp", + "Output values" + ], + "sources": [ + "cuda-model" + ], + "level": "execution", + "modelNodes": [ + "output" + ], + "modeled": true, + "modelNote": "Exact output and validity from completed model stores." + }, + "arithmetic": { + "title": "Arithmetic execution resources", + "summary": "Physical arithmetic resources carry out instructions issued by a scheduler.", + "detail": [ + "Both reference SMs have 128 FP32 arithmetic resources, shown as four groups of 32. RTX Blackwell unifies FP32 and INT32 execution on those resources; Hopper also has separate integer and FP64 resources not individually drawn here.", + "These 32 cells are architectural resources, not the 32 logical threads of a warp. A warp is a scheduled group of work that uses execution resources over time. Enter the warp view to inspect the teaching model’s actual thread values." + ], + "path": [ + "GPU", + "One SM", + "Arithmetic resources" + ], + "sources": [ + "nvidia-blackwell", + "nvidia-hopper", + "cuda-hardware" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context. No physical instruction issue, FP32 pipeline timing or exact mapping from a model lane to a hardware cell is simulated." + } + } + }, + "tensor": { + "name": "Tensor processor", + "overview": "Explore the first TPU’s documented matrix engine, then follow real operands through a smaller, explicitly different systolic teaching model.", + "levels": [ + { + "id": "processor", + "title": "Processor", + "description": "Published TPU v1 functional units in an original schematic." + }, + { + "id": "core", + "title": "Array", + "description": "A representative window into the documented 256 × 256 matrix array." + }, + { + "id": "execution", + "title": "Live 4 × 4 array", + "description": "Exact integer products in an output-stationary teaching array." + } + ], + "blocks": { + "processor": { + "title": "Neural inference processor", + "summary": "A processor designed around repeated matrix arithmetic.", + "detail": [ + "TPU v1 accelerates inference: evaluating a trained network on new inputs. Its documented design puts a large matrix unit beside local storage and host interfaces.", + "Open the array to inspect how operands meet. The live experiment multiplies two small matrices exactly; it does not run a trained network or emulate a full TPU." + ], + "path": [ + "Tensor processor", + "Processor", + "Neural inference processor" + ], + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "The live experiment is a 4 × 4 output-stationary integer array. TPU v1 is a 256 × 256 weight-stationary design; activity here is a conceptual correspondence, not device telemetry." + }, + "matrix": { + "title": "Matrix multiply array", + "summary": "Nearby arithmetic cells reuse values as a wave moves through the array.", + "detail": [ + "The first TPU contains 256 rows by 256 columns of multiply-accumulate units. Its weights are held in the array while input activations and partial sums move through it.", + "The displayed surface is a representative window. The live 4 × 4 experiment instead keeps each output sum at one cell and moves both input operands between neighbors." + ], + "path": [ + "Tensor processor", + "Processor", + "Matrix multiply array" + ], + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "level": "processor", + "modelNodes": [ + "compute", + "scheduler", + "registers" + ], + "modeled": true, + "modelNote": "The live experiment is a 4 × 4 output-stationary integer array. TPU v1 is a 256 × 256 weight-stationary design; activity here is a conceptual correspondence, not device telemetry." + }, + "buffer": { + "title": "Unified buffer", + "summary": "Local storage holds activations and intermediate results.", + "detail": [ + "The documented TPU has a 24 MiB on-chip unified buffer. Keeping intermediate values near arithmetic reduces repeated trips to host memory.", + "In the experiment, the scratch area holds operands for one tile. Its activity indicates model data readiness, not occupancy of the real 24 MiB buffer." + ], + "path": [ + "Tensor processor", + "Processor", + "Unified buffer" + ], + "sources": [ + "google-tpu-v1-paper" + ], + "level": "processor", + "modelNodes": [ + "scratch" + ], + "modeled": true, + "modelNote": "The live experiment is a 4 × 4 output-stationary integer array. TPU v1 is a 256 × 256 weight-stationary design; activity here is a conceptual correspondence, not device telemetry." + }, + "weights": { + "title": "Weight memory", + "summary": "The learned coefficients must reach the array before multiplication.", + "detail": [ + "TPU v1 uses 8 GiB of external DDR3 weight memory. A FIFO stages weight tiles before they enter the matrix unit.", + "Our input B is a small editable matrix, not a trained network. Editing it changes the coefficients in the exact calculation; it does not model DDR3 transfers on the real TPU." + ], + "path": [ + "Tensor processor", + "Processor", + "Weight memory" + ], + "sources": [ + "google-tpu-v1-paper" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "instruction": { + "title": "Instruction control", + "summary": "A host command can describe a large matrix operation.", + "detail": [ + "The first TPU accepts instructions from its host rather than fetching a general-purpose application. Its commands include host-memory transfers, weight reads, matrix work and activation.", + "The live scheduler has a narrower role: it starts a tile once model operands are available, then advances its wavefront. It does not decode the TPU instruction set." + ], + "path": [ + "Tensor processor", + "Processor", + "Instruction control" + ], + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "accumulator": { + "title": "Accumulation storage", + "summary": "Products are added to wider running sums.", + "detail": [ + "TPU v1 provides 4 MiB of 32-bit accumulators beside its matrix unit. Wider sums preserve range while many lower-precision products are combined.", + "Each cell in the teaching array owns one exact integer output sum. Click a cell to see completed products accumulate; that local ownership differs from the published TPU dataflow." + ], + "path": [ + "Tensor processor", + "Inside the engine", + "Accumulation storage" + ], + "sources": [ + "google-tpu-v1-paper" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "interface": { + "title": "Host interface", + "summary": "The host supplies work and retrieves results.", + "detail": [ + "TPU v1 connects to its host over PCIe Gen3 ×16. A DMA controller transfers data between host memory and the unified buffer.", + "The experiment begins with the matrices you enter and ends with their product. Browser input is not a PCIe transfer, and no host bandwidth or driver overhead is simulated." + ], + "path": [ + "Tensor processor", + "Processor", + "Host interface" + ], + "sources": [ + "google-tpu-v1-paper" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "activation": { + "title": "Activation and pooling", + "summary": "Matrix results can be transformed before the next network layer.", + "detail": [ + "The first TPU includes nonlinear activation and pooling operations. They operate on accumulated results and return intermediate values to local storage.", + "The live calculation stops at C = A × B. It does not apply ReLU, sigmoid, pooling, quantization or the surrounding layers of a neural network." + ], + "path": [ + "Tensor processor", + "Inside the engine", + "Activation and pooling" + ], + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "memory": { + "title": "Operand memory", + "summary": "A tile starts by requesting the values it will reuse.", + "detail": [ + "Matrix engines need data before arithmetic can proceed. Local reuse helps only after operands have arrived; an empty array cannot manufacture work.", + "This model loads four rows of A and four columns of B for each tile. The shown memory requests and authored delays belong to this experiment, not the TPU memory system." + ], + "path": [ + "Tensor processor", + "Live experiment", + "Operand memory" + ], + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "level": "execution", + "modelNodes": [ + "memory" + ], + "modeled": true, + "modelNote": "Live state comes from the small educational model; it does not reproduce vendor timing or implementation." + }, + "scheduler": { + "title": "Tile scheduler", + "summary": "One tile is prepared and advanced at a time.", + "detail": [ + "Regular matrix work can be organized as a sequence of tiles. Their boundaries determine which values are available together and where reuse occurs.", + "The model schedules four 4 × 4 output tiles to build the 8 × 8 result. A tile waits for its input transfer, then advances fourteen wave positions, including filling and draining the array." + ], + "path": [ + "Tensor processor", + "Live experiment", + "Tile scheduler" + ], + "sources": [ + "google-tpu-v1-explained" + ], + "level": "execution", + "modelNodes": [ + "scheduler" + ], + "modeled": true, + "modelNote": "Live state comes from the small educational model; it does not reproduce vendor timing or implementation." + }, + "registers": { + "title": "In-flight operands", + "summary": "An arithmetic cell must receive matching terms.", + "detail": [ + "In a systolic array, neighboring cells pass values onward so that data does not have to return to large memory after every multiplication.", + "The experiment tags each operand with its k index. A cell adds a product only when both operands are present for the same k; this preserves the dot product while values travel." + ], + "path": [ + "Tensor processor", + "Live experiment", + "In-flight operands" + ], + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "level": "execution", + "modelNodes": [ + "registers" + ], + "modeled": true, + "modelNote": "Live state comes from the small educational model; it does not reproduce vendor timing or implementation." + }, + "compute": { + "title": "Array execution", + "summary": "Sixteen cells compute sixteen output sums.", + "detail": [ + "A matrix product contains many independent output sums, each formed from pairs of input values. An array exposes several of those sums at once.", + "The live 4 × 4 array computes one tile using exact integer arithmetic. Operand movement and completion events drive the glow; the full result contains 64 outputs and 512 products." + ], + "path": [ + "Tensor processor", + "Live experiment", + "Array execution" + ], + "sources": [ + "google-tpu-v1-explained" + ], + "level": "execution", + "modelNodes": [ + "compute" + ], + "modeled": true, + "modelNote": "Live state comes from the small educational model; it does not reproduce vendor timing or implementation." + }, + "pe": { + "title": "Processing element", + "summary": "One cell multiplies two arriving values and updates one sum.", + "detail": [ + "A multiply-accumulate combines a product with an existing sum. Repeating it over k computes C[row,column] = Σ A[row,k] × B[k,column].", + "Here A moves right and B moves down. Each of the sixteen model cells keeps its output sum locally, adding only terms that have actually arrived; it is not a miniature copy of TPU v1’s cell." + ], + "path": [ + "Tensor processor", + "Live experiment", + "Processing element" + ], + "sources": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "level": "execution", + "modelNodes": [ + "pe0", + "pe1", + "pe2", + "pe3", + "pe4", + "pe5", + "pe6", + "pe7", + "pe8", + "pe9", + "pe10", + "pe11", + "pe12", + "pe13", + "pe14", + "pe15" + ], + "modeled": true, + "modelNote": "The live experiment is a 4 × 4 output-stationary integer array. TPU v1 is a 256 × 256 weight-stationary design; activity here is a conceptual correspondence, not device telemetry." + }, + "output": { + "title": "Completed result", + "summary": "An output appears after its arithmetic and write complete.", + "detail": [ + "A matrix engine must make results available to whatever consumes them next. Producing a sum and delivering that sum are distinct steps.", + "The model marks an output complete after all eight products and its modeled output write finish. Pending values remain pending; this display does not substitute a precomputed answer." + ], + "path": [ + "Tensor processor", + "Live experiment", + "Completed result" + ], + "sources": [ + "google-tpu-v1-explained" + ], + "level": "execution", + "modelNodes": [ + "output" + ], + "modeled": true, + "modelNote": "Live state comes from the small educational model; it does not reproduce vendor timing or implementation." + } + } + }, + "switch": { + "name": "Network processor", + "overview": "Explore public G200 functions, then create contention inside a small switch whose packets, queues and transfers you can inspect.", + "levels": [ + { + "id": "processor", + "title": "Processor", + "description": "Documented G200 functions in an original schematic." + }, + { + "id": "core", + "title": "Packet path", + "description": "How ingress, forwarding, buffering and egress fit together." + }, + { + "id": "execution", + "title": "Live switch", + "description": "Three inputs, three outputs and bounded queues with real packet state." + } + ], + "blocks": { + "processor": { + "title": "Network switching processor", + "summary": "A specialized chip decides where packets should go.", + "detail": [ + "G200 is a standalone network switch processor, with published 51.2 Tbps full-duplex bandwidth. Its arithmetic serves forwarding and traffic management rather than neural tensor computation.", + "Open the packet path, then inject traffic into a small switch. Congestion, queue order and admission are executable here; the underlying commercial device is architectural context." + ], + "path": [ + "Network processor", + "Processor", + "Network switching processor" + ], + "sources": [ + "cisco-g200" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + }, + "ingress": { + "title": "Packet ingress", + "summary": "Incoming traffic enters before it can be forwarded.", + "detail": [ + "G200’s public diagram shows Ethernet interfaces and a programmable processor. The drawing here groups their functions without asserting a number of internal processing pipelines.", + "The experiment has three inputs. An injected packet carries its chosen destination and receives an identity so you can follow it through waiting, transfer and delivery." + ], + "path": [ + "Network processor", + "Inside the engine", + "Packet ingress" + ], + "sources": [ + "cisco-g200", + "p4-psa" + ], + "level": "core", + "modelNodes": [ + "ingress-0", + "ingress-1", + "ingress-2" + ], + "modeled": true, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + }, + "parser": { + "title": "Header parsing", + "summary": "Headers turn a stream of bytes into fields a forwarding program can use.", + "detail": [ + "In the open P4 switch model, a parser extracts header fields before ingress decisions. This is a functional explanation, not a claim that G200 implements P4 PSA.", + "Our packets already have an input and destination. No Ethernet/IP header parsing, checksum validation or malformed-packet handling is executed in this experiment." + ], + "path": [ + "Network processor", + "Inside the engine", + "Header parsing" + ], + "sources": [ + "p4-psa" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "lookup": { + "title": "Forwarding lookup", + "summary": "A table lookup connects packet fields to an action.", + "detail": [ + "A programmable forwarding path can select a destination and modify metadata using match-action rules. Cisco publicly identifies programmable processing and databases in G200.", + "The experiment bypasses forwarding lookup: you choose the destination directly. This makes queue contention visible without pretending to simulate routing tables or a specific ASIC lookup pipeline." + ], + "path": [ + "Network processor", + "Inside the engine", + "Forwarding lookup" + ], + "sources": [ + "p4-psa", + "cisco-g200" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "buffer": { + "title": "Shared packet buffer", + "summary": "Several ports can draw on one storage budget.", + "detail": [ + "Cisco documents a fully shared on-chip G200 packet buffer. Its public data sheet does not provide an exact byte capacity.", + "The teaching switch has 32 shared packet slots, including packets in transit. When all slots are occupied, a new arrival is rejected; this number is authored for a small experiment." + ], + "path": [ + "Network processor", + "Inside the engine", + "Shared packet buffer" + ], + "sources": [ + "cisco-g200" + ], + "level": "core", + "modelNodes": [], + "modeled": false, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + }, + "queues": { + "title": "Queues and head-of-line blocking", + "summary": "Queue order determines which waiting packet can be considered.", + "detail": [ + "Virtual output queues separate traffic by destination. Cisco’s QoS reference explains this mechanism on named Q200-based platforms; those hardware details are not G200 specifications.", + "In FIFO mode only an input’s first packet may compete. In VOQ mode the first packet for each destination may compete, so a blocked destination does not trap unrelated traffic behind it." + ], + "path": [ + "Network processor", + "Inside the engine", + "Queues and head-of-line blocking" + ], + "sources": [ + "cisco-voq" + ], + "level": "core", + "modelNodes": [ + "fifo-0", + "fifo-1", + "fifo-2", + "voq-0-0", + "voq-0-1", + "voq-0-2", + "voq-1-0", + "voq-1-1", + "voq-1-2", + "voq-2-0", + "voq-2-1", + "voq-2-2" + ], + "modeled": true, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + }, + "scheduler": { + "title": "Transfer scheduler", + "summary": "When packets compete, a rule selects which may move.", + "detail": [ + "Traffic scheduling considers available destinations and waiting traffic. The real implementation may have many classes and policies; this view does not disclose or infer Cisco arbitration logic.", + "Our rotating grants allow at most one transfer per input and one per output each tick. Blocking an output stops new grants to it and holds any transfer already in flight." + ], + "path": [ + "Network processor", + "Inside the engine", + "Transfer scheduler" + ], + "sources": [ + "cisco-voq" + ], + "level": "core", + "modelNodes": [ + "scheduler" + ], + "modeled": true, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + }, + "fabric": { + "title": "Internal transfer", + "summary": "A granted packet moves toward its chosen output.", + "detail": [ + "The connection between admission, buffering and egress is shown as a functional transfer path. The geometry is not a disclosed crossbar or a measurement of G200 wire lengths.", + "A granted packet normally arrives on the following model tick. Its identity remains visible in transit, and a blocked output can extend that wait. Model ticks have no nanosecond conversion." + ], + "path": [ + "Network processor", + "Inside the engine", + "Internal transfer" + ], + "sources": [ + "p4-psa" + ], + "level": "core", + "modelNodes": [ + "fabric" + ], + "modeled": true, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + }, + "egress": { + "title": "Packet egress", + "summary": "Delivery releases space for other traffic.", + "detail": [ + "Egress is the output side of a switch. Processing and transmission details depend on the actual device and configuration.", + "Delivery increments the selected output’s counter and releases one occupied slot. Observed waiting time includes queueing and modeled transit; it is not G200 packet latency." + ], + "path": [ + "Network processor", + "Inside the engine", + "Packet egress" + ], + "sources": [ + "p4-psa" + ], + "level": "core", + "modelNodes": [ + "egress-0", + "egress-1", + "egress-2" + ], + "modeled": true, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + }, + "interface": { + "title": "Ethernet and SerDes", + "summary": "Physical links carry traffic into and out of the chip.", + "detail": [ + "G200 exposes 512 long-reach 112G SerDes and 512 Ethernet MACs, with flexible port combinations up to 800 Gbps. SerDes lanes and configured Ethernet ports are different concepts.", + "The three-port experiment is deliberately small. Its ports do not stand for a proportional slice of real bandwidth, and interface decoration does not enumerate the chip’s physical lanes." + ], + "path": [ + "Network processor", + "Processor", + "Ethernet and SerDes" + ], + "sources": [ + "cisco-g200" + ], + "level": "processor", + "modelNodes": [], + "modeled": false, + "modelNote": "Architectural context only. No separate execution or physical placement is simulated for this block." + }, + "drop": { + "title": "Admission drop", + "summary": "Finite storage cannot admit every burst.", + "detail": [ + "Queue management must account for finite resources. The G200 data sheet describes shared buffering, but the exact rejection and congestion policies are not reproduced here.", + "When the model’s 32 occupied slots are full, a new packet is counted as dropped without entering a queue. Attempted arrivals always equal drops plus delivered and currently held packets." + ], + "path": [ + "Network processor", + "Live experiment", + "Admission drop" + ], + "sources": [ + "cisco-g200" + ], + "level": "execution", + "modelNodes": [ + "drop" + ], + "modeled": true, + "modelNote": "A generic three-port, input-queued experiment with authored timing and capacity. It does not reproduce G200’s internal organization, proprietary scheduler or physical layout." + } + } + } + } +} diff --git a/public/assets/molecules/dopamine.json b/public/assets/molecules/dopamine.json deleted file mode 100644 index 30b24d5..0000000 --- a/public/assets/molecules/dopamine.json +++ /dev/null @@ -1,254 +0,0 @@ -{ - "id": "dopamine", - "cid": 681, - "source": "https://pubchem.ncbi.nlm.nih.gov/compound/681", - "download": "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/681/record/JSON?record_type=3d", - "sha256": "e0130a27feb85183eeb52b05704cdeffeb3c6914ff3096bdc0cff3d231b7ee77", - "note": "One PubChem 3D conformer, recentered and rescaled. Atom sizes are illustrative; protonation is that of the source record, not a physiological pH calculation. Glutamate view uses L-glutamic acid.", - "atoms": [ - { - "element": "O", - "x": -2.8005772727272724, - "y": 2.0544363636363636, - "z": 0.07067272727272728 - }, - { - "element": "O", - "x": -3.9170772727272727, - "y": -0.46936363636363637, - "z": 0.40267272727272724 - }, - { - "element": "N", - "x": 3.8467227272727276, - "y": 0.3542363636363637, - "z": 0.36037272727272723 - }, - { - "element": "C", - "x": 1.6014227272727273, - "y": 0.07063636363636366, - "z": -0.6454272727272727 - }, - { - "element": "C", - "x": 0.1426227272727274, - "y": -0.06846363636363634, - "z": -0.36912727272727275 - }, - { - "element": "C", - "x": 2.4248227272727276, - "y": 0.19263636363636366, - "z": 0.6447727272727273 - }, - { - "element": "C", - "x": -0.6612772727272725, - "y": 1.0677363636363637, - "z": -0.27602727272727273 - }, - { - "element": "C", - "x": -0.4179772727272726, - "y": -1.3348636363636364, - "z": -0.20282727272727272 - }, - { - "element": "C", - "x": -2.0255772727272725, - "y": 0.9374363636363636, - "z": -0.01672727272727272 - }, - { - "element": "C", - "x": -1.7822772727272727, - "y": -1.4651636363636362, - "z": 0.056572727272727275 - }, - { - "element": "C", - "x": -2.5860772727272727, - "y": -0.32896363636363635, - "z": 0.14947272727272728 - }, - { - "element": "H", - "x": 1.9497227272727273, - "y": -0.7898636363636364, - "z": -1.2322272727272727 - }, - { - "element": "H", - "x": 1.7718227272727276, - "y": 0.9482363636363638, - "z": -1.2834272727272726 - }, - { - "element": "H", - "x": 2.2876227272727276, - "y": -0.6969636363636362, - "z": 1.2699727272727273 - }, - { - "element": "H", - "x": 2.0843227272727276, - "y": 1.0511363636363638, - "z": 1.2350727272727273 - }, - { - "element": "H", - "x": -0.22987727272727254, - "y": 2.0577363636363635, - "z": -0.40282727272727276 - }, - { - "element": "H", - "x": 0.1980227272727274, - "y": -2.2276636363636366, - "z": -0.27102727272727273 - }, - { - "element": "H", - "x": -2.205877272727273, - "y": -2.4577636363636364, - "z": 0.1844727272727273 - }, - { - "element": "H", - "x": 3.9854227272727276, - "y": 1.1786363636363637, - "z": -0.22292727272727272 - }, - { - "element": "H", - "x": 4.174822727272728, - "y": -0.4366636363636363, - "z": -0.19302727272727271 - }, - { - "element": "H", - "x": -3.7154772727272727, - "y": 1.7784363636363638, - "z": 0.2535727272727273 - }, - { - "element": "H", - "x": -4.125277272727272, - "y": -1.4155636363636364, - "z": 0.4879727272727273 - } - ], - "bonds": [ - [ - 0, - 8, - 1 - ], - [ - 0, - 20, - 1 - ], - [ - 1, - 10, - 1 - ], - [ - 1, - 21, - 1 - ], - [ - 2, - 5, - 1 - ], - [ - 2, - 18, - 1 - ], - [ - 2, - 19, - 1 - ], - [ - 3, - 4, - 1 - ], - [ - 3, - 5, - 1 - ], - [ - 3, - 11, - 1 - ], - [ - 3, - 12, - 1 - ], - [ - 4, - 6, - 2 - ], - [ - 4, - 7, - 1 - ], - [ - 5, - 13, - 1 - ], - [ - 5, - 14, - 1 - ], - [ - 6, - 8, - 1 - ], - [ - 6, - 15, - 1 - ], - [ - 7, - 9, - 2 - ], - [ - 7, - 16, - 1 - ], - [ - 8, - 10, - 2 - ], - [ - 9, - 10, - 1 - ], - [ - 9, - 17, - 1 - ] - ] -} \ No newline at end of file diff --git a/public/assets/molecules/gaba.json b/public/assets/molecules/gaba.json deleted file mode 100644 index 74c64bb..0000000 --- a/public/assets/molecules/gaba.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "id": "gaba", - "cid": 119, - "source": "https://pubchem.ncbi.nlm.nih.gov/compound/119", - "download": "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/119/record/JSON?record_type=3d", - "sha256": "39c3be7dde9cdd048e0c1a94db2dd0d54ae0c6ee7d1ff612dd4bce52bd80a4d6", - "note": "One PubChem 3D conformer, recentered and rescaled. Atom sizes are illustrative; protonation is that of the source record, not a physiological pH calculation. Glutamate view uses L-glutamic acid.", - "atoms": [ - { - "element": "O", - "x": -3.2616812499999996, - "y": 0.61304375, - "z": 0.0318375 - }, - { - "element": "O", - "x": -2.22058125, - "y": -1.40475625, - "z": -0.0256625 - }, - { - "element": "N", - "x": 2.7931187499999997, - "y": -0.45855625, - "z": 0.025637499999999997 - }, - { - "element": "C", - "x": 0.3457187500000001, - "y": -0.31575625, - "z": 0.021437499999999998 - }, - { - "element": "C", - "x": -0.87378125, - "y": 0.59934375, - "z": -0.03376250000000001 - }, - { - "element": "C", - "x": 1.6620187499999999, - "y": 0.46074375000000006, - "z": -0.0335625 - }, - { - "element": "C", - "x": -2.16388125, - "y": -0.18175625, - "z": -0.001062500000000002 - }, - { - "element": "H", - "x": 0.3074187500000001, - "y": -0.91505625, - "z": 0.9409375000000001 - }, - { - "element": "H", - "x": 0.3050187500000001, - "y": -1.02675625, - "z": -0.8144625 - }, - { - "element": "H", - "x": -0.85748125, - "y": 1.1892437500000002, - "z": -0.9566625 - }, - { - "element": "H", - "x": -0.8639812499999999, - "y": 1.27974375, - "z": 0.8247375 - }, - { - "element": "H", - "x": 1.7215187500000002, - "y": 1.04464375, - "z": -0.9581624999999999 - }, - { - "element": "H", - "x": 1.72541875, - "y": 1.16264375, - "z": 0.8047375 - }, - { - "element": "H", - "x": 2.7348187499999996, - "y": -1.1230562499999999, - "z": -0.7452624999999999 - }, - { - "element": "H", - "x": 2.7383187500000004, - "y": -1.01435625, - "z": 0.8784375000000001 - }, - { - "element": "H", - "x": -4.09198125, - "y": 0.09064375000000001, - "z": 0.04083749999999999 - } - ], - "bonds": [ - [ - 0, - 6, - 1 - ], - [ - 0, - 15, - 1 - ], - [ - 1, - 6, - 2 - ], - [ - 2, - 5, - 1 - ], - [ - 2, - 13, - 1 - ], - [ - 2, - 14, - 1 - ], - [ - 3, - 4, - 1 - ], - [ - 3, - 5, - 1 - ], - [ - 3, - 7, - 1 - ], - [ - 3, - 8, - 1 - ], - [ - 4, - 6, - 1 - ], - [ - 4, - 9, - 1 - ], - [ - 4, - 10, - 1 - ], - [ - 5, - 11, - 1 - ], - [ - 5, - 12, - 1 - ] - ] -} \ No newline at end of file diff --git a/public/assets/molecules/glutamate.json b/public/assets/molecules/glutamate.json deleted file mode 100644 index 6d27e1d..0000000 --- a/public/assets/molecules/glutamate.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "id": "glutamate", - "cid": 33032, - "source": "https://pubchem.ncbi.nlm.nih.gov/compound/33032", - "download": "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/33032/record/JSON?record_type=3d", - "sha256": "c939497e1ce0ac81a82f6ad010e7044b42ddb488487c5e76bec8ae8d890ed66d", - "note": "One PubChem 3D conformer, recentered and rescaled. Atom sizes are illustrative; protonation is that of the source record, not a physiological pH calculation. Glutamate view uses L-glutamic acid.", - "atoms": [ - { - "element": "O", - "x": 1.7732105263157893, - "y": 1.6905842105263158, - "z": -0.9598473684210527 - }, - { - "element": "O", - "x": -3.2103894736842102, - "y": 0.9236842105263158, - "z": 1.1248526315789473 - }, - { - "element": "O", - "x": 2.4937105263157897, - "y": 0.9575842105263158, - "z": 1.0608526315789475 - }, - { - "element": "O", - "x": -3.0124894736842105, - "y": -0.01811578947368417, - "z": -0.9334473684210526 - }, - { - "element": "N", - "x": 1.5865105263157895, - "y": -1.5631157894736842, - "z": 0.6348526315789474 - }, - { - "element": "C", - "x": -0.21788947368421058, - "y": -0.4483157894736842, - "z": -0.6691473684210526 - }, - { - "element": "C", - "x": 1.2915105263157893, - "y": -0.5804157894736842, - "z": -0.4057473684210526 - }, - { - "element": "C", - "x": -1.0549894736842105, - "y": 0.13338421052631583, - "z": 0.4733526315789474 - }, - { - "element": "C", - "x": 1.9196105263157894, - "y": 0.7406842105263158, - "z": 0.0014526315789473929 - }, - { - "element": "C", - "x": -2.5096894736842104, - "y": 0.3297842105263158, - "z": 0.1272526315789474 - }, - { - "element": "H", - "x": -0.6222894736842106, - "y": -1.4320157894736842, - "z": -0.9439473684210525 - }, - { - "element": "H", - "x": -0.36088947368421054, - "y": 0.18288421052631582, - "z": -1.5567473684210527 - }, - { - "element": "H", - "x": 1.7895105263157893, - "y": -0.9102157894736842, - "z": -1.3253473684210526 - }, - { - "element": "H", - "x": -1.0069894736842107, - "y": -0.5320157894736842, - "z": 1.3423526315789474 - }, - { - "element": "H", - "x": -0.6583894736842106, - "y": 1.1132842105263159, - "z": 0.7610526315789474 - }, - { - "element": "H", - "x": 2.5938105263157896, - "y": -1.7079157894736843, - "z": 0.6948526315789474 - }, - { - "element": "H", - "x": 1.1845105263157893, - "y": -2.463915789473684, - "z": 0.3781526315789474 - }, - { - "element": "H", - "x": 2.1772105263157897, - "y": 2.544084210526316, - "z": -0.6943473684210526 - }, - { - "element": "H", - "x": -4.15558947368421, - "y": 1.0400842105263157, - "z": 0.8895526315789474 - } - ], - "bonds": [ - [ - 0, - 8, - 1 - ], - [ - 0, - 17, - 1 - ], - [ - 1, - 9, - 1 - ], - [ - 1, - 18, - 1 - ], - [ - 2, - 8, - 2 - ], - [ - 3, - 9, - 2 - ], - [ - 4, - 6, - 1 - ], - [ - 4, - 15, - 1 - ], - [ - 4, - 16, - 1 - ], - [ - 5, - 6, - 1 - ], - [ - 5, - 7, - 1 - ], - [ - 5, - 10, - 1 - ], - [ - 5, - 11, - 1 - ], - [ - 6, - 8, - 1 - ], - [ - 6, - 12, - 1 - ], - [ - 7, - 9, - 1 - ], - [ - 7, - 13, - 1 - ], - [ - 7, - 14, - 1 - ] - ] -} \ No newline at end of file diff --git a/public/brain-comparison.js b/public/brain-comparison.js new file mode 100644 index 0000000..4eac0c1 --- /dev/null +++ b/public/brain-comparison.js @@ -0,0 +1,841 @@ +import { configureHDR, hdrCapable } from "./display.js"; + +/** A source-guided atlas tour. Its authored clock never consumes silicon ticks. */ +export async function initBrainComparison(wasm) { + const $ = (selector) => document.querySelector(selector); + let canvas = $("#brain-canvas"); + if (!canvas) throw new Error("Comparison atlas canvas is missing"); + window.dokeBrainComparison?.dispose?.(); + const controls = { + title: $("#brain-guide-title"), + copy: $("#brain-guide-copy"), + time: $("#brain-guide-time"), + play: $("#brain-guide-play"), + next: $("#brain-guide-next"), + reset: $("#brain-guide-reset"), + stimulus: $("#brain-stimulus"), + }; + const abort = new AbortController(); + const reducedQuery = matchMedia("(prefers-reduced-motion: reduce)"); + let reduced = $("#reduce-motion")?.checked ?? reducedQuery.matches; + let renderer = null; + let comparison = null; + let atlas = null; + let elapsed = 0; + let playing = false; + let ready = false; + let disposed = false; + let error = null; + let hdr = false; + let dirty = true; + let frameId = 0; + let lastFrame = null; + let lastStage = -1; + let lastTimeText = ""; + let resizeObserver = null; + let pointers = new Map(); + let pinchDistance = null; + let lastInputKey = ""; + let input = Array(64).fill(0); + let selectedRegion = null; + let cutaway = "whole"; + let quality = "overview"; + let requestedQuality = "overview"; + let qualityLoading = false; + let qualityError = null; + let qualityToken = 0; + let qualityAbort = null; + let overviewMesh = null; + let activeMesh = null; + const listen = (target, type, handler, options = {}) => + target?.addEventListener(type, handler, { + ...options, + signal: abort.signal, + }); + const duration = () => comparison?.brain?.durationSeconds || 16; + const stages = () => comparison?.brain?.stages || []; + const stageIndex = () => { + const index = stages().findIndex((stage) => elapsed < stage.endSeconds); + return index < 0 ? Math.max(0, stages().length - 1) : index; + }; + const currentStage = () => stages()[stageIndex()]; + + function snapshot() { + return { + ready, + disposed, + error, + playing, + time: elapsed, + duration: duration(), + timing: "Guide time · authored pacing", + stageIndex: stageIndex(), + stageId: currentStage()?.id || null, + regions: [...(currentStage()?.regions || [])], + input: [...input], + reducedMotion: reduced, + hdr, + backend: renderer?.backend() || null, + quality, + requestedQuality, + qualityLoading, + qualityError, + selectedRegion, + cutaway, + pixelWidth: canvas.width, + pixelHeight: canvas.height, + }; + } + function selectRegion(id, focus = false) { + const region = atlas?.regions.find((item) => item.id === id); + if (!region || !renderer || !ready || disposed) return false; + selectedRegion = id; + renderer.select(region.index); + if (focus && Array.isArray(region.centroid)) + renderer.focus(...region.centroid); + evidence(region); + dirty = true; + return true; + } + function setCutaway(mode = "whole") { + const views = { whole: [0, 2], left: [2, 2], right: [3, 2], deep: [1, 2] }; + if (!Object.hasOwn(views, mode)) + throw new RangeError("Unknown anatomical view"); + cutaway = mode; + renderer?.reveal(...views[mode]); + dirty = true; + } + function resetView() { + if (disposed) return; + selectedRegion = null; + renderer?.select(-1); + setCutaway("whole"); + initialView(); + } + function loadingStatus(message, failure = false) { + const element = $("#brain-loading"); + if (!element) return; + element.hidden = !message; + element.textContent = message || ""; + element.dataset.error = String(failure); + } + function qualityStatus(message) { + const element = $("#quality-status"); + if (element) element.textContent = message; + } + async function boundedBytes(response, limit, signal) { + if (!response.ok) + throw Error(`Atlas download unavailable (${response.status})`); + const reader = response.body?.getReader(); + if (!reader) throw Error("Atlas download stream unavailable"); + const parts = []; + let length = 0; + try { + while (true) { + signal.throwIfAborted(); + const { value, done } = await reader.read(); + if (done) break; + length += value.byteLength; + if (length > limit) throw Error("Atlas data exceeds its declared size"); + parts.push(value); + } + signal.throwIfAborted(); + } catch (cause) { + await reader.cancel().catch(() => {}); + throw cause; + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(length); + let offset = 0; + for (const part of parts) { + bytes.set(part, offset); + offset += part.byteLength; + } + return bytes; + } + async function sha256(bytes) { + if (!crypto.subtle) + throw Error("Secure checksums are unavailable in this browser"); + return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))] + .map((n) => n.toString(16).padStart(2, "0")) + .join(""); + } + async function detailedMesh(tier, signal, progress) { + if (!globalThis.DecompressionStream) + throw Error( + "This browser cannot decode detailed anatomy; Overview is still available", + ); + const response = await fetch(`assets/${tier}/manifest.json`, { signal }); + const raw = await boundedBytes(response, 65536, signal); + const meta = JSON.parse(new TextDecoder().decode(raw)); + const integer = (n, low, high) => + Number.isSafeInteger(n) && n >= low && n <= high; + const hash = (s) => typeof s === "string" && /^[a-f0-9]{64}$/.test(s); + if ( + meta.encoding !== "gzip-chunks" || + !integer(meta.bytes, 16, 200_000_000) || + !integer(meta.compressedBytes, 1, 120_000_000) || + !integer(meta.triangles, 1, 6_000_000) || + !hash(meta.meshHash) || + !Array.isArray(meta.chunks) || + meta.chunks.length < 1 || + meta.chunks.length > 32 + ) + throw Error("Invalid anatomy manifest"); + let decodedTotal = 0, + compressedTotal = 0; + const urls = new Set(); + for (const part of meta.chunks) { + if ( + !integer(part.bytes, 1, 12_000_000) || + !integer(part.decodedBytes, 1, 9_000_000) || + !hash(part.sha256) || + typeof part.url !== "string" || + !new RegExp(`^assets/${tier}/part-[0-9]{3}\\.brnz$`).test(part.url) || + urls.has(part.url) + ) + throw Error("Invalid anatomy chunk manifest"); + urls.add(part.url); + decodedTotal += part.decodedBytes; + compressedTotal += part.bytes; + } + if (decodedTotal !== meta.bytes || compressedTotal !== meta.compressedBytes) + throw Error("Anatomy manifest totals do not match"); + const bytes = new Uint8Array(meta.bytes); + let offset = 0, + downloaded = 0; + for (const part of meta.chunks) { + signal.throwIfAborted(); + const response = await fetch(part.url, { signal }); + const compressed = await boundedBytes(response, part.bytes, signal); + if (compressed.length !== part.bytes) + throw Error("Incomplete anatomy chunk"); + if ((await sha256(compressed)) !== part.sha256) + throw Error("Anatomy chunk checksum mismatch"); + signal.throwIfAborted(); + const decoded = await boundedBytes( + new Response( + new Blob([compressed]) + .stream() + .pipeThrough(new DecompressionStream("gzip")), + ), + part.decodedBytes, + signal, + ); + if (decoded.length !== part.decodedBytes) + throw Error("Anatomy chunk decoded size mismatch"); + bytes.set(decoded, offset); + offset += decoded.length; + downloaded += part.bytes; + progress(downloaded / meta.compressedBytes, meta); + } + if ((await sha256(bytes)) !== meta.meshHash) + throw Error("Anatomy mesh checksum mismatch"); + signal.throwIfAborted(); + const header = new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ); + if ( + new TextDecoder().decode(bytes.subarray(0, 4)) !== "BRN1" || + header.getUint32(8, true) / 3 !== meta.triangles + ) + throw Error("Anatomy mesh does not match its manifest"); + return bytes; + } + async function setQuality(tier) { + if (!["overview", "fine", "native"].includes(tier)) + throw new RangeError("Unknown brain quality"); + if (disposed || !ready || !renderer) return false; + const token = ++qualityToken; + qualityAbort?.abort(); + qualityAbort = new AbortController(); + const signal = qualityAbort.signal; + requestedQuality = tier; + qualityLoading = true; + qualityError = null; + qualityStatus(`Preparing ${tier} anatomy…`); + try { + const bytes = + tier === quality + ? activeMesh + : tier === "overview" + ? overviewMesh + : await detailedMesh(tier, signal, (fraction, meta) => { + if (token === qualityToken && !disposed) + qualityStatus( + `${tier === "native" ? "Native" : "Fine"} anatomy · ${Math.round(fraction * 100)}% of ${Math.round(meta.compressedBytes / 1e6)} MB`, + ); + }); + signal.throwIfAborted(); + if (disposed || token !== qualityToken) return false; + if (bytes !== activeMesh) renderer.load_mesh(bytes); + activeMesh = bytes; + quality = tier; + const select = $("#brain-quality"); + if (select) select.value = quality; + qualityStatus( + `${quality[0].toUpperCase() + quality.slice(1)} · ${renderer.triangle_count().toLocaleString()} triangles`, + ); + highlight(); + dirty = true; + return true; + } catch (cause) { + if (token !== qualityToken || disposed) return false; + qualityError = + cause.name === "AbortError" + ? "Detail download canceled" + : cause.message || String(cause); + requestedQuality = quality; + const select = $("#brain-quality"); + if (select) select.value = quality; + qualityStatus(`${qualityError}. Keeping ${quality} anatomy.`); + return false; + } finally { + if (token === qualityToken) qualityLoading = false; + } + } + function drawStimulus(values) { + if ( + !Array.isArray(values) || + values.length !== 64 || + values.some((v) => !Number.isFinite(v) || v < -8 || v > 8) + ) + return; + const key = values.join(","); + if (key === lastInputKey) return; + lastInputKey = key; + input = [...values]; + const element = controls.stimulus; + if (!element) return; + // An actual copy of the same matrix, never a predicted neural response. + element.setAttribute( + "aria-label", + "The same 8 by 8 input image shown on the silicon side. It does not drive a brain simulation.", + ); + if (!(element instanceof HTMLCanvasElement)) return; + const context = element.getContext("2d"); + if (!context) return; + const width = element.width || 128; + const height = element.height || 128; + context.clearRect(0, 0, width, height); + for (let i = 0; i < 64; i++) { + const value = values[i]; + context.fillStyle = + value < 0 + ? `rgb(${35 + Math.min(8, -value) * 12},${46 + Math.min(8, -value) * 7},${54 + Math.min(8, -value) * 4})` + : `rgb(${27 + value * 26},${34 + value * 26},${35 + value * 25})`; + context.fillRect( + ((i % 8) * width) / 8 + 1, + (Math.floor(i / 8) * height) / 8 + 1, + width / 8 - 2, + height / 8 - 2, + ); + } + element.dataset.input = key; + } + function highlight() { + if (!renderer || !atlas) return; + const values = new Float32Array(64); + for (const id of $("#brain-guide")?.hidden + ? [] + : currentStage()?.regions || []) { + const region = atlas.regions.find((region) => region.id === id); + if (region) values[region.index] = 0.65; + } + renderer.activity(values); + dirty = true; + } + function update() { + const stage = currentStage(); + const index = stageIndex(); + if (stage && index !== lastStage) { + lastStage = index; + if (controls.title) controls.title.textContent = stage.title; + if (controls.copy) + controls.copy.textContent = + stage.text + + (error && !ready + ? " 3D is unavailable; the guide and evidence still work." + : ""); + highlight(); + } + const text = `${elapsed.toFixed(1)} / ${duration()} s · guide time`; + if (controls.time && text !== lastTimeText) { + controls.time.textContent = text; + controls.time.title = + "Authored reading pace, not measured neural response time."; + lastTimeText = text; + } + if (controls.play) { + controls.play.textContent = playing + ? "Pause guide" + : elapsed >= duration() + ? "Replay guide" + : "Play guide"; + controls.play.disabled = !comparison; + controls.play.setAttribute("aria-pressed", String(playing)); + } + if (controls.next) + controls.next.disabled = !comparison || index >= stages().length - 1; + if (controls.reset) controls.reset.disabled = !comparison; + canvas.dataset.guideStage = stage?.id || "loading"; + canvas.dataset.guidePlaying = String(playing); + } + function play() { + if (disposed || !comparison || document.hidden) return; + if (elapsed >= duration()) { + elapsed = 0; + lastStage = -1; + } + playing = true; + lastFrame = null; + update(); + } + function pause() { + playing = false; + lastFrame = null; + update(); + } + function next() { + if (!comparison || disposed) return; + pause(); + elapsed = + stages()[Math.min(stageIndex() + 1, stages().length - 1)].startSeconds; + update(); + } + function reset() { + if (disposed) return; + playing = false; + elapsed = 0; + lastFrame = null; + lastStage = -1; + selectedRegion = null; + renderer?.select(-1); + update(); + } + function evidence(region = null) { + if (!comparison) return; + const stage = currentStage(); + const sourceIds = [ + ...new Set( + region + ? ["atlas", ...(stage?.sources || [])] + : ["atlas", ...(stage?.sources || []), "vision-timing", "bold"], + ), + ]; + const sources = sourceIds.map((id) => ({ id, ...comparison.sources[id] })); + const title = region ? region.name : stage?.title || comparison.title; + const paragraphs = region + ? [ + `Anatomical reference: ${region.name}. This teaching surface groups atlas labels; it is not a measured response to your image.`, + comparison.brain.scope, + ] + : [ + stage?.text || comparison.summary, + comparison.brain.scope, + "The 16-second clock is authored reading pace. Silicon model ticks, physiological response times and this guide clock are independent.", + ]; + const attribution = atlas + ? `${atlas.attribution} License: ${atlas.license}.` + : ""; + if (attribution) paragraphs.push(attribution); + window.dispatchEvent( + new CustomEvent("doke-comparison-evidence", { + detail: { + regionId: region?.id || null, + region: region ? structuredClone(region) : null, + title, + sourceIds, + sources, + scope: comparison.brain.scope, + attribution, + panel: { + id: region + ? `brain-region:${region.id}` + : `brain-guide:${stage?.id || "overview"}`, + title, + eyebrow: "ANATOMICAL CONTEXT · FOLLOW THE EVIDENCE", + paragraphs, + sources, + }, + }, + }), + ); + } + function resize() { + if (disposed) return; + const rect = canvas.getBoundingClientRect(); + const mode = $("#resolution")?.value || "sharp"; + const dpr = devicePixelRatio || 1; + const pixelRatio = + mode === "balanced" + ? Math.min(dpr, 1.5) + : mode === "ultra" + ? Math.max(3, dpr * 1.75) + : Math.max(2, dpr * 1.25); + const wantedWidth = Math.max(1, rect.width * pixelRatio); + const wantedHeight = Math.max(1, rect.height * pixelRatio); + const fit = Math.min( + 1, + 4096 / wantedWidth, + 4096 / wantedHeight, + Math.sqrt(12_000_000 / (wantedWidth * wantedHeight)), + ); + const width = Math.max(1, Math.floor(wantedWidth * fit)); + const height = Math.max(1, Math.floor(wantedHeight * fit)); + if (canvas.width !== width) canvas.width = width; + if (canvas.height !== height) canvas.height = height; + if (renderer) { + renderer.resize(width, height); + updateLight(); + } + if ( + document.body.dataset.world === "brain" && + renderer && + $("#render-status") + ) + $("#render-status").textContent = + renderer.backend() + " · " + width.toLocaleString() + " px"; + dirty = true; + } + function updateLight() { + const mode = $("#light-mode")?.value || "auto"; + hdr = configureHDR(canvas, renderer, mode === "auto"); + renderer?.hdr_output(hdr); + renderer?.glow(mode === "quiet" ? 0.25 : reduced ? 0.8 : 1.3); + dirty = true; + } + function initialView() { + renderer?.view(5); + renderer?.orbit(0, -0.35); + dirty = true; + } + function bindCanvas() { + canvas.tabIndex = 0; + canvas.setAttribute( + "aria-label", + "Reference brain atlas. Drag to orbit, pinch or scroll to zoom. Arrow keys orbit, plus and minus zoom, Home resets the camera. The guide highlights anatomical landmarks, not a simulated response.", + ); + canvas.style.touchAction = "none"; + listen(canvas, "pointerdown", (event) => { + if (event.pointerType === "mouse" && event.button !== 0) return; + canvas.focus({ preventScroll: true }); + canvas.setPointerCapture(event.pointerId); + pointers.set(event.pointerId, { + x: event.clientX, + y: event.clientY, + startX: event.clientX, + startY: event.clientY, + moved: pointers.size > 0, + }); + if (pointers.size > 1) { + for (const point of pointers.values()) point.moved = true; + const [a, b] = [...pointers.values()]; + pinchDistance = Math.hypot(a.x - b.x, a.y - b.y); + } + }); + listen(canvas, "pointermove", (event) => { + const point = pointers.get(event.pointerId); + if (!point) return; + const dx = event.clientX - point.x, + dy = event.clientY - point.y; + point.x = event.clientX; + point.y = event.clientY; + if (Math.hypot(point.x - point.startX, point.y - point.startY) > 4) + point.moved = true; + if (pointers.size > 1) { + const [a, b] = [...pointers.values()]; + const distance = Math.hypot(a.x - b.x, a.y - b.y); + if (distance > 0 && pinchDistance > 0) + renderer?.zoom(Math.log(pinchDistance / distance)); + pinchDistance = distance; + } else renderer?.orbit(dx * 0.005, dy * 0.005); + dirty = true; + }); + listen(canvas, "pointerup", (event) => { + const point = pointers.get(event.pointerId); + if (point && !point.moved && renderer && atlas) { + const rect = canvas.getBoundingClientRect(); + if (rect.width && rect.height) { + const index = renderer.pick( + (event.clientX - rect.left) / rect.width, + (event.clientY - rect.top) / rect.height, + ); + const region = atlas.regions.find((region) => region.index === index); + if (region) selectRegion(region.id); + } + } + pointers.delete(event.pointerId); + pinchDistance = null; + }); + const release = (event) => { + pointers.delete(event.pointerId); + pinchDistance = null; + }; + listen(canvas, "pointercancel", release); + listen(canvas, "lostpointercapture", release); + listen( + canvas, + "wheel", + (event) => { + event.preventDefault(); + renderer?.zoom(Math.max(-0.2, Math.min(0.2, event.deltaY * 0.001))); + dirty = true; + }, + { passive: false }, + ); + listen(canvas, "keydown", (event) => { + const move = { + ArrowLeft: [-0.1, 0], + ArrowRight: [0.1, 0], + ArrowUp: [0, -0.1], + ArrowDown: [0, 0.1], + }[event.key]; + if (move) { + event.preventDefault(); + renderer?.orbit(...move); + } else if (event.key === "+" || event.key === "=") { + event.preventDefault(); + renderer?.zoom(-0.15); + } else if (event.key === "-") { + event.preventDefault(); + renderer?.zoom(0.15); + } else if (event.key === "Home") { + event.preventDefault(); + initialView(); + } else if (event.key === "Enter") evidence(); + else return; + dirty = true; + }); + } + function frame(now) { + if (disposed) return; + const dt = + lastFrame === null + ? 0 + : Math.min(0.1, Math.max(0, (now - lastFrame) / 1000)); + lastFrame = now; + if (!document.hidden) { + if (playing) { + elapsed = Math.min(duration(), elapsed + dt); + if (elapsed >= duration()) playing = false; + update(); + } + if (dirty && renderer) { + try { + renderer.render(0); + } catch (cause) { + error = `Atlas graphics interrupted: ${cause?.message || cause}`; + ready = false; + pause(); + canvas.dataset.error = error; + canvas.dataset.ready = "false"; + loadingStatus( + "The 3D atlas is unavailable. Reload to retry; anatomy and evidence remain available.", + true, + ); + if (controls.copy) + controls.copy.textContent = `${error}. Reload to restore the 3D view. The evidence remains available.`; + } + dirty = false; + } + } + frameId = requestAnimationFrame(frame); + } + function dispose() { + if (disposed) return; + disposed = true; + playing = false; + cancelAnimationFrame(frameId); + abort.abort(); + qualityToken++; + qualityAbort?.abort(); + qualityLoading = false; + overviewMesh = null; + activeMesh = null; + resizeObserver?.disconnect(); + pointers.clear(); + renderer?.free?.(); + renderer = null; + ready = false; + canvas.dataset.ready = "false"; + canvas.dataset.disposed = "true"; + } + const controller = { + play, + pause, + next, + reset, + dispose, + snapshot, + getRenderer: () => renderer, + getAtlas: () => (atlas ? structuredClone(atlas) : null), + selectRegion, + setCutaway, + resetView, + redraw: () => { + resize(); + highlight(); + dirty = true; + }, + setQuality, + get state() { + return snapshot(); + }, + get renderer() { + return renderer; + }, + }; + window.dokeBrainComparison = controller; + listen(controls.play, "click", () => (playing ? pause() : play())); + listen(controls.next, "click", next); + listen(controls.reset, "click", reset); + listen(window, "doke-silicon-state", (event) => + drawStimulus(event.detail?.input), + ); + listen(document, "visibilitychange", () => { + if (document.hidden) pause(); + lastFrame = null; + }); + listen(reducedQuery, "change", (event) => { + reduced = event.matches; + updateLight(); + if (reduced) pause(); + dirty = true; + }); + listen($("#light-mode"), "change", resize); + listen($("#resolution"), "change", resize); + listen($("#brain-quality"), "change", (event) => { + void setQuality(event.target.value); + }); + listen($("#reduce-motion"), "change", (event) => { + reduced = event.target.checked; + if (reduced) pause(); + updateLight(); + }); + listen(window, "resize", resize); + drawStimulus(window.dokeSilicon?.input?.() || input); + if ($("#brain-quality")) $("#brain-quality").disabled = true; + loadingStatus("Opening the atlas…"); + update(); + try { + const get = async (url, json) => { + const response = await fetch(url, { signal: abort.signal }); + if (!response.ok) + throw new Error(`${url} unavailable (${response.status})`); + return json + ? response.json() + : new Uint8Array(await response.arrayBuffer()); + }; + const [guideResult, atlasResult, meshResult] = await Promise.allSettled([ + get("comparison.json", true), + get("assets/atlas.json", true), + get("assets/brain.bin", false), + ]); + if (disposed) return controller; + if (guideResult.status === "rejected") throw guideResult.reason; + const data = guideResult.value; + const guide = data?.brain; + if ( + !guide || + guide.durationSeconds !== 16 || + !Array.isArray(guide.stages) || + guide.stages.length !== 4 || + !data.sources || + guide.stages.some( + (stage, i) => + stage.startSeconds !== i * 4 || + stage.endSeconds !== (i + 1) * 4 || + typeof stage.title !== "string" || + typeof stage.text !== "string" || + !Array.isArray(stage.regions) || + !Array.isArray(stage.sources) || + stage.sources.some((id) => !data.sources[id]), + ) + ) { + throw new Error("Comparison guide is invalid"); + } + // Preserve valid explanations and source access even when the mesh fails. + comparison = data; + if (atlasResult.status === "rejected") throw atlasResult.reason; + atlas = atlasResult.value; + if ( + !Array.isArray(atlas.regions) || + guide.stages.some((stage) => + stage.regions.some( + (id) => !atlas.regions.some((region) => region.id === id), + ), + ) + ) { + throw new Error("Comparison guide does not match the atlas"); + } + if (meshResult.status === "rejected") throw meshResult.reason; + const mesh = meshResult.value; + overviewMesh = mesh; + resize(); + const forceGL = + !navigator.gpu || + new URLSearchParams(location.search).get("backend") === "webgl"; + try { + renderer = await wasm.BrainRenderer.create(canvas, forceGL, hdrCapable()); + } catch (cause) { + if (forceGL || disposed) throw cause; + const replacement = canvas.cloneNode(); + canvas.replaceWith(replacement); + canvas = replacement; + resize(); + renderer = await wasm.BrainRenderer.create(canvas, true, false); + } + if (disposed) { + renderer.free?.(); + renderer = null; + return controller; + } + renderer.load_mesh(mesh); + activeMesh = mesh; + renderer.anatomy(); + renderer.accents(true); + initialView(); + resize(); + bindCanvas(); + resizeObserver = new ResizeObserver(resize); + resizeObserver.observe(canvas); + ready = true; + if ($("#brain-quality")) { + $("#brain-quality").disabled = false; + $("#brain-quality").value = quality; + } + loadingStatus(""); + qualityStatus( + `Overview · ${renderer.triangle_count().toLocaleString()} triangles`, + ); + canvas.dataset.ready = "true"; + canvas.dataset.backend = renderer.backend(); + lastStage = -1; + update(); + } catch (cause) { + if (disposed) return controller; + error = `Reference atlas unavailable: ${cause?.message || cause}`; + renderer?.free?.(); + renderer = null; + canvas.dataset.ready = "false"; + canvas.dataset.error = error; + loadingStatus( + "The 3D atlas is unavailable. Reload to retry; the guide and evidence remain available.", + true, + ); + update(); + if (!comparison && controls.title) + controls.title.textContent = "Reference anatomy"; + if (controls.copy) + controls.copy.textContent = comparison + ? `${currentStage().text} 3D is unavailable; the guide and evidence still work.` + : `${error}. Reload to retry loading the guide.`; + console.warn(error); + } + if (!disposed) frameId = requestAnimationFrame(frame); + return controller; +} diff --git a/public/brain.json b/public/brain.json new file mode 100644 index 0000000..3759c8f --- /dev/null +++ b/public/brain.json @@ -0,0 +1,216 @@ +{ + "version": "2.0.0", + "scope": "Reference anatomy and source-guided visual pathway. Not a model of individual perception or neural activity.", + "regions": { + "motor": { + "summary": "On the frontal side of the central sulcus, the precentral gyrus includes primary motor cortex. It participates in control of voluntary movement.", + "sources": ["allen", "brain"] + }, + "prefrontal": { + "summary": "These frontal gyri participate in planning, working memory and cognitive control. Their contributions depend on the task and their interactions with other regions.", + "sources": ["allen", "brain"] + }, + "inferior-frontal": { + "summary": "Inferior frontal regions participate in language and control processes. A broad anatomical gyrus is not identical to a functional language area.", + "sources": ["allen", "brain"] + }, + "orbitofrontal": { + "summary": "Orbital frontal regions participate in evaluating outcomes and guiding decisions in context. Valuation is distributed across several interacting structures.", + "sources": ["allen", "brain"] + }, + "somatosensory": { + "summary": "The postcentral gyrus includes primary somatosensory cortex, which processes information about the body. The atlas does not resolve a particular finger representation.", + "sources": ["allen", "brain"] + }, + "parietal": { + "summary": "Superior parietal regions participate in spatial processing and coordination of sensory information with action.", + "sources": ["allen", "brain"] + }, + "inferior-parietal": { + "summary": "The angular and supramarginal gyri sit at an association-cortex crossroads. Their roles include integration across sensory and cognitive domains.", + "sources": ["allen", "brain"] + }, + "precuneus": { + "summary": "A medial parietal region involved in several internally directed and spatial processes. It participates in distributed networks rather than a single isolated function.", + "sources": ["allen", "yeo"] + }, + "superior-temporal": { + "summary": "Superior temporal regions participate in auditory processing. This teaching group also includes adjacent temporal landmarks named in the atlas.", + "sources": ["allen", "brain"] + }, + "middle-temporal": { + "summary": "A temporal association region. The atlas defines its anatomical boundary; functional patterns can span it and neighboring cortex.", + "sources": ["allen"] + }, + "inferior-temporal": { + "summary": "Part of the broader ventral visual system involved in object representations. Information is distributed across overlapping response patterns.", + "sources": ["allen", "vision"] + }, + "fusiform": { + "summary": "The fusiform gyrus lies on the ventral temporal-occipital surface. It participates in visual recognition; the entire gyrus is not a dedicated \u201cface center.\u201d", + "sources": ["allen", "vision"] + }, + "auditory": { + "summary": "Heschl\u2019s gyrus is an important auditory landmark on the superior temporal plane. It becomes easier to inspect when the surrounding cortex is hidden.", + "sources": ["allen", "brain"] + }, + "visual": { + "summary": "Occipital structures participate in visual processing. This grouped mesh contains several anatomical labels and does not delineate every visual area.", + "sources": ["allen", "brain"] + }, + "insula": { + "summary": "The insula lies within the lateral sulcus, beneath surrounding cortex. Its broad roles include integration of bodily and sensory information.", + "sources": ["allen", "brain"] + }, + "anterior-cingulate": { + "summary": "Anterior cingulate regions participate in control, motivation and affect-related processes. Activity here does not establish one specific emotion.", + "sources": ["allen", "brain"] + }, + "posterior-cingulate": { + "summary": "Posterior medial cortex participates in intrinsic functional networks. Its involvement changes with task and context.", + "sources": ["allen", "yeo"] + }, + "parahippocampal": { + "summary": "These medial temporal gyri neighbor the hippocampus. This is an anatomical grouping, not a claim of a single memory pathway.", + "sources": ["allen"] + }, + "hippocampus": { + "summary": "The hippocampus participates in forming and organizing memories, in interaction with a wider medial temporal and cortical system.", + "sources": ["allen", "brain"] + }, + "amygdala": { + "summary": "An anatomically complex set of nuclei in the medial temporal lobe. It participates in learning and the relevance of stimuli, beyond any one emotion.", + "sources": ["allen", "brain"] + }, + "caudate": { + "summary": "A striatal structure with head, body and tail. In the music study, caudate responses were associated with anticipation of peak emotion.", + "sources": ["allen", "music"] + }, + "putamen": { + "summary": "A striatal structure involved in basal ganglia circuits, including those concerned with action.", + "sources": ["allen", "basal"] + }, + "accumbens": { + "summary": "Part of the ventral striatum. The music study associated this region with peak pleasurable responses; this does not make it a universal pleasure meter.", + "sources": ["allen", "music"] + }, + "pallidum": { + "summary": "The internal and external pallidal segments participate in basal ganglia circuits. This teaching mesh groups both segments.", + "sources": ["allen", "basal"] + }, + "thalamus": { + "summary": "A collection of nuclei that relay and regulate information in interactions with cortex and other structures. This mesh groups multiple atlas nuclei.", + "sources": ["allen", "brain"] + }, + "hypothalamus": { + "summary": "A small, diverse region involved in bodily regulation and neuroendocrine functions. Its nuclei have distinct roles that are not resolved by a single global activity value.", + "sources": ["allen", "brain"] + }, + "basal-forebrain": { + "summary": "An anatomical teaching group combining basal forebrain, septal and neighboring atlas labels. Individual cell populations and projections have different signaling roles.", + "sources": ["allen"] + }, + "substantia-nigra": { + "summary": "A midbrain structure participating in basal ganglia circuits. Dopaminergic neurons are associated with its pars compacta; the atlas mesh does not separate all cell types.", + "sources": ["allen", "basal"] + }, + "midbrain": { + "summary": "This teaching group combines midbrain tegmental and orienting structures. It is not a cell-specific dopamine source map.", + "sources": ["allen"] + }, + "cerebellum": { + "summary": "The cerebellum contributes to coordination and learning of movement, alongside broader cognitive roles. It interacts with systems outside the cerebellum.", + "sources": ["allen", "brain"] + }, + "pons": { + "summary": "The pons is part of the brainstem. This mesh combines atlas subdivisions and a connecting cerebellar peduncle for anatomical context.", + "sources": ["allen"] + }, + "medulla": { + "summary": "The medulla contains systems involved in autonomic regulation and pathways between brain and body. The displayed group includes adjacent labeled hindbrain tissue.", + "sources": ["allen", "brain"] + }, + "callosum": { + "summary": "The corpus callosum is a major white matter connection between the cerebral hemispheres. A bundle is not a single axon or a single signal.", + "sources": ["allen", "brain"] + }, + "white-matter": { + "summary": "White matter contains axonal pathways. Its dense internal atlas mesh is omitted from rendering to keep deeper structures visible.", + "sources": ["allen"] + }, + "olfactory": { + "summary": "This group contains the olfactory bulb, anterior olfactory nucleus and nearby olfactory cortical labels. Its boundaries come from the atlas ontology.", + "sources": ["allen"] + }, + "subthalamic": { + "summary": "A small nucleus participating in basal ganglia circuitry. Its role depends on its connections and activity patterns.", + "sources": ["allen", "basal"] + }, + "other-deep": { + "summary": "Context group: claustrum, habenular nuclei, pineal body and zona incerta. They are separate anatomical structures with different functions, grouped only for this overview.", + "sources": ["allen"] + } + }, + "sources": { + "allen": { + "title": "Allen Human Reference Atlas \u2013 3D, 2020", + "authors": "Ding and colleagues \u00b7 Allen Institute", + "year": "2020", + "method": "Anatomical atlas", + "species": "Human reference template", + "url": "https://download.alleninstitute.org/informatics-archive/allen_human_reference_atlas_3d_2020/version_1/README.pdf", + "finding": "An anatomical parcellation of the ICBM 2009b symmetric reference. The rendered meshes are derived from labeled voxels.", + "limit": "A symmetric reference atlas, not a scan of you. Teaching groups combine atlas labels; they are not functional parcels." + }, + "music": { + "title": "Anatomically distinct dopamine release during anticipation and experience of peak emotion to music", + "authors": "Salimpoor et al. \u00b7 Nature Neuroscience", + "year": "2011", + "method": "PET + fMRI", + "species": "Human", + "url": "https://pubmed.ncbi.nlm.nih.gov/21217764/", + "finding": "The study reported striatal dopamine release during intensely pleasurable music, with caudate involvement during anticipation and nucleus accumbens involvement during peak responses.", + "limit": "Selected listeners and stimuli. PET release inference and complementary fMRI responses do not provide a universal second-by-second dopamine concentration." + }, + "vision": { + "title": "Distributed and overlapping representations of faces and objects in ventral temporal cortex", + "authors": "Haxby et al. \u00b7 Science", + "year": "2001", + "method": "fMRI", + "species": "Human", + "url": "https://pubmed.ncbi.nlm.nih.gov/11577229/", + "finding": "Faces and objects elicited discriminable, distributed patterns in ventral temporal cortex. Information was not restricted to the maximally responding patch.", + "limit": "Broad atlas gyri stand in for the study\u2019s patterns. Display intensity and stage timing are authored; they are not the original fMRI values." + }, + "brain": { + "title": "Physiology, Brain", + "authors": "NCBI Bookshelf \u00b7 StatPearls", + "year": "Reference", + "method": "Anatomy and physiology reference", + "species": "Human", + "url": "https://www.ncbi.nlm.nih.gov/books/NBK551718/", + "finding": "Brain functions arise through interacting cortical, subcortical, cerebellar and brainstem systems.", + "limit": "Short region descriptions are introductory summaries, not exhaustive or exclusive functional assignments." + }, + "basal": { + "title": "Neuroanatomy, Basal Ganglia", + "authors": "NCBI Bookshelf \u00b7 StatPearls", + "year": "Reference", + "method": "Anatomy reference", + "species": "Human", + "url": "https://www.ncbi.nlm.nih.gov/books/NBK537141/", + "finding": "Basal ganglia participate in circuits related to action, learning and cognition, through interactions with cortex and thalamus.", + "limit": "Individual pathways contain cell-type and receptor-specific effects that a regional atlas does not resolve." + }, + "yeo": { + "title": "The organization of the human cerebral cortex estimated by intrinsic functional connectivity", + "authors": "Yeo et al. \u00b7 Journal of Neurophysiology", + "year": "2011", + "method": "Resting-state functional connectivity", + "species": "Human", + "url": "https://surfer.nmr.mgh.harvard.edu/pub/articles/2011/2011_-_Yeo_et_al._-_Journal_of_neurophysiology.pdf", + "finding": "Large-scale cortical networks can be estimated from patterns of functional connectivity.", + "limit": "Network membership does not imply a unique mental function or a directed anatomical connection." + } + } +} diff --git a/public/comparison.json b/public/comparison.json new file mode 100644 index 0000000..0ac03dd --- /dev/null +++ b/public/comparison.json @@ -0,0 +1,159 @@ +{ + "version": "1.0.0", + "title": "Same image, different mechanisms", + "summary": "Change an image. Follow the numbers through silicon, then explore the anatomy of seeing.", + "input": { + "width": 8, + "height": 8, + "label": "Your image", + "operation": "C = A \u00d7 B", + "description": "The image supplies matrix A. A derivative filter supplies matrix B. CPU and GPU models calculate the same filtered output C.", + "scope": "Filtering reveals changes in pixel values. It does not identify an object. The brain view is a source-guided illustration, not a calculation of your perception." + }, + "brain": { + "label": "Human anatomy \u00b7 illustrative pathway", + "timingLabel": "Guide time \u00b7 authored pacing", + "scope": "Highlights guide your attention through reference anatomy. Their timing, brightness and connections are illustrative. Unlit areas are not inactive.", + "durationSeconds": 16, + "stages": [ + { + "id": "anatomy", + "startSeconds": 0, + "endSeconds": 4, + "title": "Find the visual landmarks", + "text": "Start at the occipital surface. This broad atlas grouping helps you locate visual anatomy; it does not resolve individual visual areas or predict a response to your drawing.", + "regions": ["visual"], + "sources": ["atlas"] + }, + { + "id": "patterns", + "startSeconds": 4, + "endSeconds": 8, + "title": "Look across a pattern", + "text": "A human fMRI study found distinguishable responses to visual categories across ventral temporal cortex. Several anatomical landmarks are shown here to introduce that distributed organization.", + "regions": ["fusiform", "inferior-temporal"], + "sources": ["vision"] + }, + { + "id": "overlap", + "startSeconds": 8, + "endSeconds": 12, + "title": "One region, many contributions", + "text": "The study's category information extended beyond the most strongly responding patches. A whole gyrus is not a dedicated face detector, and these highlights are not the original measured patterns.", + "regions": ["fusiform", "inferior-temporal", "middle-temporal"], + "sources": ["vision"] + }, + { + "id": "measurement", + "startSeconds": 12, + "endSeconds": 16, + "title": "Ask what was measured", + "text": "The study measured fMRI responses, not individual action potentials. Follow the evidence to distinguish anatomical context, measured signals and the numerical filter running on the silicon side.", + "regions": ["visual", "fusiform", "inferior-temporal"], + "sources": ["vision", "bold"] + } + ] + }, + "mechanisms": [ + { + "id": "cpu", + "label": "CPU", + "unit": "Arithmetic work and memory accesses", + "organization": "A scheduler gives ready work to available execution resources.", + "state": "Operands, partial results and memory contents", + "tradeoff": "More execution capacity helps only when operands and independent work are available.", + "limit": "A simplified work scheduler, not an instruction-level reproduction of a particular CPU. Model ticks are not measured chip cycles.", + "sources": ["cpu-issue"] + }, + { + "id": "gpu", + "label": "GPU", + "unit": "Groups of arithmetic lanes", + "organization": "Many threads perform related work; their memory addresses and active lanes matter.", + "state": "Per-thread values, lane masks and shared memory resources", + "tradeoff": "Regular work can fill lanes efficiently. Divergent paths and scattered accesses can reduce useful work per issued operation.", + "limit": "This teaching schedule does not reproduce every GPU, or measure the GPU displaying this page.", + "sources": ["gpu-simt"] + }, + { + "id": "human", + "label": "Human brain", + "unit": "Biological signals studied at different scales", + "organization": "Visual representations involve distributed, overlapping activity across tissue.", + "state": "The atlas provides anatomical context; this guide does not calculate a neural state.", + "tradeoff": "Different measurements reveal different aspects of the system. Regional fMRI patterns are not individual spikes.", + "limit": "No equivalent CPU clock, FLOPS score, energy per image or predicted human response is inferred.", + "sources": ["vision", "bold"] + } + ], + "evidenceSourceIds": [ + "vision", + "vision-timing", + "bold", + "atlas", + "cpu-issue", + "gpu-simt" + ], + "sources": { + "atlas": { + "title": "Allen Human Reference Atlas \u2013 3D, 2020", + "authors": "Ding and colleagues \u00b7 Allen Institute", + "year": "2020", + "method": "Anatomical reference atlas", + "species": "Human reference template", + "url": "https://download.alleninstitute.org/informatics-archive/allen_human_reference_atlas_3d_2020/version_1/README.pdf", + "finding": "The reference provides anatomical labels from which doke's teaching surfaces are derived.", + "limit": "These grouped surfaces are neither an individual's scan nor a measurement of functional activation." + }, + "vision": { + "title": "Distributed and overlapping representations of faces and objects in ventral temporal cortex", + "authors": "Haxby et al. \u00b7 Science", + "year": "2001", + "method": "Human fMRI experiment", + "species": "Human", + "url": "https://pubmed.ncbi.nlm.nih.gov/11577229/", + "finding": "Category-related response patterns were distributed and overlapping; information remained outside the maximally responding regions.", + "limit": "The atlas highlights are explanatory landmarks, not the study's voxel patterns or an image-recognition model." + }, + "vision-timing": { + "title": "Speed of processing in the human visual system", + "authors": "Thorpe, Fize & Marlot \u00b7 Nature", + "year": "1996", + "method": "Go/no-go categorization and event-related potentials", + "species": "Human", + "url": "https://pubmed.ncbi.nlm.nih.gov/8632824/", + "finding": "In an animal-categorization task with photographs displayed for 20 ms, a no-go-related ERP difference emerged around 150 ms after image onset.", + "limit": "This is a task-specific electrical measurement, not button-response time, a brain clock or the timing of this guide. It does not calibrate a brain-versus-chip race." + }, + "bold": { + "title": "Neurophysiological investigation of the basis of the fMRI signal", + "authors": "Logothetis et al. \u00b7 Nature", + "year": "2001", + "method": "Simultaneous electrophysiology and BOLD fMRI", + "species": "Nonhuman primate", + "url": "https://www.nature.com/articles/35084005", + "finding": "In the studied visual-cortex conditions, local field potentials predicted the BOLD response better than multi-unit spiking output.", + "limit": "BOLD cannot be read as a direct count of outgoing spikes. The relationship depends on the preparation and measurement." + }, + "cpu-issue": { + "title": "The Issue Unit", + "authors": "RISCV-BOOM documentation", + "year": "Reference", + "method": "Open processor architecture documentation", + "species": "Hardware", + "url": "https://docs.boom-core.org/en/latest/sections/issue-units.html", + "finding": "BOOM's issue logic considers operand readiness and available execution resources.", + "limit": "This supports the scheduling principle. Doke's generic work scheduler is not a BOOM emulator or a timing model for commercial CPUs." + }, + "gpu-simt": { + "title": "CUDA C++ Programming Guide \u00b7 SIMT Architecture", + "authors": "NVIDIA", + "year": "2023 \u00b7 CUDA 12.2", + "method": "Published GPU programming and architecture documentation", + "species": "Hardware", + "url": "https://docs.nvidia.com/cuda/archive/12.2.0/cuda-c-programming-guide/index.html#simt-architecture", + "finding": "CUDA groups threads into warps; execution paths and memory-access organization affect how work proceeds.", + "limit": "This is documentation for a particular GPU programming family. Doke uses a reduced teaching schedule, not a device-specific performance prediction." + } + } +} diff --git a/public/content.json b/public/content.json deleted file mode 100644 index 2de828f..0000000 --- a/public/content.json +++ /dev/null @@ -1,748 +0,0 @@ -{ - "version": "1.1.0", - "reviewDate": "2026-09-06", - "review": "Editorial source review; not independent expert validation. Activity timing and intensity are authored.", - "sources": { - "allen": { - "title": "Allen Human Reference Atlas \u2013 3D, 2020", - "authors": "Ding and colleagues \u00b7 Allen Institute", - "year": "2020", - "method": "Anatomical atlas", - "species": "Human reference template", - "url": "https://download.alleninstitute.org/informatics-archive/allen_human_reference_atlas_3d_2020/version_1/README.pdf", - "finding": "An anatomical parcellation of the ICBM 2009b symmetric reference. The rendered meshes are derived from labeled voxels.", - "limit": "A symmetric reference atlas, not a scan of you. Teaching groups combine atlas labels; they are not functional parcels." - }, - "music": { - "title": "Anatomically distinct dopamine release during anticipation and experience of peak emotion to music", - "authors": "Salimpoor et al. \u00b7 Nature Neuroscience", - "year": "2011", - "method": "PET + fMRI", - "species": "Human", - "url": "https://pubmed.ncbi.nlm.nih.gov/21217764/", - "finding": "The study reported striatal dopamine release during intensely pleasurable music, with caudate involvement during anticipation and nucleus accumbens involvement during peak responses.", - "limit": "Selected listeners and stimuli. PET release inference and complementary fMRI responses do not provide a universal second-by-second dopamine concentration." - }, - "reward": { - "title": "A Neural Substrate of Prediction and Reward", - "authors": "Schultz, Dayan & Montague \u00b7 Science", - "year": "1997", - "method": "Physiology + computational theory", - "species": "Nonhuman primate", - "url": "https://www.gatsby.ucl.ac.uk/~dayan/papers/sdm97.html", - "finding": "Dopaminergic responses can reflect differences between predicted and obtained reward. Learned predictions change responses to cues and outcomes.", - "limit": "A simplified teaching account of a subset of dopaminergic responses. The human atlas localization and animation are schematic." - }, - "vision": { - "title": "Distributed and overlapping representations of faces and objects in ventral temporal cortex", - "authors": "Haxby et al. \u00b7 Science", - "year": "2001", - "method": "fMRI", - "species": "Human", - "url": "https://pubmed.ncbi.nlm.nih.gov/11577229/", - "finding": "Faces and objects elicited discriminable, distributed patterns in ventral temporal cortex. Information was not restricted to the maximally responding patch.", - "limit": "Broad atlas gyri stand in for the study\u2019s patterns. Display intensity and stage timing are authored; they are not the original fMRI values." - }, - "motor": { - "title": "Function in the Human Connectome: Task-fMRI and Individual Differences in Behavior", - "authors": "Barch et al. \u00b7 NeuroImage", - "year": "2013", - "method": "Task-fMRI", - "species": "Human", - "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC4011498/", - "finding": "The HCP task battery includes movement conditions to examine functional activation. A controlled task and its contrast define what an activation map means.", - "limit": "The lesson highlights broad anatomical participants; it does not replay an HCP dataset or resolve finger somatotopy." - }, - "brain": { - "title": "Physiology, Brain", - "authors": "NCBI Bookshelf \u00b7 StatPearls", - "year": "Reference", - "method": "Anatomy and physiology reference", - "species": "Human", - "url": "https://www.ncbi.nlm.nih.gov/books/NBK551718/", - "finding": "Brain functions arise through interacting cortical, subcortical, cerebellar and brainstem systems.", - "limit": "Short region descriptions are introductory summaries, not exhaustive or exclusive functional assignments." - }, - "basal": { - "title": "Neuroanatomy, Basal Ganglia", - "authors": "NCBI Bookshelf \u00b7 StatPearls", - "year": "Reference", - "method": "Anatomy reference", - "species": "Human", - "url": "https://www.ncbi.nlm.nih.gov/books/NBK537141/", - "finding": "Basal ganglia participate in circuits related to action, learning and cognition, through interactions with cortex and thalamus.", - "limit": "Individual pathways contain cell-type and receptor-specific effects that a regional atlas does not resolve." - }, - "synthesis": { - "title": "Neurotransmitter Synthesis", - "authors": "Purves et al. \u00b7 Neuroscience", - "year": "2001", - "method": "Mechanism reference", - "species": "General neurobiology", - "url": "https://www.ncbi.nlm.nih.gov/books/NBK11110/", - "finding": "Synthesis, vesicle packaging, release and clearance are distinct processes. Small molecules and peptides have different synthesis and transport pathways.", - "limit": "The synapse is a representative diagram with expanded space and slowed teaching time, not a molecular recording." - }, - "receptors": { - "title": "Two Families of Postsynaptic Receptors", - "authors": "Purves et al. \u00b7 Neuroscience", - "year": "2001", - "method": "Mechanism reference", - "species": "General neurobiology", - "url": "https://www.ncbi.nlm.nih.gov/books/NBK10855/", - "finding": "Ionotropic receptors directly gate ion channels. Metabotropic receptors act through intracellular signaling pathways.", - "limit": "A transmitter\u2019s effect depends on receptor type and cellular context, not its name alone." - }, - "glutamate": { - "title": "Glutamatergic Signaling in the Central Nervous System", - "authors": "Reiner & Levitz \u00b7 Neuron", - "year": "2018", - "method": "Mechanistic review", - "species": "Across experimental systems", - "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC6484838/", - "finding": "Glutamate signals through both ionotropic and metabotropic receptor families, with different downstream effects.", - "limit": "The AMPA-like teaching diagram represents one mechanism; it does not represent all glutamate receptors." - }, - "catecholamines": { - "title": "Physiology, Catecholamines", - "authors": "NCBI Bookshelf \u00b7 StatPearls", - "year": "Reference", - "method": "Biochemical reference", - "species": "General physiology", - "url": "https://www.ncbi.nlm.nih.gov/books/NBK507716/", - "finding": "Dopamine synthesis proceeds from tyrosine through L-DOPA. Dopamine is also a precursor in the catecholamine pathway.", - "limit": "Precursor availability or an activity choice does not determine a visitor\u2019s dopamine level." - }, - "neurotransmitters": { - "title": "Neurotransmitters", - "authors": "Purves et al. \u00b7 Neuroscience", - "year": "2001", - "method": "Teaching reference", - "species": "General neurobiology", - "url": "https://www.ncbi.nlm.nih.gov/books/NBK10795/", - "finding": "Neurotransmitters act through diverse receptors; neurons can use more than one signaling molecule.", - "limit": "Excitation and inhibition depend on receptor and cellular conditions. No single chemical corresponds to one feeling." - }, - "bold": { - "title": "Neurophysiological investigation of the basis of the fMRI signal", - "authors": "Logothetis et al. \u00b7 Nature", - "year": "2001", - "method": "Electrophysiology + fMRI", - "species": "Nonhuman primate", - "url": "https://www.nature.com/articles/35084005", - "finding": "BOLD is an indirect neurovascular signal; in the studied visual cortex conditions it related more strongly to local field potentials than to multi-unit output.", - "limit": "fMRI brightness cannot be converted directly into spike counts or chemical release." - }, - "wilson": { - "title": "Excitatory and inhibitory interactions in localized populations of model neurons", - "authors": "Wilson & Cowan \u00b7 Biophysical Journal", - "year": "1972", - "method": "Mathematical model", - "species": "Abstract neural populations", - "url": "https://pubmed.ncbi.nlm.nih.gov/4332108/", - "finding": "Coupled excitatory and inhibitory populations can produce nonlinear temporal behavior.", - "limit": "Our simplified rate model uses pedagogical parameters, not parameters fitted to a person or a particular brain region." - }, - "yeo": { - "title": "The organization of the human cerebral cortex estimated by intrinsic functional connectivity", - "authors": "Yeo et al. \u00b7 Journal of Neurophysiology", - "year": "2011", - "method": "Resting-state functional connectivity", - "species": "Human", - "url": "https://surfer.nmr.mgh.harvard.edu/pub/articles/2011/2011_-_Yeo_et_al._-_Journal_of_neurophysiology.pdf", - "finding": "Large-scale cortical networks can be estimated from patterns of functional connectivity.", - "limit": "Network membership does not imply a unique mental function or a directed anatomical connection." - }, - "release": { - "title": "Calcium Control of Neurotransmitter Release", - "authors": "S\u00fcdhof \u00b7 Cold Spring Harbor Perspectives in Biology", - "year": "2012", - "method": "Mechanistic review", - "species": "Across experimental systems", - "url": "https://pubmed.ncbi.nlm.nih.gov/22453967/", - "finding": "Calcium entry couples electrical activity to vesicular release through calcium sensing and membrane fusion machinery.", - "limit": "Geometry, particle counts, diffusion paths and timing in this teaching view are authored, not molecular dynamics or measured kinetics." - }, - "neuron-morphology": { - "title": "Pyramidal neurons: dendritic structure and synaptic integration", - "authors": "Spruston \u00b7 Nature Reviews Neuroscience", - "year": "2008", - "method": "Review of neuronal structure and integration", - "species": "Across experimental systems", - "url": "https://pubmed.ncbi.nlm.nih.gov/18270515/", - "finding": "Pyramidal neurons have distinct apical and basal dendritic trees; dendritic structure participates in synaptic integration.", - "limit": "The enlarged neural tissue window is procedural teaching geometry. Cell positions, density, contacts and flashes are not reconstructed from the atlas or calibrated to recordings." - }, - "structure-dopamine": { - "title": "Dopamine \u2014 PubChem compound record", - "authors": "PubChem \u00b7 NCBI", - "year": "Accessed 2026-09-06", - "method": "Computed 3D molecular conformer", - "species": "Chemical structure", - "url": "https://pubchem.ncbi.nlm.nih.gov/compound/681", - "finding": "Atom identities, bond orders and one 3D conformer provide the displayed ball-and-stick structure.", - "limit": "One structure, not molecular dynamics. Hydrogen and protonation states follow the source record; L-glutamic acid is the conjugate acid of physiological glutamate. Display radii are illustrative." - }, - "structure-glutamate": { - "title": "L-glutamic acid \u2014 PubChem compound record", - "authors": "PubChem \u00b7 NCBI", - "year": "Accessed 2026-09-06", - "method": "Computed 3D molecular conformer", - "species": "Chemical structure", - "url": "https://pubchem.ncbi.nlm.nih.gov/compound/33032", - "finding": "Atom identities, bond orders and one 3D conformer provide the displayed ball-and-stick structure.", - "limit": "One structure, not molecular dynamics. Hydrogen and protonation states follow the source record; L-glutamic acid is the conjugate acid of physiological glutamate. Display radii are illustrative." - }, - "structure-gaba": { - "title": "GABA \u2014 PubChem compound record", - "authors": "PubChem \u00b7 NCBI", - "year": "Accessed 2026-09-06", - "method": "Computed 3D molecular conformer", - "species": "Chemical structure", - "url": "https://pubchem.ncbi.nlm.nih.gov/compound/119", - "finding": "Atom identities, bond orders and one 3D conformer provide the displayed ball-and-stick structure.", - "limit": "One structure, not molecular dynamics. Hydrogen and protonation states follow the source record; L-glutamic acid is the conjugate acid of physiological glutamate. Display radii are illustrative." - } - }, - "regions": { - "motor": { - "summary": "On the frontal side of the central sulcus, the precentral gyrus includes primary motor cortex. It participates in control of voluntary movement.", - "sources": [ - "allen", - "brain" - ] - }, - "prefrontal": { - "summary": "These frontal gyri participate in planning, working memory and cognitive control. Their contributions depend on the task and their interactions with other regions.", - "sources": [ - "allen", - "brain" - ] - }, - "inferior-frontal": { - "summary": "Inferior frontal regions participate in language and control processes. A broad anatomical gyrus is not identical to a functional language area.", - "sources": [ - "allen", - "brain" - ] - }, - "orbitofrontal": { - "summary": "Orbital frontal regions participate in evaluating outcomes and guiding decisions in context. Valuation is distributed across several interacting structures.", - "sources": [ - "allen", - "brain" - ] - }, - "somatosensory": { - "summary": "The postcentral gyrus includes primary somatosensory cortex, which processes information about the body. The atlas does not resolve a particular finger representation.", - "sources": [ - "allen", - "brain" - ] - }, - "parietal": { - "summary": "Superior parietal regions participate in spatial processing and coordination of sensory information with action.", - "sources": [ - "allen", - "brain" - ] - }, - "inferior-parietal": { - "summary": "The angular and supramarginal gyri sit at an association-cortex crossroads. Their roles include integration across sensory and cognitive domains.", - "sources": [ - "allen", - "brain" - ] - }, - "precuneus": { - "summary": "A medial parietal region involved in several internally directed and spatial processes. It participates in distributed networks rather than a single isolated function.", - "sources": [ - "allen", - "yeo" - ] - }, - "superior-temporal": { - "summary": "Superior temporal regions participate in auditory processing. This teaching group also includes adjacent temporal landmarks named in the atlas.", - "sources": [ - "allen", - "brain" - ] - }, - "middle-temporal": { - "summary": "A temporal association region. The atlas defines its anatomical boundary; functional patterns can span it and neighboring cortex.", - "sources": [ - "allen" - ] - }, - "inferior-temporal": { - "summary": "Part of the broader ventral visual system involved in object representations. Information is distributed across overlapping response patterns.", - "sources": [ - "allen", - "vision" - ] - }, - "fusiform": { - "summary": "The fusiform gyrus lies on the ventral temporal-occipital surface. It participates in visual recognition; the entire gyrus is not a dedicated \u201cface center.\u201d", - "sources": [ - "allen", - "vision" - ] - }, - "auditory": { - "summary": "Heschl\u2019s gyrus is an important auditory landmark on the superior temporal plane. It becomes easier to inspect when the surrounding cortex is hidden.", - "sources": [ - "allen", - "brain" - ] - }, - "visual": { - "summary": "Occipital structures participate in visual processing. This grouped mesh contains several anatomical labels and does not delineate every visual area.", - "sources": [ - "allen", - "brain" - ] - }, - "insula": { - "summary": "The insula lies within the lateral sulcus, beneath surrounding cortex. Its broad roles include integration of bodily and sensory information.", - "sources": [ - "allen", - "brain" - ] - }, - "anterior-cingulate": { - "summary": "Anterior cingulate regions participate in control, motivation and affect-related processes. Activity here does not establish one specific emotion.", - "sources": [ - "allen", - "brain" - ] - }, - "posterior-cingulate": { - "summary": "Posterior medial cortex participates in intrinsic functional networks. Its involvement changes with task and context.", - "sources": [ - "allen", - "yeo" - ] - }, - "parahippocampal": { - "summary": "These medial temporal gyri neighbor the hippocampus. This is an anatomical grouping, not a claim of a single memory pathway.", - "sources": [ - "allen" - ] - }, - "hippocampus": { - "summary": "The hippocampus participates in forming and organizing memories, in interaction with a wider medial temporal and cortical system.", - "sources": [ - "allen", - "brain" - ] - }, - "amygdala": { - "summary": "An anatomically complex set of nuclei in the medial temporal lobe. It participates in learning and the relevance of stimuli, beyond any one emotion.", - "sources": [ - "allen", - "brain" - ] - }, - "caudate": { - "summary": "A striatal structure with head, body and tail. In the music study, caudate responses were associated with anticipation of peak emotion.", - "sources": [ - "allen", - "music" - ] - }, - "putamen": { - "summary": "A striatal structure involved in basal ganglia circuits, including those concerned with action.", - "sources": [ - "allen", - "basal" - ] - }, - "accumbens": { - "summary": "Part of the ventral striatum. The music study associated this region with peak pleasurable responses; this does not make it a universal pleasure meter.", - "sources": [ - "allen", - "music" - ] - }, - "pallidum": { - "summary": "The internal and external pallidal segments participate in basal ganglia circuits. This teaching mesh groups both segments.", - "sources": [ - "allen", - "basal" - ] - }, - "thalamus": { - "summary": "A collection of nuclei that relay and regulate information in interactions with cortex and other structures. This mesh groups multiple atlas nuclei.", - "sources": [ - "allen", - "brain" - ] - }, - "hypothalamus": { - "summary": "A small, diverse region involved in bodily regulation and neuroendocrine functions. Its nuclei have distinct roles that are not resolved by a single global activity value.", - "sources": [ - "allen", - "brain" - ] - }, - "basal-forebrain": { - "summary": "An anatomical teaching group combining basal forebrain, septal and neighboring atlas labels. Individual cell populations and projections have different signaling roles.", - "sources": [ - "allen" - ] - }, - "substantia-nigra": { - "summary": "A midbrain structure participating in basal ganglia circuits. Dopaminergic neurons are associated with its pars compacta; the atlas mesh does not separate all cell types.", - "sources": [ - "allen", - "basal" - ] - }, - "midbrain": { - "summary": "This teaching group combines midbrain tegmental and orienting structures. It is not a cell-specific dopamine source map.", - "sources": [ - "allen" - ] - }, - "cerebellum": { - "summary": "The cerebellum contributes to coordination and learning of movement, alongside broader cognitive roles. It interacts with systems outside the cerebellum.", - "sources": [ - "allen", - "brain" - ] - }, - "pons": { - "summary": "The pons is part of the brainstem. This mesh combines atlas subdivisions and a connecting cerebellar peduncle for anatomical context.", - "sources": [ - "allen" - ] - }, - "medulla": { - "summary": "The medulla contains systems involved in autonomic regulation and pathways between brain and body. The displayed group includes adjacent labeled hindbrain tissue.", - "sources": [ - "allen", - "brain" - ] - }, - "callosum": { - "summary": "The corpus callosum is a major white matter connection between the cerebral hemispheres. A bundle is not a single axon or a single signal.", - "sources": [ - "allen", - "brain" - ] - }, - "white-matter": { - "summary": "White matter contains axonal pathways. Its dense internal atlas mesh is omitted from rendering to keep deeper structures visible.", - "sources": [ - "allen" - ] - }, - "olfactory": { - "summary": "This group contains the olfactory bulb, anterior olfactory nucleus and nearby olfactory cortical labels. Its boundaries come from the atlas ontology.", - "sources": [ - "allen" - ] - }, - "subthalamic": { - "summary": "A small nucleus participating in basal ganglia circuitry. Its role depends on its connections and activity patterns.", - "sources": [ - "allen", - "basal" - ] - }, - "other-deep": { - "summary": "Context group: claustrum, habenular nuclei, pineal body and zona incerta. They are separate anatomical structures with different functions, grouped only for this overview.", - "sources": [ - "allen" - ] - } - }, - "activities": [ - { - "id": "music", - "number": "01", - "title": "When music moves you", - "short": "Music & anticipation", - "tag": "REWARD \u00b7 DOPAMINE", - "intro": "The moment before your favorite part. Then the part itself. Explore two phases of a studied musical experience.", - "duration": 60, - "chemical": "dopamine", - "sources": [ - "music", - "synthesis", - "bold" - ], - "kind": "Human PET + fMRI", - "chapters": [ - { - "title": "Set the scene", - "text": "The study used emotionally powerful music selected for the listeners. This story illustrates the findings; it does not measure your response to a song.", - "regions": [ - "caudate", - "accumbens" - ], - "source": "music" - }, - { - "title": "Anticipation", - "text": "Bring the caudate into view. Anticipation and peak emotion were associated with different striatal responses in this study.", - "regions": [ - "caudate" - ], - "source": "music" - }, - { - "title": "The peak", - "text": "Now locate the nucleus accumbens. This chapter illustrates its association with the experience of peak emotion.", - "regions": [ - "accumbens" - ], - "source": "music" - }, - { - "title": "A chemical signal", - "text": "Dopamine release, its synthesis, and receptor action are different processes. Open the synapse lesson to examine signaling at a smaller scale.", - "regions": [ - "caudate", - "accumbens" - ], - "source": "synthesis" - } - ] - }, - { - "id": "reward", - "number": "02", - "title": "The surprise in a reward", - "short": "Reward & prediction", - "tag": "LEARNING \u00b7 DOPAMINE", - "intro": "An expected outcome and a surprising one are not the same event for a learning system.", - "duration": 48, - "chemical": "dopamine", - "sources": [ - "reward", - "basal" - ], - "kind": "Primate physiology + teaching model", - "chapters": [ - { - "title": "An unexpected outcome", - "text": "A reward can produce a response when it was not predicted. The highlighted human structures provide schematic anatomical context.", - "regions": [ - "substantia-nigra", - "caudate", - "accumbens" - ], - "source": "reward" - }, - { - "title": "Learn the cue", - "text": "With learning, a reward-predicting cue can acquire a response. This is an explanatory sequence, not a fitted learning trajectory.", - "regions": [ - "caudate", - "accumbens" - ], - "source": "reward" - }, - { - "title": "Expected or omitted?", - "text": "Compare a predicted outcome with its omission. A negative prediction error is different from \u201cno dopamine anywhere.\u201d", - "regions": [ - "substantia-nigra", - "caudate" - ], - "source": "reward" - }, - { - "title": "A learning signal", - "text": "The idea is a difference between prediction and outcome. It does not mean that every dopaminergic neuron carries the same signal.", - "regions": [ - "caudate", - "accumbens" - ], - "source": "reward" - } - ] - }, - { - "id": "vision", - "number": "03", - "title": "More than meets the eye", - "short": "Faces & objects", - "tag": "PERCEPTION \u00b7 DISTRIBUTED PATTERNS", - "intro": "Recognition is a pattern across a system. Look beyond a single bright spot.", - "duration": 48, - "chemical": "glutamate", - "sources": [ - "vision", - "bold" - ], - "kind": "Human fMRI", - "chapters": [ - { - "title": "An anatomical starting point", - "text": "Find occipital cortex and the ventral temporal surface. These are broad atlas landmarks for the visual system.", - "regions": [ - "visual" - ], - "source": "brain" - }, - { - "title": "A distributed pattern", - "text": "Ventral temporal response patterns can distinguish faces and objects. Several regions are highlighted to illustrate distribution.", - "regions": [ - "fusiform", - "inferior-temporal" - ], - "source": "vision" - }, - { - "title": "Overlapping representations", - "text": "Switch face and object conditions. The illustrative patterns overlap; the entire fusiform gyrus is not a dedicated face detector.", - "regions": [ - "fusiform", - "inferior-temporal", - "middle-temporal" - ], - "source": "vision" - }, - { - "title": "What was measured?", - "text": "The study measured fMRI patterns. This does not specify individual spikes or task-evoked glutamate quantities.", - "regions": [ - "fusiform", - "inferior-temporal" - ], - "source": "bold" - } - ] - }, - { - "id": "movement", - "number": "04", - "title": "A small move. A whole system.", - "short": "Move your fingers", - "tag": "ACTION \u00b7 SENSORIMOTOR SYSTEMS", - "intro": "A simple movement offers a window onto coordinated brain function.", - "duration": 48, - "chemical": "gaba", - "sources": [ - "motor", - "brain", - "basal" - ], - "kind": "Task-fMRI + anatomy", - "chapters": [ - { - "title": "A defined task", - "text": "Choose a hand for the teaching scenario. The atlas cannot resolve the exact neurons responsible for your movement.", - "regions": [ - "motor" - ], - "source": "motor" - }, - { - "title": "Motor cortex", - "text": "Inspect the precentral gyrus. Contralateral emphasis is an introductory simplification; both hemispheres can participate.", - "regions": [ - "motor" - ], - "source": "brain" - }, - { - "title": "A wider system", - "text": "Cerebellar and basal ganglia systems contribute to movement. These paths are schematic relationships, not reconstructed axon trajectories.", - "regions": [ - "motor", - "putamen", - "cerebellum" - ], - "source": "basal" - }, - { - "title": "Sensation and action", - "text": "Sensory information participates in ongoing movement. Region brightness represents authored emphasis, not a measured firing rate.", - "regions": [ - "motor", - "somatosensory", - "cerebellum" - ], - "source": "brain" - } - ] - } - ], - "chemicals": [ - { - "id": "dopamine", - "name": "Dopamine", - "symbol": "DA", - "className": "Catecholamine \u00b7 neuromodulator", - "tagline": "A signal shaped by context.", - "summary": "Dopamine participates in learning, movement and other functions. Its effect depends on the pathway, receptor and state of the receiving cells.", - "synthesis": "Tyrosine \u2192 L-DOPA \u2192 dopamine. Synthesis is a biochemical process inside appropriate cells; a chosen activity does not specify its rate.", - "release": "Release occurs from dopaminergic terminals. The music lesson links to a study-specific inference about striatal release.", - "receptors": "Dopamine receptors are metabotropic: they influence intracellular signaling. This is distinct from a transmitter directly opening an ion channel.", - "clearance": "Transporters and metabolism help regulate the signal. The animation does not model calibrated dopamine kinetics.", - "regions": [ - "substantia-nigra", - "caudate", - "accumbens" - ], - "sources": [ - "catecholamines", - "receptors", - "music", - "reward", - "release", - "structure-dopamine" - ], - "activities": [ - "music", - "reward" - ] - }, - { - "id": "glutamate", - "name": "Glutamate", - "symbol": "Glu", - "className": "Amino acid \u00b7 neurotransmitter", - "tagline": "Fast signals. Many possibilities.", - "summary": "A major excitatory transmitter in the adult brain, acting through several ionotropic and metabotropic receptor types.", - "synthesis": "Neurons can form glutamate from metabolic precursors. Recycling between neurons and glia helps sustain signaling.", - "release": "Vesicles release glutamate at appropriate terminals. The lesson illustrates a representative chemical synapse.", - "receptors": "AMPA and NMDA are ionotropic receptor families; metabotropic glutamate receptors signal through G proteins. The synapse lesson uses an AMPA-like example.", - "clearance": "Transporters in neurons and glia remove glutamate from extracellular space. This is distinct from a synapse simply \u201cturning off.\u201d", - "regions": [], - "sources": [ - "glutamate", - "synthesis", - "receptors", - "release", - "structure-glutamate" - ], - "activities": [] - }, - { - "id": "gaba", - "name": "GABA", - "symbol": "GABA", - "className": "Amino acid \u00b7 neurotransmitter", - "tagline": "Inhibition is active work.", - "summary": "A major inhibitory transmitter in the mature brain. Effects depend on receptor type and ionic conditions, so inhibition is not a universal property in every context.", - "synthesis": "GABA can be synthesized from glutamate by glutamic acid decarboxylase in GABAergic neurons.", - "release": "GABAergic terminals release GABA. A whole-region highlight cannot identify the activity of all inhibitory cells.", - "receptors": "GABA-A receptors are ionotropic; GABA-B receptors are metabotropic. The neural circuit illustrates inhibitory population interactions, not receptor-specific kinetics.", - "clearance": "Transporters contribute to GABA uptake. Signaling and recycling occur alongside ongoing network activity.", - "regions": [], - "sources": [ - "neurotransmitters", - "synthesis", - "receptors", - "release", - "structure-gaba" - ], - "activities": [] - } - ] -} \ No newline at end of file diff --git a/public/display.js b/public/display.js index 600e302..46447cb 100644 --- a/public/display.js +++ b/public/display.js @@ -74,14 +74,14 @@ export class HDRText { c.fillStyle = "white"; c.textBaseline = "alphabetic"; for (const el of document.querySelectorAll( - ".brand,#details h2,.micro-caption strong", + ".brand,[data-hdr],#inspection h2", )) { if ( !el.checkVisibility({ opacityProperty: true, visibilityProperty: true }) ) continue; c.save(); - const panel = el.closest("#details"); + const panel = el.closest("#inspection"); if (panel) { const bounds = panel.getBoundingClientRect(); c.beginPath(); diff --git a/public/hardware.json b/public/hardware.json new file mode 100644 index 0000000..379bdb3 --- /dev/null +++ b/public/hardware.json @@ -0,0 +1,479 @@ +{ + "schema_version": 1, + "checked_on": "2026-10-15", + "scope": "Publicly documented reference hardware. Product facts do not parameterize the educational simulation.", + "profiles": [ + { + "id": "ryzen-9950x", + "family": "cpu", + "name": "AMD Ryzen 9 9950X", + "facts": [ + { + "label": "Architecture", + "value": "Zen 5", + "source": "amd-9950x", + "locator": "General Specifications → Processor Architecture" + }, + { + "label": "CPU cores / threads", + "value": "16 / 32", + "source": "amd-9950x", + "locator": "General Specifications → # of CPU Cores; # of Threads" + }, + { + "label": "L2 / L3 cache", + "value": "16 MB / 64 MB", + "source": "amd-9950x", + "locator": "General Specifications → L2 Cache; L3 Cache" + }, + { + "label": "Base / maximum boost", + "value": "4.3 GHz / up to 5.7 GHz", + "source": "amd-9950x", + "locator": "General Specifications → Base Clock; Max. Boost Clock" + }, + { + "label": "Package dies", + "value": "3", + "source": "amd-9950x", + "locator": "General Specifications → Package Die Count" + }, + { + "label": "Instruction set", + "value": "x86-64", + "source": "amd-9950x", + "locator": "General Specifications → Instruction Set" + } + ], + "sources": [ + { + "id": "amd-9950x", + "title": "AMD Ryzen 9 9950X — specifications", + "url": "https://www.amd.com/en/products/processors/desktops/ryzen/9000-series/amd-ryzen-9-9950x.html", + "checked": "2026-10-15" + } + ], + "limits": [ + "These are published product facts. The small CPU model uses authored timing and cache rules, not Zen 5 microarchitecture.", + "Maximum boost is a conditional peak, not a clock promised for every core or workload.", + "Schematic block positions are explanatory; no physical die layout or measured workload trace is reproduced." + ], + "timing": { + "kind": "clock", + "hz": 4300000000, + "label": "4.3 GHz base clock", + "sourceIds": [ + "amd-9950x" + ], + "scope": "Published base frequency. Actual core frequency varies with operating conditions. Its reciprocal is a reference clock period, not instruction latency or model-tick duration." + } + }, + { + "id": "rtx-5090", + "family": "gpu", + "name": "NVIDIA GeForce RTX 5090", + "facts": [ + { + "label": "Architecture", + "value": "Blackwell", + "source": "nvidia-5090", + "locator": "Full Specs → Technology Support → NVIDIA Architecture" + }, + { + "label": "CUDA cores", + "value": "21,760", + "source": "nvidia-5090", + "locator": "Full Specs → GPU Engine Specs → NVIDIA CUDA Cores" + }, + { + "label": "Tensor Core generation", + "value": "5th generation", + "source": "nvidia-5090", + "locator": "Full Specs → GPU Engine Specs → Tensor Cores (AI)" + }, + { + "label": "Memory", + "value": "32 GB GDDR7", + "source": "nvidia-5090", + "locator": "Full Specs → Memory Specs → Standard Memory Config" + }, + { + "label": "Memory interface width", + "value": "512-bit", + "source": "nvidia-5090", + "locator": "Full Specs → Memory Specs → Memory Interface Width" + }, + { + "label": "Base / boost clock", + "value": "2.01 GHz / 2.41 GHz", + "source": "nvidia-5090", + "locator": "Full Specs → GPU Engine Specs → Base Clock; Boost Clock" + } + ], + "sources": [ + { + "id": "nvidia-5090", + "title": "NVIDIA GeForce RTX 5090 — full specifications", + "url": "https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/", + "checked": "2026-10-15" + } + ], + "limits": [ + "The listed specifications describe NVIDIA reference/Founders Edition hardware; partner designs can differ.", + "The parallel-lane model is an educational mechanism, not an RTX 5090 performance model.", + "CUDA core counts are not comparable one-for-one with CPU cores, Tensor Cores or biological neurons." + ], + "timing": { + "kind": "clock", + "hz": 2010000000, + "label": "2.01 GHz base clock", + "sourceIds": [ + "nvidia-5090" + ], + "scope": "NVIDIA reference base frequency. Operating frequency varies; one cycle is not one completed CUDA instruction or one model tick." + } + }, + { + "id": "h100-sxm", + "family": "gpu", + "name": "NVIDIA H100 SXM5", + "facts": [ + { + "label": "Architecture", + "value": "Hopper", + "source": "nvidia-hopper", + "locator": "H100 GPU architecture" + }, + { + "label": "Streaming multiprocessors", + "value": "132", + "source": "nvidia-hopper", + "locator": "H100 GPU architecture → H100 with SXM5 board form-factor" + }, + { + "label": "FP32 CUDA cores", + "value": "128 per SM; 16,896 per GPU", + "source": "nvidia-hopper", + "locator": "H100 GPU architecture → H100 with SXM5 board form-factor" + }, + { + "label": "Tensor Cores", + "value": "4 fourth-generation units per SM; 528 per GPU", + "source": "nvidia-hopper", + "locator": "H100 GPU architecture → H100 with SXM5 board form-factor" + }, + { + "label": "Memory", + "value": "80 GB HBM3", + "source": "nvidia-hopper", + "locator": "H100 GPU architecture → H100 with SXM5 board form-factor" + }, + { + "label": "L2 cache", + "value": "50 MB", + "source": "nvidia-hopper", + "locator": "H100 GPU architecture → H100 with SXM5 board form-factor" + } + ], + "sources": [ + { + "id": "nvidia-hopper", + "title": "NVIDIA Hopper Architecture In-Depth", + "url": "https://developer.nvidia.com/blog/nvidia-hopper-architecture-in-depth/", + "checked": "2026-10-14" + }, + { + "id": "nvidia-hgx-specifications", + "title": "NVIDIA HGX reference architecture: GPU specifications", + "url": "https://docs.nvidia.com/enterprise-reference-architectures/hgx-ai-factory-h100-h200-b200/latest/components.html", + "checked": "2026-10-15" + } + ], + "limits": [ + "This profile is H100 SXM5, not H100 PCIe or the full 144-SM GH100 design illustrated in the source figure.", + "The generic model does not reproduce Hopper scheduling, instruction timing or actual compiler placement.", + "No peak-throughput figure is inferred from core counts or the source article’s preliminary performance table." + ], + "timing": { + "kind": "bandwidth", + "bytesPerSecond": 3350000000000, + "label": "3.35 TB/s memory bandwidth", + "sourceIds": [ + "nvidia-hgx-specifications" + ], + "scope": "Published H100 SXM memory bandwidth, using decimal TB/s. This is aggregate data movement capacity, not a clock frequency, memory latency or measured application throughput." + } + }, + { + "id": "tpu-v1", + "family": "npu", + "name": "Google TPU v1", + "facts": [ + { + "label": "Generation / use", + "value": "First-generation TPU · neural-network inference", + "source": "google-tpu-v1-paper", + "locator": "Abstract; section 2" + }, + { + "label": "Matrix array", + "value": "256 × 256 · 65,536 8-bit MACs", + "source": "google-tpu-v1-paper", + "locator": "Page 3, Figure 1 and matrix-unit description" + }, + { + "label": "Unified buffer / accumulators", + "value": "24 MiB / 4 MiB", + "source": "google-tpu-v1-paper", + "locator": "Page 3, unified buffer and accumulator descriptions" + }, + { + "label": "External weight memory", + "value": "8 GiB DDR3", + "source": "google-tpu-v1-paper", + "locator": "Pages 3 and 5; Table 2" + }, + { + "label": "Clock", + "value": "700 MHz", + "source": "google-tpu-v1-paper", + "locator": "Page 5, Table 2" + }, + { + "label": "Host interface", + "value": "PCIe Gen3 ×16", + "source": "google-tpu-v1-paper", + "locator": "Page 3, TPU instructions and interface" + } + ], + "sources": [ + { + "id": "google-tpu-v1-paper", + "title": "Jouppi et al.: In-Datacenter Performance Analysis of a Tensor Processing Unit", + "url": "https://arxiv.org/pdf/1704.04760", + "checked": "2026-10-15" + }, + { + "id": "google-tpu-v1-explained", + "title": "Google: An in-depth look at the first TPU", + "url": "https://cloud.google.com/blog/products/ai-machine-learning/an-in-depth-look-at-googles-first-tensor-processing-unit-tpu?hl=en", + "checked": "2026-10-15" + } + ], + "limits": [ + "A historical, well-documented inference accelerator; not the latest TPU generation.", + "The visible array is representative, not a physical die reproduction or a drawing of every MAC.", + "TPU v1 stores weights in its matrix array. Doke’s separate 4 × 4 output-stationary model moves both operands and stores each output sum at a cell.", + "Model ticks, integer operands and memory rules are educational assumptions, not TPU instruction or performance emulation." + ], + "timing": { + "kind": "clock", + "hz": 700000000, + "label": "700 MHz documented clock", + "sourceIds": [ + "google-tpu-v1-paper", + "google-tpu-v1-explained" + ], + "scope": "The 2017 paper’s first-generation inference TPU. Its clock period is a hardware reference; doke’s 4 × 4 output-stationary model is not this 256 × 256 weight-stationary implementation and its ticks are not TPU cycles." + } + }, + { + "id": "tpu-v4", + "family": "npu", + "name": "Google TPU v4", + "facts": [ + { + "label": "Accelerator type", + "value": "Tensor processor for machine learning", + "source": "google-tpu-architecture", + "locator": "How a TPU works" + }, + { + "label": "TensorCores per chip", + "value": "2", + "source": "google-tpu-v4", + "locator": "System architecture → opening paragraph" + }, + { + "label": "Matrix units per TensorCore", + "value": "4 MXUs, plus vector and scalar units", + "source": "google-tpu-v4", + "locator": "System architecture → opening paragraph" + }, + { + "label": "HBM2 capacity", + "value": "32 GiB per chip", + "source": "google-tpu-v4", + "locator": "System architecture → specifications table → HBM2 capacity and bandwidth" + }, + { + "label": "HBM2 bandwidth", + "value": "1,200 GB/s", + "source": "google-tpu-v4", + "locator": "System architecture → specifications table → HBM2 capacity and bandwidth" + }, + { + "label": "Maximum pod size", + "value": "4,096 chips", + "source": "google-tpu-v4", + "locator": "System architecture → specifications table → TPU Pod size" + } + ], + "sources": [ + { + "id": "google-tpu-v4", + "title": "Google Cloud TPU v4 — system architecture", + "url": "https://docs.cloud.google.com/tpu/docs/v4", + "checked": "2026-10-14" + }, + { + "id": "google-tpu-architecture", + "title": "Google Cloud TPU architecture", + "url": "https://docs.cloud.google.com/tpu/docs/system-architecture-tpu-vm", + "checked": "2026-10-14" + } + ], + "limits": [ + "NPU here means a neural accelerator. TPU is Google’s named tensor processor, not a network packet processor.", + "The small matrix model computes real sums but uses an authored dataflow; it does not emulate a TPU v4 MXU.", + "This is a documented reference generation, not a claim of the latest or fastest TPU." + ] + }, + { + "id": "silicon-one-g200", + "family": "asic", + "name": "Cisco Silicon One G200", + "facts": [ + { + "label": "Function", + "value": "Standalone network switching processor", + "source": "cisco-g200", + "locator": "Product overview" + }, + { + "label": "Published switching bandwidth", + "value": "51.2 Tbps, full-duplex", + "source": "cisco-g200", + "locator": "Product overview" + }, + { + "label": "SerDes", + "value": "512 × 112G", + "source": "cisco-g200", + "locator": "Features → long-reach SerDes" + }, + { + "label": "Ethernet port rates", + "value": "Up to 800 Gbps", + "source": "cisco-g200", + "locator": "Features → flexible port configuration" + }, + { + "label": "Packet storage", + "value": "Fully shared on-die packet buffer", + "source": "cisco-g200", + "locator": "Architectural characteristics → Large and fully unified packet buffer" + }, + { + "label": "Host connection", + "value": "PCIe Gen3 + two Ethernet interfaces", + "source": "cisco-g200", + "locator": "Features → host CPU complex interfaces" + } + ], + "sources": [ + { + "id": "cisco-g200", + "title": "Cisco Silicon One G200 data sheet", + "url": "https://www.cisco.com/c/en/us/solutions/collateral/silicon-one/silicon-one-g200-ds.html", + "checked": "2026-10-15" + }, + { + "id": "cisco-voq", + "title": "Cisco Silicon One QoS white paper — mechanism context", + "url": "https://www.cisco.com/c/en/us/products/collateral/switches/catalyst-9500-series-switches/catalyst-9500x-9600x-qos-q200-wp.html", + "checked": "2026-10-14" + } + ], + "limits": [ + "Cisco’s NPU terminology means Network Processing Unit. This chip moves network traffic; it is not a transformer tensor-compute engine.", + "The packet scene is a generic input-queued switch with authored capacity, rotating grants and model ticks. It does not emulate G200 or a proprietary Cisco scheduler.", + "The QoS reference documents VOQ mechanisms on named Catalyst platforms. Their Q200 SMS/HBM capacities are not attributed to G200.", + "The G200 data sheet used here does not disclose an exact buffer capacity, internal stage timing or a physical floorplan." + ], + "timing": { + "kind": "throughput", + "bitsPerSecond": 51200000000000, + "label": "51.2 Tbps, full-duplex", + "sourceIds": [ + "cisco-g200" + ], + "scope": "Cisco’s published aggregate switching bandwidth, preserving its full-duplex convention. Not a processor clock, packet latency or guarantee of application throughput; no conversion to clock period or model ticks is made." + } + }, + { + "id": "silicon-one-g300", + "family": "asic", + "name": "Cisco Silicon One G300", + "facts": [ + { + "label": "Function", + "value": "Standalone network switching processor", + "source": "cisco-g300", + "locator": "Product overview" + }, + { + "label": "Published switching bandwidth", + "value": "102.4 Tbps, full-duplex", + "source": "cisco-g300", + "locator": "Product overview" + }, + { + "label": "SerDes", + "value": "512 × 224 Gbps", + "source": "cisco-g300", + "locator": "Features → long-reach SerDes" + }, + { + "label": "Ethernet port rates", + "value": "Up to 1,600 Gbps", + "source": "cisco-g300", + "locator": "Features → flexible port configuration" + }, + { + "label": "Packet storage", + "value": "Fully shared on-die packet buffer", + "source": "cisco-g300", + "locator": "Architectural characteristics → Large and fully unified packet buffer" + }, + { + "label": "Host connection", + "value": "PCIe Gen4 + four Ethernet interfaces", + "source": "cisco-g300", + "locator": "Features → host CPU complex interfaces" + } + ], + "sources": [ + { + "id": "cisco-g300", + "title": "Cisco Silicon One G300 data sheet", + "url": "https://www.cisco.com/c/en/us/solutions/collateral/silicon-one/silicon-one-g300-ds.html", + "checked": "2026-10-14" + }, + { + "id": "cisco-voq", + "title": "Cisco Silicon One QoS white paper — mechanism context", + "url": "https://www.cisco.com/c/en/us/products/collateral/switches/catalyst-9500-series-switches/catalyst-9500x-9600x-qos-q200-wp.html", + "checked": "2026-10-14" + } + ], + "limits": [ + "Cisco’s NPU terminology means Network Processing Unit. This chip moves network traffic; it is not a transformer tensor-compute engine.", + "The packet scene is a generic input-queued switch with authored capacity, rotating grants and model ticks. It does not emulate G300 or a proprietary Cisco scheduler.", + "The QoS reference documents VOQ mechanisms on named Catalyst platforms. Their Q200 SMS/HBM capacities are not attributed to G300.", + "The G300 data sheet used here does not disclose an exact buffer capacity, internal stage timing or a physical floorplan." + ] + } + ], + "provenance": "Original factual summaries of linked public primary sources. Vendor artwork, SDKs, RTL and model weights are not included. Linked pages retain their own rights and terms." +} diff --git a/public/index.html b/public/index.html index 6701dc7..7d4d7aa 100644 --- a/public/index.html +++ b/public/index.html @@ -3,302 +3,196 @@ - + doke - - - + + - - + +
- doke◦ -
- - -
-
- ANATOMICAL ATLASWhole brain -
-
-
-

Loading the reference atlas.

- LOADING HUMAN ANATOMY -
-
-
- CHANGE YOUR PERSPECTIVE -
- - - - - - -
-
- -
- -
-
- DRAG TO ROTATE · SCROLL TO ZOOM · CLICK TO - DISCOVER -
- - + aria-label="Interactive processor. Drag to orbit, Shift-drag to pan, pinch or scroll to zoom, click a component to inspect, double click a core to enter. Arrow keys orbit, plus and minus zoom, Home resets." + > +
- -
-