Determinism — 1000 FFTs, one hash
The substrate runs a Cooley–Tukey radix-2 FFT 256-pt 1000 times per frame on a fixed two-tone input. Each output is hashed with FNV-1a-64 and the unique-hash count is displayed live. On a deterministic kernel the count is 1: every cell on the grid is emerald, the receipt strip flips ≡, and the canonical hash is stable across reloads. Toggle the perturbation checkbox to inject a per-iteration input kick that mimics the batch-size-variance failure mode Horace He's post identifies — the cells diverge into multiple colours and the ≠ indicator fires.
view the kernel that wrote this receipt crates/mathground-view/src/web.rs
//! JS surface — `wasm` feature only.
//!
//! Exposes three `mount_*` entry points. Each takes a canvas ID + a small
//! parameter struct (passed as JSON from JS), drives the kernel + draw loop
//! via `requestAnimationFrame`, and emits a per-frame `Receipt` back to the
//! page through an updater callback.
//!
//! The page is responsible for: providing the `<canvas>` and a receipt
//! sink. The crate is responsible for: math, drawing, timing.
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, WebGl2RenderingContext};
use mgai_meter_web::{build_receipt, primitive_from_measurement, now_ns, DEFAULT_TDP_W};
use mgai_active_inference::structure::StructureLearner;
use mgai_active_inference::{Agent as AiAgent, GenerativeModel as AiModel};
use mgai_voxel::{Aabb as VoxAabb, Vec3 as VoxVec3, VoxelGrid};
use mgai_cea::{
search as cea_search, ConstrainedProblem, Constraint, Design, MaxMass, MinimizeVolume,
ParametricSphere, SearchParams,
};
use crate::cascade::{self, CascadeTier, SubExpert, TimeFn};
// ── JSON-shaped cascade walk for the escalate exhibit ────────────────
//
// The lazy-load demo at /exhibits/escalate is driven entirely from JS:
// it imports this function to walk the cascade, decides from
// `resolved_at` whether to lazy-import the l2-leaf bundle, and renders
// the trace itself. The walk is the same `cascade::walk` the on-canvas
// reason exhibit uses.
#[wasm_bindgen]
pub fn cascade_walk_json(query: &str) -> JsValue {
let time = TimeFn { now_ns };
let trace = cascade::walk(query, &time);
serde_wasm_bindgen::to_value(&trace).unwrap_or(JsValue::NULL)
}
use crate::fft::{fft_in_place, fft_power_spectrum};
use crate::fft2d::{fft2d_in_place, log_magnitude_shifted};
use crate::graddescent::{gd_step, rosenbrock, GdParams, GdState};
use crate::heat::{heat_step, HeatParams};
use crate::hdc::Hv;
use crate::lorenz::{lorenz_rk4_step, LorenzParams, LorenzState};
use crate::modal::{encode_audio, encode_image, encode_text, encode_video};
use crate::pfilter::{pfilter_step, PFilterParams, PFilterState};
use crate::sdf::marching_squares;
use crate::sdf3d;
#[wasm_bindgen(start)]
pub fn _start() {
console_error_panic_hook::set_once();
}
fn ctx_for(canvas_id: &str) -> Option<(HtmlCanvasElement, CanvasRenderingContext2d)> {
let win = web_sys::window()?;
let doc = win.document()?;
let canvas = doc.get_element_by_id(canvas_id)?;
let canvas: HtmlCanvasElement = canvas.dyn_into().ok()?;
let ctx: CanvasRenderingContext2d = canvas
.get_context("2d")
.ok()??
.dyn_into()
.ok()?;
Some((canvas, ctx))
}
fn emit_receipt(
sink: &js_sys::Function,
label: &str,
v_class: &str,
iters: u64,
wall_ns: f64,
bit_ops_per_op: u64,
) {
let prim = primitive_from_measurement(label, v_class, iters, wall_ns, bit_ops_per_op, DEFAULT_TDP_W);
let receipt = build_receipt(vec![prim], DEFAULT_TDP_W);
let js = serde_wasm_bindgen::to_value(&receipt).unwrap_or(JsValue::NULL);
let _ = sink.call1(&JsValue::NULL, &js);
}
/// Emit a receipt + carry an `output_hash` on the primitive. The
/// determinism exhibit uses this so the receipt strip on the page can
/// flip its ≡ / ≠ indicator on the substrate-reported hash instead of
/// re-hashing on the JS side.
fn emit_receipt_with_hash(
sink: &js_sys::Function,
label: &str,
v_class: &str,
iters: u64,
wall_ns: f64,
bit_ops_per_op: u64,
output_hash: u64,
) {
let prim = primitive_from_measurement(label, v_class, iters, wall_ns, bit_ops_per_op, DEFAULT_TDP_W)
.with_output_hash(output_hash);
let receipt = build_receipt(vec![prim], DEFAULT_TDP_W);
let js = serde_wasm_bindgen::to_value(&receipt).unwrap_or(JsValue::NULL);
let _ = sink.call1(&JsValue::NULL, &js);
}
/// FNV-1a 64-bit over a `&[f32]`'s raw bytes. Used by the determinism
/// exhibit to fingerprint each FFT output cheaply — the same family as
/// the proxy's resolver fingerprint (`lux_worlds_faeble::receipt::
/// fnv1a_64`). Re-implemented here so mathground-view stays free of
/// runtime deps on the faeble side; FNV-1a's offset basis and prime
/// are public constants.
fn fnv1a_64_f32_slice(xs: &[f32]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for x in xs {
// Hash the IEEE-754 bit pattern so +0/-0 and NaN payloads are
// distinguishable across runs. `to_bits()` is the canonical
// f32 → u32 byte view.
let bits = x.to_bits().to_le_bytes();
for &b in &bits {
h ^= b as u64;
h = h.wrapping_mul(0x100_0000_01b3);
}
}
h
}
fn schedule(closure: &Closure<dyn FnMut()>) {
let _ = web_sys::window()
.expect("window")
.request_animation_frame(closure.as_ref().unchecked_ref());
}
// ── FFT exhibit ──────────────────────────────────────────────────────
/// Mount the FFT exhibit on `canvas_id`. The kernel reuses a stationary
/// test signal (sum of two sines) so the spectrum is interpretable; in
/// future iterations the page can push live samples.
#[wasm_bindgen]
pub fn mount_fft(canvas_id: &str, n: u32, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let n_usize = n as usize;
if !n_usize.is_power_of_two() || n_usize < 16 {
return Err(JsValue::from_str("n must be a power of two ≥ 16"));
}
let buf = Rc::new(RefCell::new(vec![0.0f32; 2 * n_usize]));
let spec = Rc::new(RefCell::new(vec![0.0f32; n_usize / 2]));
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let buf2 = buf.clone();
let spec2 = spec.clone();
let mut phase = 0.0f32;
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
// ── Signal: two sines, swept phase to keep it visually alive ──
let mut b = buf2.borrow_mut();
for k in 0..n_usize {
let t = k as f32 / n_usize as f32;
let s = (2.0 * core::f32::consts::PI * 4.0 * t + phase).sin()
+ 0.5 * (2.0 * core::f32::consts::PI * 12.0 * t).sin();
b[2 * k] = s;
b[2 * k + 1] = 0.0;
}
phase += 0.05;
// ── Compute pass — timed ──
let t0 = now_ns();
let report = fft_in_place(&mut b).expect("power-of-two");
let mut s = spec2.borrow_mut();
fft_power_spectrum(&b, &mut s);
let wall_ns = now_ns() - t0;
// ── Draw pass — not metered (page chrome) ──
let w = canvas.width() as f64;
let h = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, w, h);
ctx.set_stroke_style_str("rgba(120, 220, 255, 0.95)");
ctx.set_line_width(1.5);
ctx.begin_path();
let bins = s.len();
for i in 0..bins {
let x = (i as f64 / bins as f64) * w;
// s[i] is dB ∈ [-120, 0] roughly. Map to canvas.
let db = s[i].clamp(-120.0, 0.0) as f64;
let y = h - (db + 120.0) / 120.0 * h;
if i == 0 {
ctx.move_to(x, y);
} else {
ctx.line_to(x, y);
}
}
ctx.stroke();
// ── Live-indicator cursor: a thin vertical line that sweeps
// across the canvas. Drives the "live" intuition that the
// receipt next to this kernel updates every frame, even when
// the spectrum itself looks stationary at a glance. ──
let cursor_x = ((phase * 60.0) as f64).rem_euclid(w);
ctx.set_stroke_style_str("rgba(255, 220, 140, 0.85)");
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.move_to(cursor_x, 0.0);
ctx.line_to(cursor_x, h);
ctx.stroke();
// A small dot at the spectrum at the cursor x — reads as "the
// measurement just sampled this bin".
let bin_at_cursor = ((cursor_x / w) * (bins as f64)).floor() as usize;
let bin_at_cursor = bin_at_cursor.min(bins.saturating_sub(1));
if bins > 0 {
let db = s[bin_at_cursor].clamp(-120.0, 0.0) as f64;
let y = h - (db + 120.0) / 120.0 * h;
ctx.set_fill_style_str("rgba(255, 220, 140, 0.95)");
ctx.begin_path();
ctx.arc(cursor_x, y, 2.5, 0.0, std::f64::consts::TAU).ok();
ctx.fill();
}
// ── Emit receipt for the compute pass only ──
// Hash the per-frame power spectrum so /receipts/05's
// reproducibility receipt has live data per frame. Spectrum
// evolves with the sweeping phase, so consecutive hashes differ
// by design; the field is populated, not claiming ≡-stability.
emit_receipt_with_hash(
&sink,
"fft_radix2",
"L0Closed",
1,
wall_ns,
report.bit_ops,
fnv1a_64_f32_slice(&s),
);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
// Keep the closure alive forever.
std::mem::forget(g);
Ok(())
}
// ── Lorenz exhibit ───────────────────────────────────────────────────
/// Mount the Lorenz attractor on `canvas_id`. Draws a trail in the (x, z)
/// projection; advances `steps_per_frame` RK4 steps per animation frame.
#[wasm_bindgen]
pub fn mount_lorenz(canvas_id: &str, steps_per_frame: u32, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let params = LorenzParams::default();
let state = Rc::new(RefCell::new(LorenzState::default()));
let trail: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::with_capacity(4096)));
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let state2 = state.clone();
let trail2 = trail.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let t0 = now_ns();
let mut s = state2.borrow_mut();
let mut bit_ops_total: u64 = 0;
let mut tr = trail2.borrow_mut();
for _ in 0..steps_per_frame {
bit_ops_total += lorenz_rk4_step(&mut s, ¶ms);
tr.push((s.x as f32, s.z as f32));
}
// Cap trail length.
let cap = 4096usize;
if tr.len() > cap {
let drop = tr.len() - cap;
tr.drain(0..drop);
}
let wall_ns = now_ns() - t0;
let w = canvas.width() as f64;
let h = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, w, h);
// Centre + scale: x ∈ ~[-25, 25], z ∈ ~[0, 50].
let cx = w * 0.5;
let cy = h * 0.5;
let scale = (w.min(h) / 60.0) as f32;
ctx.set_stroke_style_str("rgba(255, 180, 80, 0.7)");
ctx.set_line_width(1.0);
ctx.begin_path();
for (i, (x, z)) in tr.iter().enumerate() {
let px = cx as f32 + x * scale;
let py = cy as f32 - (z - 25.0) * scale;
if i == 0 {
ctx.move_to(px as f64, py as f64);
} else {
ctx.line_to(px as f64, py as f64);
}
}
ctx.stroke();
emit_receipt(
&sink,
"lorenz_rk4",
"L0Closed",
steps_per_frame as u64,
wall_ns,
bit_ops_total / steps_per_frame.max(1) as u64,
);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── SDF exhibit ──────────────────────────────────────────────────────
/// Mount the 2-D SDF exhibit. Renders the iso-contour of a slowly drifting
/// scalar field (sum of two moving circles' SDFs, deformed by a sine) via
/// marching squares.
#[wasm_bindgen]
pub fn mount_sdf(canvas_id: &str, grid_w: u32, grid_h: u32, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let cells = Rc::new(RefCell::new(vec![0.0f32; (grid_w * grid_h) as usize]));
let segs = Rc::new(RefCell::new(Vec::with_capacity(8192)));
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let cells2 = cells.clone();
let segs2 = segs.clone();
let mut tau = 0.0f32;
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
// Update field.
let mut c = cells2.borrow_mut();
let cx1 = grid_w as f32 * (0.4 + 0.1 * (tau).cos());
let cy1 = grid_h as f32 * 0.5;
let cx2 = grid_w as f32 * (0.6 + 0.1 * (tau + 1.0).sin());
let cy2 = grid_h as f32 * 0.5;
let r = grid_w.min(grid_h) as f32 * 0.15;
for y in 0..grid_h {
for x in 0..grid_w {
let dx1 = x as f32 - cx1;
let dy1 = y as f32 - cy1;
let d1 = (dx1 * dx1 + dy1 * dy1).sqrt() - r;
let dx2 = x as f32 - cx2;
let dy2 = y as f32 - cy2;
let d2 = (dx2 * dx2 + dy2 * dy2).sqrt() - r;
// Smooth-min union (k = 4) — Quílez.
let k = 4.0_f32;
let h_ = ((k - (d1 - d2).abs()).max(0.0) / k).powi(2) * 0.5;
let d = d1.min(d2) - h_ * k * 0.5;
c[(y * grid_w + x) as usize] = d;
}
}
tau += 0.02;
let t0 = now_ns();
let mut s = segs2.borrow_mut();
let report = marching_squares(&c, grid_w, grid_h, 0.0, &mut s);
let wall_ns = now_ns() - t0;
// Draw.
let w = canvas.width() as f64;
let h_can = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, w, h_can);
let sx = w / grid_w as f64;
let sy = h_can / grid_h as f64;
ctx.set_stroke_style_str("rgba(180, 255, 200, 0.95)");
ctx.set_line_width(1.5);
ctx.begin_path();
let mut i = 0;
while i + 3 < s.len() {
ctx.move_to(s[i] as f64 * sx, s[i + 1] as f64 * sy);
ctx.line_to(s[i + 2] as f64 * sx, s[i + 3] as f64 * sy);
i += 4;
}
ctx.stroke();
emit_receipt(&sink, "sdf_marching_squares", "L0Closed", 1, wall_ns, report.bit_ops);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Heat-equation exhibit ────────────────────────────────────────────
#[wasm_bindgen]
pub fn mount_heat(canvas_id: &str, grid_w: u32, grid_h: u32, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let n = (grid_w * grid_h) as usize;
let mut u0 = vec![0.0f32; n];
u0[(grid_h / 2 * grid_w + grid_w / 2) as usize] = 200.0;
for k in 0..6 {
let cx = (grid_w as f32 * (0.2 + 0.12 * k as f32)).min((grid_w - 1) as f32) as u32;
let cy = (grid_h as f32 * (0.7 - 0.04 * k as f32)).max(0.0) as u32;
u0[(cy * grid_w + cx) as usize] = 200.0;
}
let u = Rc::new(RefCell::new(u0));
let u_next = Rc::new(RefCell::new(vec![0.0f32; n]));
let params = HeatParams::default();
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let u2 = u.clone();
let u_next2 = u_next.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let t0 = now_ns();
let mut a = u2.borrow_mut();
let mut b = u_next2.borrow_mut();
let steps = 4u32;
let mut bit_ops_total: u64 = 0;
for _ in 0..steps {
let r = heat_step(&a, &mut b, grid_w, grid_h, ¶ms);
bit_ops_total += r.bit_ops;
std::mem::swap(&mut *a, &mut *b);
}
let wall_ns = now_ns() - t0;
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
let sx = cw / grid_w as f64;
let sy = ch / grid_h as f64;
let mut maxv = 1e-9f32;
for &v in a.iter() {
if v > maxv { maxv = v; }
}
for y in 0..grid_h {
for x in 0..grid_w {
let v = (a[(y * grid_w + x) as usize] / maxv).clamp(0.0, 1.0);
let r = (255.0 * v.powf(0.5)) as u32;
let g_ = (60.0 + 120.0 * v) as u32;
let bb = (160.0 * (1.0 - v)) as u32;
ctx.set_fill_style_str(&format!("rgb({r},{g_},{bb})"));
ctx.fill_rect(x as f64 * sx, y as f64 * sy, sx + 1.0, sy + 1.0);
}
}
emit_receipt(&sink, "heat_forward_euler", "L0Closed", steps as u64, wall_ns, bit_ops_total / steps as u64);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Particle-filter exhibit ──────────────────────────────────────────
#[wasm_bindgen]
pub fn mount_pfilter(canvas_id: &str, n_particles: u32, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let mut params = PFilterParams::default();
params.n_particles = n_particles.max(32);
let state = Rc::new(RefCell::new(PFilterState::new(¶ms, 0xC0DE_BEEF_F00Du64)));
let truth_trail: Rc<RefCell<Vec<f32>>> = Rc::new(RefCell::new(Vec::with_capacity(800)));
let obs_trail: Rc<RefCell<Vec<f32>>> = Rc::new(RefCell::new(Vec::with_capacity(800)));
let mean_trail: Rc<RefCell<Vec<f32>>> = Rc::new(RefCell::new(Vec::with_capacity(800)));
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let state2 = state.clone();
let truth2 = truth_trail.clone();
let obs2 = obs_trail.clone();
let mean2 = mean_trail.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let t0 = now_ns();
let mut s = state2.borrow_mut();
let r = pfilter_step(&mut s, ¶ms, 0.1);
let wall_ns = now_ns() - t0;
let mut t = truth2.borrow_mut();
let mut o = obs2.borrow_mut();
let mut m = mean2.borrow_mut();
t.push(r.truth);
o.push(r.obs);
m.push(s.posterior_mean);
let cap = 800usize;
for v in [&mut t, &mut o, &mut m] {
if v.len() > cap { let d = v.len() - cap; v.drain(0..d); }
}
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
let amp = params.amplitude as f64;
let y_of = |v: f32| ch * 0.5 - (v as f64) * (ch * 0.35 / amp);
let x_of = |i: usize, n: usize| cw * (i as f64) / (n.max(1) as f64);
let n_trail = t.len();
ctx.set_fill_style_str("rgba(255, 180, 80, 0.5)");
for (i, &v) in o.iter().enumerate() {
let x = x_of(i, n_trail);
let y = y_of(v);
ctx.fill_rect(x - 1.0, y - 1.0, 2.0, 2.0);
}
ctx.set_stroke_style_str("rgba(120, 220, 255, 0.95)");
ctx.set_line_width(2.0);
ctx.begin_path();
for (i, &v) in t.iter().enumerate() {
let x = x_of(i, n_trail);
let y = y_of(v);
if i == 0 { ctx.move_to(x, y); } else { ctx.line_to(x, y); }
}
ctx.stroke();
ctx.set_stroke_style_str("rgba(180, 255, 200, 0.9)");
ctx.set_line_width(1.5);
ctx.begin_path();
for (i, &v) in m.iter().enumerate() {
let x = x_of(i, n_trail);
let y = y_of(v);
if i == 0 { ctx.move_to(x, y); } else { ctx.line_to(x, y); }
}
ctx.stroke();
ctx.set_fill_style_str("rgba(255, 180, 220, 0.45)");
let x_now = cw - 4.0;
for &x_val in s.particles.iter() {
ctx.fill_rect(x_now, y_of(x_val) - 1.0, 3.0, 3.0);
}
emit_receipt(&sink, "particle_filter_systematic", "L0Closed", 1, wall_ns, r.bit_ops);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Gradient-descent exhibit ─────────────────────────────────────────
#[wasm_bindgen]
pub fn mount_grad(canvas_id: &str, steps_per_frame: u32, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let p = GdParams::default();
let state = Rc::new(RefCell::new(GdState::default()));
let trail: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::with_capacity(8192)));
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let state2 = state.clone();
let trail2 = trail.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let t0 = now_ns();
let mut s = state2.borrow_mut();
let mut tr = trail2.borrow_mut();
let mut bit_ops_total: u64 = 0;
let mut loss = 0.0f32;
for _ in 0..steps_per_frame {
let r = gd_step(&mut s, &p);
bit_ops_total += r.bit_ops;
loss = r.loss;
tr.push((s.x, s.y));
}
let cap = 4096usize;
if tr.len() > cap { let drop = tr.len() - cap; tr.drain(0..drop); }
if !s.x.is_finite() || !s.y.is_finite() || s.x.abs() > 5.0 || s.y.abs() > 8.0 || loss > 1e8 {
*s = GdState::default();
tr.clear();
}
let wall_ns = now_ns() - t0;
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
let x_min = -2.0f32; let x_max = 2.0f32;
let y_min = -1.5f32; let y_max = 3.5f32;
let to_px = |x: f32, y: f32| (
((x - x_min) / (x_max - x_min)) as f64 * cw,
(1.0 - (y - y_min) / (y_max - y_min)) as f64 * ch,
);
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
let gw = 40u32; let gh = 30u32;
for j in 0..gh {
for i in 0..gw {
let x = x_min + (i as f32 + 0.5) * (x_max - x_min) / gw as f32;
let y = y_min + (j as f32 + 0.5) * (y_max - y_min) / gh as f32;
let v = rosenbrock(x, y, p.a, p.b).max(1e-3).ln();
let t = ((v + 4.0) / 12.0).clamp(0.0, 1.0) as f64;
let r = (40.0 + 130.0 * t) as u32;
let gg = (40.0 + 100.0 * (1.0 - t)) as u32;
let bb = (90.0 + 80.0 * (1.0 - t)) as u32;
ctx.set_fill_style_str(&format!("rgb({r},{gg},{bb})"));
let (px0, py0) = to_px(x_min + i as f32 * (x_max - x_min) / gw as f32,
y_min + (j + 1) as f32 * (y_max - y_min) / gh as f32);
let (px1, py1) = to_px(x_min + (i + 1) as f32 * (x_max - x_min) / gw as f32,
y_min + j as f32 * (y_max - y_min) / gh as f32);
ctx.fill_rect(px0, py0, px1 - px0 + 1.0, py1 - py0 + 1.0);
}
}
ctx.set_stroke_style_str("rgba(255, 240, 140, 0.92)");
ctx.set_line_width(2.0);
ctx.begin_path();
for (i, &(x, y)) in tr.iter().enumerate() {
let (px, py) = to_px(x, y);
if i == 0 { ctx.move_to(px, py); } else { ctx.line_to(px, py); }
}
ctx.stroke();
let (mpx, mpy) = to_px(1.0, 1.0);
ctx.set_fill_style_str("rgba(255, 80, 120, 0.95)");
ctx.begin_path();
ctx.arc(mpx, mpy, 4.0, 0.0, std::f64::consts::TAU).unwrap();
ctx.fill();
emit_receipt(&sink, "gd_momentum_rosenbrock", "L0Closed", steps_per_frame as u64, wall_ns, bit_ops_total / steps_per_frame.max(1) as u64);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── 2-D FFT exhibit (visual-fft-revolution lineage) ──────────────────
#[wasm_bindgen]
pub fn mount_fft2d(canvas_id: &str, dim: u32, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let dim_us = dim as usize;
if !dim_us.is_power_of_two() || dim_us < 32 {
return Err(JsValue::from_str("dim must be a power of two ≥ 32"));
}
let buf = Rc::new(RefCell::new(vec![0.0f32; 2 * dim_us * dim_us]));
let img = Rc::new(RefCell::new(vec![0.0f32; dim_us * dim_us]));
let mag = Rc::new(RefCell::new(vec![0.0f32; dim_us * dim_us]));
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let buf2 = buf.clone();
let img2 = img.clone();
let mag2 = mag.clone();
let mut tau = 0.0f32;
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let mut im = img2.borrow_mut();
for y in 0..dim_us {
for x in 0..dim_us {
let fx = (x as f32 / dim_us as f32 - 0.5) * 2.0;
let fy = (y as f32 / dim_us as f32 - 0.5) * 2.0;
let s1 = (12.0 * fx + tau).sin();
let s2 = (10.0 * fy + 0.7 * tau).sin();
let s3 = (9.0 * fx + 9.0 * fy + tau * 0.3).sin();
let pulse = (-((fx * fx + fy * fy) * 4.0)).exp() * (tau * 0.5).cos();
im[y * dim_us + x] = 0.33 * (s1 + s2 + s3) + 0.5 * pulse;
}
}
let mut b = buf2.borrow_mut();
for k in 0..(dim_us * dim_us) {
b[2 * k] = im[k];
b[2 * k + 1] = 0.0;
}
let t0 = now_ns();
let report = fft2d_in_place(&mut b, dim, dim).expect("power-of-two");
let mut m = mag2.borrow_mut();
let _ = log_magnitude_shifted(&b, dim, dim, &mut m);
let wall_ns = now_ns() - t0;
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
let panel_w = cw * 0.5;
let sx = panel_w / dim as f64;
let sy = ch / dim as f64;
let mut min_v = f32::INFINITY;
let mut max_v = f32::NEG_INFINITY;
for &v in im.iter() {
if v < min_v { min_v = v; }
if v > max_v { max_v = v; }
}
let scale = (max_v - min_v).max(1e-6);
for y in 0..dim_us {
for x in 0..dim_us {
let v = (im[y * dim_us + x] - min_v) / scale;
let r = (255.0 * v) as u32;
let g_ = (120.0 + 100.0 * v) as u32;
let bb = (220.0 - 100.0 * v) as u32;
ctx.set_fill_style_str(&format!("rgb({r},{g_},{bb})"));
ctx.fill_rect(x as f64 * sx, y as f64 * sy, sx + 1.0, sy + 1.0);
}
}
let mut min_l = f32::INFINITY;
let mut max_l = f32::NEG_INFINITY;
for &v in m.iter() {
if v.is_finite() {
if v < min_l { min_l = v; }
if v > max_l { max_l = v; }
}
}
let scale2 = (max_l - min_l).max(1e-6);
for y in 0..dim_us {
for x in 0..dim_us {
let v = ((m[y * dim_us + x] - min_l) / scale2).clamp(0.0, 1.0);
let r = (80.0 + 175.0 * v) as u32;
let g_ = (30.0 + 120.0 * v) as u32;
let bb = (160.0 * v) as u32;
ctx.set_fill_style_str(&format!("rgb({r},{g_},{bb})"));
ctx.fill_rect(panel_w + x as f64 * sx, y as f64 * sy, sx + 1.0, sy + 1.0);
}
}
tau += 0.05;
emit_receipt_with_hash(
&sink,
"fft2d_radix2",
"L0Closed",
1,
wall_ns,
report.bit_ops,
fnv1a_64_f32_slice(&m),
);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── 3-D SDF (WebGL2 sphere-trace) exhibit ────────────────────────────
fn compile_shader(
gl: &WebGl2RenderingContext,
kind: u32,
src: &str,
) -> Result<web_sys::WebGlShader, String> {
let shader = gl.create_shader(kind).ok_or("create_shader failed")?;
gl.shader_source(&shader, src);
gl.compile_shader(&shader);
let ok = gl
.get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false);
if !ok {
return Err(gl.get_shader_info_log(&shader).unwrap_or_default());
}
Ok(shader)
}
fn link_program(
gl: &WebGl2RenderingContext,
vs: &web_sys::WebGlShader,
fs: &web_sys::WebGlShader,
) -> Result<web_sys::WebGlProgram, String> {
let prog = gl.create_program().ok_or("create_program failed")?;
gl.attach_shader(&prog, vs);
gl.attach_shader(&prog, fs);
gl.link_program(&prog);
let ok = gl
.get_program_parameter(&prog, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false);
if !ok {
return Err(gl.get_program_info_log(&prog).unwrap_or_default());
}
Ok(prog)
}
// ── Reason exhibit (cascade walk visualised + per-hop receipts) ──────
const REASON_QUERIES: &[&str] = &[
"2 + 2",
"6!",
"pi",
"pythagorean theorem",
"circumference of a unit circle",
"rms of a sine wave amplitude a",
"compose pi times e",
"what is the meaning of life?",
];
fn tier_color(t: CascadeTier) -> &'static str {
match t {
CascadeTier::L0Closed => "rgba(120, 220, 255, 0.95)",
CascadeTier::L0Retrieved => "rgba(180, 255, 200, 0.95)",
CascadeTier::L0Identifiable => "rgba(190, 200, 255, 0.95)",
CascadeTier::L1 => "rgba(255, 220, 140, 0.95)",
CascadeTier::L2 => "rgba(255, 140, 160, 0.95)",
}
}
#[wasm_bindgen]
pub fn mount_reason(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let query_idx = Rc::new(RefCell::new(0usize));
let frame_in_query = Rc::new(RefCell::new(0u32));
let qi = query_idx.clone();
let fi = frame_in_query.clone();
let time = TimeFn { now_ns };
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let mut i = qi.borrow_mut();
let mut fr = fi.borrow_mut();
let q = REASON_QUERIES[*i];
let trace = cascade::walk(q, &time);
// Draw.
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
// Query banner.
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("query", 28.0, 32.0).ok();
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("18px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(q, 28.0, 56.0).ok();
// Hops, ladder-rendered. Each tier may contribute one (reasoning
// only) or two (reasoning + generation) hops. Generation hops are
// indented to make the MoT split visible at a glance.
let mut y = 100.0;
let row_h = 44.0;
for hop in trace.hops.iter() {
let is_gen = matches!(hop.expert, SubExpert::Generation);
let indent = if is_gen { 18.0 } else { 0.0 };
// Side bar: tier color (solid for reasoning, dashed/lower-alpha for generation).
let bar_alpha = if is_gen { 0.55 } else { 1.0 };
let bar_color = tier_color(hop.tier).replace("0.95", &format!("{:.2}", bar_alpha));
ctx.set_fill_style_str(&bar_color);
ctx.fill_rect(28.0 + indent, y - 14.0, 4.0, 36.0);
// Tier + expert label.
ctx.set_fill_style_str(tier_color(hop.tier));
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
let header = format!("{} · {}", hop.tier.label(), hop.expert.label());
ctx.fill_text(&header, 44.0 + indent, y).ok();
// Action.
ctx.set_fill_style_str("rgba(180, 188, 208, 1)");
ctx.fill_text(&hop.action, 44.0 + indent, y + 16.0).ok();
// Result on the right: ✓ / ✗ + answer if any.
let status = if hop.matched { "✓ matched" } else { "✗ no match" };
ctx.set_fill_style_str(if hop.matched {
"rgba(180, 255, 200, 1)"
} else {
"rgba(255, 180, 180, 0.6)"
});
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(status, cw - 220.0, y).ok();
if let Some(a) = &hop.answer {
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
let short = if a.chars().count() > 28 {
let mut s: String = a.chars().take(28).collect();
s.push('…');
s
} else {
a.clone()
};
ctx.fill_text(&short, cw - 220.0, y + 16.0).ok();
}
// Bit-ops + wall_ns/op.
ctx.set_fill_style_str("rgba(120, 130, 150, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
let cost = format!("{} bit-ops · {:.0} ns/op", hop.bit_ops, hop.wall_ns_per_op);
ctx.fill_text(&cost, 44.0 + indent, y + 30.0).ok();
y += row_h;
}
// Footer.
let resolved = match trace.resolved_at {
Some(t) => format!("resolved at {}", t.label()),
None => "refused at L2 (no model shipped)".to_string(),
};
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("13px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&resolved, 28.0, ch - 36.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let total = format!("total bit-ops across hops: {}", trace.total_bit_ops);
ctx.fill_text(&total, 28.0, ch - 18.0).ok();
// Emit a receipt summarising the whole trace as one primitive.
// Use the matched tier's name (or L2-refused) as the primitive id.
let prim_name = format!(
"cascade_walk[{}]",
trace.resolved_at.map(|t| t.label()).unwrap_or("L2_refused")
);
let v_class = match trace.resolved_at {
Some(CascadeTier::L0Closed) => "L0Closed",
Some(CascadeTier::L0Retrieved) => "L0Retrieved",
Some(CascadeTier::L0Identifiable) => "L0Identifiable",
Some(CascadeTier::L1) => "L1",
Some(CascadeTier::L2) => "L2",
None => "L2_refused",
};
// Sum wall_ns across hops.
let wall_ns_total: f64 = trace
.hops
.iter()
.map(|h| h.wall_ns_per_op * (h.bit_ops.max(1) as f64))
.sum();
emit_receipt(&sink, &prim_name, v_class, 1, wall_ns_total, trace.total_bit_ops);
// Rotate to next query after ~60 frames (~1 s at 60 fps).
*fr += 1;
if *fr > 60 {
*fr = 0;
*i = (*i + 1) % REASON_QUERIES.len();
}
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
#[wasm_bindgen]
pub fn mount_sdf3d(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
let win = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
let doc = win.document().ok_or_else(|| JsValue::from_str("no document"))?;
let canvas = doc
.get_element_by_id(canvas_id)
.ok_or_else(|| JsValue::from_str("canvas not found"))?;
let canvas: HtmlCanvasElement = canvas
.dyn_into()
.map_err(|_| JsValue::from_str("not a canvas"))?;
let gl = canvas
.get_context("webgl2")
.map_err(|_| JsValue::from_str("webgl2 unsupported"))?
.ok_or_else(|| JsValue::from_str("webgl2 unsupported"))?
.dyn_into::<WebGl2RenderingContext>()
.map_err(|_| JsValue::from_str("webgl2 ctx wrong type"))?;
let vs = compile_shader(&gl, WebGl2RenderingContext::VERTEX_SHADER, sdf3d::VERTEX_SRC)
.map_err(|e| JsValue::from_str(&format!("vs: {e}")))?;
let fs = compile_shader(&gl, WebGl2RenderingContext::FRAGMENT_SHADER, sdf3d::FRAGMENT_SRC)
.map_err(|e| JsValue::from_str(&format!("fs: {e}")))?;
let prog = link_program(&gl, &vs, &fs)
.map_err(|e| JsValue::from_str(&format!("link: {e}")))?;
let verts: [f32; 6] = [-1.0, -1.0, 3.0, -1.0, -1.0, 3.0];
let vbo = gl.create_buffer().ok_or_else(|| JsValue::from_str("vbo"))?;
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&vbo));
unsafe {
let arr = js_sys::Float32Array::view(&verts);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&arr,
WebGl2RenderingContext::STATIC_DRAW,
);
}
let vao = gl.create_vertex_array().ok_or_else(|| JsValue::from_str("vao"))?;
gl.bind_vertex_array(Some(&vao));
let a_pos = gl.get_attrib_location(&prog, "a_pos") as u32;
gl.enable_vertex_attrib_array(a_pos);
gl.vertex_attrib_pointer_with_i32(a_pos, 2, WebGl2RenderingContext::FLOAT, false, 0, 0);
let u_time = gl
.get_uniform_location(&prog, "u_time")
.ok_or_else(|| JsValue::from_str("u_time"))?;
let u_res = gl
.get_uniform_location(&prog, "u_resolution")
.ok_or_else(|| JsValue::from_str("u_resolution"))?;
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let start_ns = now_ns();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let t0 = now_ns();
let t_sec = (t0 - start_ns) * 1e-9;
let w = canvas.width();
let h = canvas.height();
gl.viewport(0, 0, w as i32, h as i32);
gl.clear_color(0.03, 0.05, 0.08, 1.0);
gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
gl.use_program(Some(&prog));
gl.uniform1f(Some(&u_time), t_sec as f32);
gl.uniform2f(Some(&u_res), w as f32, h as f32);
gl.bind_vertex_array(Some(&vao));
gl.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, 3);
let wall_ns = now_ns() - t0;
let bit_ops = sdf3d::estimate_bit_ops_per_frame(w, h);
emit_receipt(&sink, "sdf3d_sphere_trace", "L0Closed", 1, wall_ns, bit_ops);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Omnimodal exhibit ────────────────────────────────────────────────
//
// Four deterministic encoders project text, image, audio, and video into
// the same HDC shape-key space. The exhibit cycles through three scenes;
// each scene supplies one sample per modality, and the page renders four
// panels + a 4×4 cosine matrix proving the cross-modal projection works.
#[derive(Clone, Copy)]
struct Scene {
label: &'static str,
text: &'static str,
image: ImgKind,
audio: AudKind,
video: VidKind,
}
#[derive(Clone, Copy)]
enum ImgKind { Circle, Square, Diagonal }
#[derive(Clone, Copy)]
enum AudKind { SineTone, SquareWave, FreqSweep }
#[derive(Clone, Copy)]
enum VidKind { ExpandingCircle, PulsingSquare, ScrollingDiagonal }
const SCENES: &[Scene] = &[
Scene {
label: "circle",
text: "circle of radius one",
image: ImgKind::Circle,
audio: AudKind::SineTone,
video: VidKind::ExpandingCircle,
},
Scene {
label: "square",
text: "square grid pattern",
image: ImgKind::Square,
audio: AudKind::SquareWave,
video: VidKind::PulsingSquare,
},
Scene {
label: "diagonal",
text: "diagonal stripes scan",
image: ImgKind::Diagonal,
audio: AudKind::FreqSweep,
video: VidKind::ScrollingDiagonal,
},
];
fn make_image(kind: ImgKind) -> [u8; 64] {
let mut g = [0u8; 64];
match kind {
ImgKind::Circle => {
for r in 0..8 {
for c in 0..8 {
let dx = c as f32 - 3.5;
let dy = r as f32 - 3.5;
let d = (dx * dx + dy * dy).sqrt();
g[r * 8 + c] = if (d - 2.5).abs() < 1.0 { 220 } else { 30 };
}
}
}
ImgKind::Square => {
for r in 0..8 {
for c in 0..8 {
let on_ring = r == 1 || r == 6 || c == 1 || c == 6;
let inside = r >= 1 && r <= 6 && c >= 1 && c <= 6;
g[r * 8 + c] = if on_ring && inside { 220 } else { 30 };
}
}
}
ImgKind::Diagonal => {
for r in 0..8 {
for c in 0..8 {
g[r * 8 + c] = if (r + c) % 3 == 0 { 220 } else { 30 };
}
}
}
}
g
}
fn make_audio(kind: AudKind, n: usize) -> Vec<f32> {
let mut out = vec![0.0f32; n];
match kind {
AudKind::SineTone => {
for k in 0..n {
let t = k as f32 / n as f32;
out[k] = (2.0 * core::f32::consts::PI * 4.0 * t).sin();
}
}
AudKind::SquareWave => {
for k in 0..n {
let t = k as f32 / n as f32;
let phase = (2.0 * core::f32::consts::PI * 4.0 * t).sin();
out[k] = if phase >= 0.0 { 1.0 } else { -1.0 };
}
}
AudKind::FreqSweep => {
for k in 0..n {
let t = k as f32 / n as f32;
let f = 2.0 + 12.0 * t;
out[k] = (2.0 * core::f32::consts::PI * f * t).sin();
}
}
}
out
}
fn make_video(kind: VidKind, frames: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(64 * frames);
for t in 0..frames {
let f = t as f32 / frames as f32;
let frame = match kind {
VidKind::ExpandingCircle => {
let mut g = [0u8; 64];
let radius = 1.5 + f * 2.5;
for r in 0..8 {
for c in 0..8 {
let dx = c as f32 - 3.5;
let dy = r as f32 - 3.5;
let d = (dx * dx + dy * dy).sqrt();
g[r * 8 + c] = if (d - radius).abs() < 0.8 { 220 } else { 30 };
}
}
g
}
VidKind::PulsingSquare => {
let mut g = [0u8; 64];
let pulse = (0.5 + 0.5 * (2.0 * core::f32::consts::PI * f).sin()) * 220.0;
for r in 0..8 {
for c in 0..8 {
let on = (r == 1 || r == 6 || c == 1 || c == 6)
&& r >= 1 && r <= 6 && c >= 1 && c <= 6;
g[r * 8 + c] = if on { pulse as u8 } else { 30 };
}
}
g
}
VidKind::ScrollingDiagonal => {
let mut g = [0u8; 64];
let shift = (f * 6.0) as usize;
for r in 0..8 {
for c in 0..8 {
g[r * 8 + c] =
if ((r + c + shift) % 3) == 0 { 220 } else { 30 };
}
}
g
}
};
out.extend_from_slice(&frame);
}
out
}
fn draw_grid8(
ctx: &CanvasRenderingContext2d,
grid: &[u8],
x0: f64,
y0: f64,
cell: f64,
) {
for r in 0..8 {
for c in 0..8 {
let v = grid[r * 8 + c] as f64 / 255.0;
let shade = (40.0 + 180.0 * v) as u32;
ctx.set_fill_style_str(&format!("rgb({shade},{shade},{shade})"));
ctx.fill_rect(x0 + c as f64 * cell, y0 + r as f64 * cell, cell, cell);
}
}
}
fn draw_waveform(
ctx: &CanvasRenderingContext2d,
samples: &[f32],
x0: f64,
y0: f64,
w: f64,
h: f64,
) {
ctx.set_stroke_style_str("rgba(120, 220, 255, 0.95)");
ctx.set_line_width(1.2);
ctx.begin_path();
let n = samples.len();
for (i, &s) in samples.iter().enumerate() {
let x = x0 + (i as f64 / n.max(1) as f64) * w;
let y = y0 + h * 0.5 - (s as f64 * 0.5 * h);
if i == 0 {
ctx.move_to(x, y);
} else {
ctx.line_to(x, y);
}
}
ctx.stroke();
}
fn draw_hv_sig(
ctx: &CanvasRenderingContext2d,
hv: &Hv,
x0: f64,
y0: f64,
w: f64,
h: f64,
) {
let take = 64usize.min(hv.0.len());
let bw = w / take as f64;
for i in 0..take {
let v = hv.0[i];
let color = if v >= 0 { "rgba(120, 220, 255, 0.9)" } else { "rgba(255, 140, 160, 0.9)" };
ctx.set_fill_style_str(color);
let mag = h * 0.5;
if v >= 0 {
ctx.fill_rect(x0 + i as f64 * bw, y0 + h * 0.5 - mag, bw - 0.5, mag);
} else {
ctx.fill_rect(x0 + i as f64 * bw, y0 + h * 0.5, bw - 0.5, mag);
}
}
}
fn cosine_color(c: f32) -> String {
let v = c.clamp(-1.0, 1.0);
if v >= 0.0 {
let t = v.powf(0.6);
let r = (40.0 + 200.0 * t) as u32;
let g = (60.0 + 160.0 * t) as u32;
let b = (80.0 + 80.0 * t) as u32;
format!("rgb({r},{g},{b})")
} else {
let t = (-v).powf(0.6);
let r = (80.0 - 40.0 * t) as u32;
let g = (80.0 - 40.0 * t) as u32;
let b = (60.0 + 100.0 * t) as u32;
format!("rgb({r},{g},{b})")
}
}
#[wasm_bindgen]
pub fn mount_omnimodal(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let frame = Rc::new(RefCell::new(0u32));
let fr = frame.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let mut fc = fr.borrow_mut();
let scene_idx = ((*fc / 180) as usize) % SCENES.len();
let scene = SCENES[scene_idx];
// ── Compute pass: encode all four modalities ──
let t0 = now_ns();
let img = make_image(scene.image);
let img_r = encode_image(&img);
let aud = make_audio(scene.audio, 128);
let aud_r = encode_audio(&aud);
let vid = make_video(scene.video, 4);
let vid_r = encode_video(&vid, 4);
let txt_r = encode_text(scene.text);
let wall_ns_total = now_ns() - t0;
let bit_ops_total =
img_r.bit_ops + aud_r.bit_ops + vid_r.bit_ops + txt_r.bit_ops;
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
// Scene banner.
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("scene: {} ({}/{})", scene.label, scene_idx + 1, SCENES.len()), 24.0, 26.0).ok();
// Four panels across the top half.
let panel_w = (cw - 80.0) / 4.0;
let panel_h = ch * 0.5 - 50.0;
let panel_y = 44.0;
let names = ["text", "image", "audio", "video"];
let hvs = [&txt_r.hv, &img_r.hv, &aud_r.hv, &vid_r.hv];
for i in 0..4 {
let x = 24.0 + i as f64 * (panel_w + 8.0);
ctx.set_stroke_style_str("rgba(40, 50, 70, 1)");
ctx.set_line_width(1.0);
ctx.stroke_rect(x, panel_y, panel_w, panel_h);
// Header.
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(names[i], x + 10.0, panel_y + 18.0).ok();
// Preview area (top half).
let preview_y = panel_y + 26.0;
let preview_h = panel_h * 0.45;
ctx.set_fill_style_str("rgba(14, 18, 28, 1)");
ctx.fill_rect(x + 10.0, preview_y, panel_w - 20.0, preview_h);
match i {
0 => {
ctx.set_fill_style_str("rgba(200, 210, 230, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
let mut yy = preview_y + 16.0;
for line in scene.text.split_whitespace() {
ctx.fill_text(line, x + 14.0, yy).ok();
yy += 12.0;
}
}
1 => {
let cell = ((panel_w - 30.0) / 8.0).min(preview_h / 8.0);
let off_x = x + (panel_w - cell * 8.0) * 0.5;
let off_y = preview_y + (preview_h - cell * 8.0) * 0.5;
draw_grid8(&ctx, &img, off_x, off_y, cell);
}
2 => {
draw_waveform(&ctx, &aud, x + 14.0, preview_y + 4.0, panel_w - 28.0, preview_h - 8.0);
}
3 => {
// Show 4 mini frames side-by-side.
let mini_cell = ((panel_w - 30.0) / 32.0).min(preview_h / 8.0);
let off_y = preview_y + (preview_h - mini_cell * 8.0) * 0.5;
for t in 0..4 {
let off_x = x + 14.0 + (t as f64) * (mini_cell * 8.0 + 4.0);
let frame_slice = &vid[t * 64..(t + 1) * 64];
draw_grid8(&ctx, frame_slice, off_x, off_y, mini_cell);
}
}
_ => {}
}
// Hv signature (bottom half).
let sig_y = preview_y + preview_h + 8.0;
let sig_h = panel_h - (preview_h + 32.0);
ctx.set_fill_style_str("rgba(160, 170, 190, 0.9)");
ctx.set_font("9px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("shape-key (first 64 dims)", x + 10.0, sig_y - 2.0).ok();
draw_hv_sig(&ctx, hvs[i], x + 10.0, sig_y, panel_w - 20.0, sig_h);
}
// ── 4×4 cosine matrix ──
let mat_x = 24.0;
let mat_y = ch * 0.5 + 16.0;
let mat_avail_h = ch - mat_y - 56.0;
let cell_w = ((cw - 48.0 - 80.0) / 4.0).min(110.0);
let cell_h = (mat_avail_h / 5.0).min(36.0);
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("cross-modal cosine — same HDC shape-key space", mat_x, mat_y - 4.0).ok();
// Column headers.
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
for c in 0..4 {
ctx.fill_text(names[c], mat_x + 80.0 + c as f64 * cell_w + cell_w * 0.4, mat_y + 14.0).ok();
}
// Row labels + cells.
for r in 0..4 {
let row_y = mat_y + 22.0 + r as f64 * cell_h;
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(names[r], mat_x + 20.0, row_y + cell_h * 0.6).ok();
for c in 0..4 {
let x = mat_x + 80.0 + c as f64 * cell_w;
let value = hvs[r].cosine(hvs[c]);
ctx.set_fill_style_str(&cosine_color(value));
ctx.fill_rect(x, row_y, cell_w - 4.0, cell_h - 4.0);
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.fill_text(&format!("{:+.3}", value), x + 8.0, row_y + cell_h * 0.6).ok();
}
}
// Footer summary.
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
let footer_y = ch - 24.0;
ctx.fill_text(
&format!(
"bit-ops across 4 encoders: {} · wall_ns: {:.0}",
bit_ops_total,
wall_ns_total
),
mat_x,
footer_y,
).ok();
// Receipt: sum across the four encoders, V-class = L0-identifiable.
emit_receipt(
&sink,
"omnimodal_encoders[text|image|audio|video]",
"L0Identifiable",
1,
wall_ns_total,
bit_ops_total,
);
*fc = fc.wrapping_add(1);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Determinism exhibit ─────────────────────────────────────────────
//
// Runs a fixed-input FFT 256-pt 1000 times per frame, hashes each
// output, counts uniques. With the substrate's deterministic kernel,
// all 1000 outputs hash identically — the live readout reads
// "1 / 1000 unique hashes". A `get_perturb` callback returns a bool
// flag; when true, a per-iteration scrambling of the input phase is
// injected which mimics the failure mode Horace He's
// `batch-invariant-ops` post calls out — same prompt, different
// numerical path → divergent hash counts.
//
// The receipt this exhibit emits carries the FFT's output_hash on its
// primitive, so the receipt strip's ≡/≠ indicator updates from the
// substrate-reported field instead of being re-hashed on the JS side.
// That is the load-bearing demonstration of the 05 receipt shape on
// `/receipts`.
#[wasm_bindgen]
pub fn mount_determinism(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let n: usize = 256;
let runs_per_frame: usize = 1000;
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let mut frame_count: u32 = 0;
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
// Read the page-side perturbation toggle each frame. Checkbox
// ID is by convention `determinism-perturb`; missing element
// → perturb stays false (substrate's deterministic default).
let perturb = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id("determinism-perturb"))
.and_then(|el| el.dyn_into::<web_sys::HtmlInputElement>().ok())
.map(|cb| cb.checked())
.unwrap_or(false);
// Fixed input every frame so any drift is from the kernel, not
// the signal. Two-tone, length 256, deterministic.
let mut hashes: Vec<u64> = Vec::with_capacity(runs_per_frame);
let t0 = now_ns();
let mut total_bit_ops: u64 = 0;
let mut buf = vec![0.0f32; 2 * n];
let mut spec = vec![0.0f32; n / 2];
for run in 0..runs_per_frame {
// Reset to canonical input each iteration.
for k in 0..n {
let t = k as f32 / n as f32;
let s = (2.0 * core::f32::consts::PI * 4.0 * t).sin()
+ 0.5 * (2.0 * core::f32::consts::PI * 12.0 * t).sin();
buf[2 * k] = s;
buf[2 * k + 1] = 0.0;
}
// Perturbation: scramble the input by an iteration-dependent
// offset that breaks the input's bit-identity across runs.
// This is the "non-batch-invariant" failure mode in
// demonstration form — same nominal task, slightly different
// numerical path per iteration → divergent output hash.
if perturb {
let kick = (run as f32 * 1e-7) as f32;
for k in 0..n {
buf[2 * k] += kick;
}
}
let report = fft_in_place(&mut buf).expect("power-of-two");
fft_power_spectrum(&buf, &mut spec);
total_bit_ops = total_bit_ops.saturating_add(report.bit_ops);
hashes.push(fnv1a_64_f32_slice(&spec));
}
let wall_ns = now_ns() - t0;
// Count unique hashes the cheap way — sort + dedup. 1000 u64s
// is microseconds.
let mut sorted = hashes.clone();
sorted.sort_unstable();
sorted.dedup();
let unique = sorted.len();
let canonical = hashes[0];
// ── Draw the 50 × 20 grid of run cells, coloured by hash
// equivalence class. All matching → solid emerald; divergent
// → coloured by the first-seen index of each unique hash. ──
let w = canvas.width() as f64;
let h = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, w, h);
// Cell layout: 50 cols × 20 rows = 1000 cells.
let cols = 50.0_f64;
let rows = 20.0_f64;
let pad = 1.0_f64;
let cell_w = (w - (cols + 1.0) * pad) / cols;
let cell_h = (h - (rows + 1.0) * pad) / rows;
// Build a hash → palette-index map in observation order so the
// first hash always reads green (the substrate's commitment).
let mut palette_for: std::collections::HashMap<u64, usize> =
std::collections::HashMap::new();
let palette: &[&str] = &[
"#22c55e", // emerald — the canonical hash
"#fb7185", // rose — first divergence
"#fbbf24", // amber — second
"#a78bfa", // violet — third
"#38bdf8", // sky — fourth
"#f472b6", // pink — fifth
"#94a3b8", // slate — overflow
];
for &h_ in &hashes {
if !palette_for.contains_key(&h_) {
let next = palette_for.len().min(palette.len() - 1);
palette_for.insert(h_, next);
}
}
for i in 0..runs_per_frame {
let r = (i / 50) as f64;
let c = (i % 50) as f64;
let x = pad + c * (cell_w + pad);
let y = pad + r * (cell_h + pad);
let idx = *palette_for.get(&hashes[i]).unwrap_or(&0);
ctx.set_fill_style_str(palette[idx]);
ctx.fill_rect(x, y, cell_w, cell_h);
}
// ── Heading overlay: hash + unique count + canonical readout ──
ctx.set_fill_style_str("rgba(10, 14, 24, 0.85)");
let banner_h = 28.0;
ctx.fill_rect(0.0, 0.0, w, banner_h);
ctx.set_fill_style_str("rgba(220, 240, 255, 0.95)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
let label = if unique == 1 {
format!("≡ byte-reproducible · {} / {} unique hash · 0x{:016x}",
unique, runs_per_frame, canonical)
} else {
format!("≠ diverged · {} / {} unique hashes (toggle off to restore)",
unique, runs_per_frame)
};
let _ = ctx.fill_text(&label, 8.0, 18.0);
// ── Emit a receipt carrying the canonical output hash. The
// receipt strip's ≡/≠ indicator reads this directly. ──
let v_class = if perturb { "L1" } else { "L0Closed" };
emit_receipt_with_hash(
&sink,
"fft_256_x1000",
v_class,
runs_per_frame as u64,
wall_ns,
total_bit_ops / runs_per_frame as u64,
canonical,
);
frame_count = frame_count.wrapping_add(1);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── mgai-graph event-log persistence bindings ─────────────────────
//
// Used by /exhibits/persistent-graph. The page holds the mutation log
// in localStorage; on every change it sends the log here and renders
// the (count, count, hash) summary. The "byte-reproducible across
// reloads" claim is: hash(log_replayed_to_graph) is stable, even
// across full page reloads.
#[wasm_bindgen]
pub fn pg_log_summary(log_json: &str) -> Result<JsValue, JsValue> {
let mutations: Vec<mgai_graph::Mutation> = serde_json::from_str(log_json)
.map_err(|e| JsValue::from_str(&format!("log parse: {e}")))?;
let g = mgai_graph::Graph::from_log(&mutations)
.map_err(|e| JsValue::from_str(&format!("log replay: {e}")))?;
let canonical = g
.to_json()
.map_err(|e| JsValue::from_str(&format!("graph serialise: {e}")))?;
// FNV-1a 64-bit over the canonical JSON bytes — same family as
// every other output_hash on the substrate.
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in canonical.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x100_0000_01b3);
}
let result = serde_json::json!({
"nodes": g.node_count(),
"edges": g.edge_count(),
"log_len": mutations.len(),
"state_hash": format!("0x{:016x}", h),
"state_hash_u64": h,
});
serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&format!("{e}")))
}
/// Render the live edge list from a log (as `{from_label, to_label,
/// edge_kind}` triples), used by /exhibits/persistent-graph to draw the
/// current graph state without re-implementing the replay on the JS side.
#[wasm_bindgen]
pub fn pg_edge_list(log_json: &str) -> Result<JsValue, JsValue> {
let mutations: Vec<mgai_graph::Mutation> = serde_json::from_str(log_json)
.map_err(|e| JsValue::from_str(&format!("log parse: {e}")))?;
let g = mgai_graph::Graph::from_log(&mutations)
.map_err(|e| JsValue::from_str(&format!("log replay: {e}")))?;
let edges: Vec<serde_json::Value> = g
.edges()
.filter_map(|e| {
let from_label = g.node(e.from)?.label.clone();
let to_label = g.node(e.to)?.label.clone();
Some(serde_json::json!({
"from": from_label,
"to": to_label,
"kind": e.kind,
}))
})
.collect();
serde_wasm_bindgen::to_value(&edges).map_err(|e| JsValue::from_str(&format!("{e}")))
}
// ── Combined cascade-memory exhibit ───────────────────────────────
//
// Runs BOTH memory paths on the same query so a visitor sees the
// substrate's routing decision live:
//
// • Structural path — cascade::walk fires probe_l0_retrieved which
// tries the mgai-graph typed-edge pattern. Resolves at L0-retrieved
// for queries like "uses softmax" or "HDC uses".
//
// • Similarity path — HDC cosine over the 30-item corpus, like the
// /exhibits/code-memory exhibit.
//
// The canvas renders both side-by-side. The banner at the top reports
// which path the cascade considers the canonical resolution; the
// receipt carries the resolved tier as its V-class and the FNV-1a of
// the combined output as its output_hash.
#[wasm_bindgen]
pub fn mount_cascade_memory(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
use crate::cascade::{walk, CascadeTier, SubExpert, TimeFn};
use crate::modal::encode_text;
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
// Pre-encode corpus once (same 30 items as code-memory).
let encoded: Vec<(String, String, crate::hdc::Hv)> = CODE_MEMORY_CORPUS
.iter()
.map(|(id, text)| {
let report = encode_text(text);
(id.to_string(), text.to_string(), report.hv)
})
.collect();
let encoded = Rc::new(encoded);
let n_items = encoded.len();
let time = TimeFn { now_ns };
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let last_query: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let query = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id("cascade-memory-query"))
.and_then(|el| el.dyn_into::<web_sys::HtmlInputElement>().ok())
.map(|cb| cb.value())
.unwrap_or_default();
let effective_query = if query.trim().is_empty() {
"uses softmax".to_string()
} else {
query.clone()
};
// Only re-render the static parts when the query changes.
let mut last = last_query.borrow_mut();
let _changed = *last != effective_query;
*last = effective_query.clone();
drop(last);
// ── Structural path: cascade walk (which internally tries the
// mgai-graph sub-strategy at L0-retrieved). ──
let t0 = now_ns();
let trace = walk(&effective_query, &time);
let cascade_wall = now_ns() - t0;
// ── Similarity path: HDC cosine over the 30-item corpus. ──
let t1 = now_ns();
let q_report = encode_text(&effective_query);
let mut scores: Vec<(usize, f32)> = encoded
.iter()
.enumerate()
.map(|(i, (_, _, hv))| (i, q_report.hv.cosine(hv)))
.collect();
scores
.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let hdc_wall = now_ns() - t1;
let top3: Vec<(usize, f32)> = scores.iter().take(3).copied().collect();
// Combined output hash: cascade resolved-tier + HDC top-3 indices.
let mut bytes: Vec<u8> = Vec::with_capacity(16);
bytes.push(match trace.resolved_at {
Some(CascadeTier::L0Closed) => 0,
Some(CascadeTier::L0Retrieved) => 1,
Some(CascadeTier::L0Identifiable) => 2,
Some(CascadeTier::L1) => 3,
Some(CascadeTier::L2) => 4,
None => 255,
});
for (i, _) in &top3 {
bytes.extend_from_slice(&(*i as u32).to_le_bytes());
}
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in &bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100_0000_01b3);
}
// ── Render ──
let w = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, w, ch);
// Header
ctx.set_fill_style_str("rgba(220, 240, 255, 0.95)");
ctx.set_font("13px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
&format!("query · \"{}\"", effective_query),
16.0,
24.0,
);
let resolved_str = trace
.resolved_at
.map(|t| t.label())
.unwrap_or("(no L0/L1 match)");
ctx.set_fill_style_str(if trace.resolved_at.is_some() {
"rgba(34, 197, 94, 0.95)"
} else {
"rgba(160, 170, 200, 0.7)"
});
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
&format!("cascade resolved at {} · hash 0x{:016x}", resolved_str, h),
16.0,
42.0,
);
// Split canvas: left half = structural / cascade trace; right = HDC top-3.
let mid_x = w * 0.5;
ctx.set_stroke_style_str("rgba(50, 60, 80, 0.7)");
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.move_to(mid_x, 60.0);
ctx.line_to(mid_x, ch - 30.0);
ctx.stroke();
// ── Left panel: cascade trace (structural path) ──
ctx.set_fill_style_str("rgba(140, 200, 255, 0.85)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
"STRUCTURAL · mgai-graph + cascade",
16.0,
72.0,
);
let mut y = 92.0;
for hop in trace.hops.iter().take(8) {
// Tier badge
let badge_color = match hop.tier {
CascadeTier::L0Closed | CascadeTier::L0Retrieved | CascadeTier::L0Identifiable => {
if hop.matched {
"rgba(34, 197, 94, 0.95)"
} else {
"rgba(100, 110, 130, 0.55)"
}
}
CascadeTier::L1 => "rgba(125, 211, 252, 0.95)",
CascadeTier::L2 => "rgba(251, 191, 36, 0.95)",
};
ctx.set_fill_style_str(badge_color);
ctx.fill_rect(16.0, y - 12.0, 76.0, 20.0);
ctx.set_fill_style_str("rgba(10, 14, 24, 0.95)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(hop.tier.label(), 22.0, y + 2.0);
// Sub-expert + action
ctx.set_fill_style_str("rgba(220, 240, 255, 0.9)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let symbol = match hop.expert {
SubExpert::Reasoning => "?",
SubExpert::Generation => "→",
};
let mark = if hop.matched { "✓" } else { "·" };
let line = format!("{} {} {}", symbol, mark, hop.action);
let line = if line.len() > 48 { format!("{}…", &line[..47]) } else { line };
let _ = ctx.fill_text(&line, 100.0, y + 2.0);
y += 22.0;
if y > ch - 60.0 {
break;
}
}
// Final answer (if any)
if let Some(ans) = trace.final_answer.as_ref() {
ctx.set_fill_style_str("rgba(34, 197, 94, 0.95)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let mut shown = ans.clone();
if shown.len() > 52 {
shown.truncate(52);
shown.push('…');
}
let _ = ctx.fill_text(&format!("answer · {}", shown), 16.0, ch - 40.0);
}
// ── Right panel: HDC top-3 (similarity path) ──
ctx.set_fill_style_str("rgba(140, 200, 255, 0.85)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
"SIMILARITY · HDC cosine over 30 items",
mid_x + 16.0,
72.0,
);
let bar_x = mid_x + 16.0;
let bar_w_max = w - bar_x - 70.0;
let mut y2 = 96.0;
for (rank, (idx, score)) in top3.iter().enumerate() {
let (id, _, _) = &encoded[*idx];
ctx.set_fill_style_str(if rank == 0 {
"rgba(34, 197, 94, 0.95)"
} else {
"rgba(125, 211, 252, 0.85)"
});
ctx.fill_rect(bar_x, y2 - 12.0, bar_w_max * (score.clamp(0.0, 1.0) as f64), 22.0);
ctx.set_fill_style_str("rgba(220, 240, 255, 0.95)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
&format!("{} cos {:+.3}", id, score),
bar_x + 6.0,
y2 + 2.0,
);
y2 += 32.0;
}
// Footer: combined cost summary
ctx.set_fill_style_str("rgba(160, 170, 200, 0.85)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
&format!(
"cascade {:.0}ns · HDC {:.0}ns · corpus {}",
cascade_wall, hdc_wall, n_items
),
mid_x + 16.0,
ch - 16.0,
);
// Receipt: V-class = resolved cascade tier label; bit_ops = sum
// of cascade and HDC paths; output_hash = combined fingerprint.
let v_class = match trace.resolved_at {
Some(CascadeTier::L0Closed) => "L0Closed",
Some(CascadeTier::L0Retrieved) => "L0Retrieved",
Some(CascadeTier::L0Identifiable) => "L0Identifiable",
Some(CascadeTier::L1) => "L1",
Some(CascadeTier::L2) => "L2",
None => "L1",
};
let total_bit_ops = trace.total_bit_ops + q_report.bit_ops
+ (n_items as u64) * (crate::hdc::HDC_DIM as u64);
emit_receipt_with_hash(
&sink,
"cascade_memory_dual_path",
v_class,
1,
cascade_wall + hdc_wall,
total_bit_ops,
h,
);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Code-memory exhibit ──────────────────────────────────────────────
//
// Mathground's response to "give my AI a queryable memory layer over a
// corpus, without an LLM per query". Graphify-shaped surface, HDC-shaped
// substrate: each corpus item is encoded with `modal::encode_text` into
// a 10 000-dim hypervector; query lookup is cosine over the 30-item
// list. The cascade tier reported per query is `L0-retrieved` when the
// top cosine clears a threshold, otherwise `L1` (deferred to a layer
// the exhibit doesn't ship). No model at any tier.
//
// The corpus is small + fixed so the exhibit is self-contained. The
// receipt-grammar shipped this session lets the exhibit publish a
// per-query `output_hash` (fingerprint of the top-K item indices) so a
// consumer can confirm two identical queries returned identical
// rankings.
const CODE_MEMORY_CORPUS: &[(&str, &str)] = &[
("fft", "Cooley-Tukey radix-2 FFT decomposition into log-N butterfly stages"),
("matmul", "Matrix multiplication via inner-product reduction across an axis"),
("attention", "Attention layer multiplies queries with keys, softmaxes, and weights values"),
("rmsnorm", "Root-mean-square normalisation rescales activations element-wise"),
("softmax", "Softmax exponentiates and normalises a vector to sum to one"),
("cosine", "Cosine similarity is the dot product divided by the product of L2 norms"),
("bind", "Bind two bipolar hypervectors with element-wise XOR for compositional memory"),
("bundle", "Bundle multiple hypervectors via sign of their elementwise sum"),
("permute", "Permute a hypervector by cyclic shift to encode positional order"),
("unbind", "Unbind a bound hypervector by binding again with one of the operands"),
("hypervector", "Bipolar plus-or-minus-one vector at ten-thousand dimensions, MAP-VSA style"),
("receipt", "Picojoule receipt: wall time times TDP envelope, against the Landauer floor"),
("landauer", "Landauer floor: kT ln 2 joules per irreversible bit erasure, the physics bound"),
("vclass", "V-class label tags a primitive: L0-closed, L0-retrieved, L1, L2"),
("cascade", "Cascade walker resolves a query left-to-right through tiered experts"),
("escalate", "Escalation to L2 happens only when the deterministic spine cannot place a query"),
("output_hash", "Output hash on a receipt confesses whether the kernel was byte-reproducible"),
("determinism", "Batch-invariant kernels make outputs byte-identical across batch sizes"),
("batch_invariant", "Batch-size variance, not concurrent atomic adds, drives LLM nondeterminism"),
("byte_reproducible", "Two runs of the same primitive that hash identically are byte-reproducible"),
("knowledge_graph", "Knowledge graph: typed edges between entity nodes with community labels"),
("graph_traversal", "Graph traversal walks edges from a source node breadth or depth first"),
("leiden", "Leiden clustering partitions a graph into communities by modularity refinement"),
("tree_sitter", "Tree-sitter parses source code into a typed concrete syntax tree"),
("embedding", "Embedding maps a token or chunk into a learned dense vector representation"),
("retrieval", "Retrieval picks top-K corpus items by similarity to a query embedding"),
("hdc", "Hyperdimensional computing composes meaning by binding and bundling high-dim vectors"),
("omnimodal", "Four deterministic encoders project text, image, audio, video to one HDC space"),
("cosine_matrix", "Cross-modal cosine matrix is the substrate property of omnimodal routing"),
("anytime_valid", "Anytime-valid evidence: an e-process stays a martingale across stopping times"),
];
#[wasm_bindgen]
pub fn mount_code_memory(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
use crate::modal::encode_text;
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
// ── Pre-encode the corpus once ──
let encoded: Vec<(String, String, crate::hdc::Hv, u64)> = CODE_MEMORY_CORPUS
.iter()
.map(|(id, text)| {
let report = encode_text(text);
(id.to_string(), text.to_string(), report.hv, report.bit_ops)
})
.collect();
let n_items = encoded.len();
let encoded = Rc::new(encoded);
let sink = sink.clone();
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let last_query: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
let last_ranking: Rc<RefCell<Option<(Vec<usize>, Vec<f32>, f64, u64, u64)>>> =
Rc::new(RefCell::new(None));
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let query = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id("code-memory-query"))
.and_then(|el| el.dyn_into::<web_sys::HtmlInputElement>().ok())
.map(|cb| cb.value())
.unwrap_or_default();
let effective_query = if query.trim().is_empty() {
"compose hypervectors".to_string()
} else {
query.clone()
};
let mut last = last_query.borrow_mut();
let needs_recompute = *last != effective_query;
if needs_recompute {
*last = effective_query.clone();
drop(last);
let t0 = now_ns();
let q_report = encode_text(&effective_query);
let mut scores: Vec<(usize, f32)> = encoded
.iter()
.enumerate()
.map(|(i, (_, _, hv, _))| (i, q_report.hv.cosine(hv)))
.collect();
scores.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let wall_ns = now_ns() - t0;
let top_k = 5usize;
let top_idx: Vec<usize> = scores.iter().take(top_k).map(|(i, _)| *i).collect();
let top_scores: Vec<f32> = scores.iter().take(top_k).map(|(_, s)| *s).collect();
let mut bytes: Vec<u8> = Vec::with_capacity(top_k * 4);
for &i in &top_idx {
bytes.extend_from_slice(&(i as u32).to_le_bytes());
}
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in &bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100_0000_01b3);
}
let total_bit_ops = q_report.bit_ops + (n_items as u64) * (crate::hdc::HDC_DIM as u64);
*last_ranking.borrow_mut() =
Some((top_idx, top_scores, wall_ns, total_bit_ops, h));
} else {
drop(last);
}
let w = canvas.width() as f64;
let h_c = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, w, h_c);
ctx.set_fill_style_str("rgba(220, 240, 255, 0.95)");
ctx.set_font("13px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
&format!("query · \"{}\"", effective_query),
16.0,
24.0,
);
ctx.set_fill_style_str("rgba(140, 200, 255, 0.7)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
"substrate · 30 hypervectors · cosine query · top-5 retrieved",
16.0,
42.0,
);
let ranking = last_ranking.borrow();
if let Some((top_idx, top_scores, wall_ns, total_bit_ops, h_hash)) = ranking.as_ref() {
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
let row_h = 52.0;
let pad_x = 16.0;
let bar_x = 240.0;
let bar_max = w - bar_x - 70.0;
let y0 = 70.0;
for (rank, (&idx, &score)) in top_idx.iter().zip(top_scores.iter()).enumerate() {
let (id, text, _, _) = &encoded[idx];
let row_y = y0 + rank as f64 * row_h;
ctx.set_fill_style_str(if rank == 0 {
"rgba(34, 197, 94, 0.95)"
} else {
"rgba(160, 170, 200, 0.45)"
});
ctx.fill_rect(pad_x, row_y + 4.0, 24.0, 24.0);
ctx.set_fill_style_str("rgba(10, 14, 24, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(&format!("{}", rank + 1), pad_x + 8.0, row_y + 20.0);
ctx.set_fill_style_str("rgba(220, 240, 255, 0.95)");
let _ = ctx.fill_text(id, pad_x + 36.0, row_y + 16.0);
ctx.set_fill_style_str("rgba(160, 180, 210, 0.85)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let body = if text.len() > 36 { &text[..36] } else { text.as_str() };
let _ = ctx.fill_text(body, pad_x + 36.0, row_y + 32.0);
ctx.set_fill_style_str("rgba(40, 50, 70, 0.8)");
ctx.fill_rect(bar_x, row_y + 14.0, bar_max, 12.0);
let pct = score.clamp(0.0, 1.0) as f64;
ctx.set_fill_style_str(if rank == 0 {
"rgba(34, 197, 94, 0.95)"
} else {
"rgba(125, 211, 252, 0.85)"
});
ctx.fill_rect(bar_x, row_y + 14.0, bar_max * pct, 12.0);
ctx.set_fill_style_str("rgba(220, 240, 255, 0.95)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
&format!("cos {:+.3}", score),
bar_x + bar_max + 8.0,
row_y + 24.0,
);
}
let top_score = top_scores[0];
let resolved_tier = if top_score > 0.15 { "L0-retrieved" } else { "L1 (defer)" };
ctx.set_fill_style_str("rgba(160, 180, 210, 0.85)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let _ = ctx.fill_text(
&format!(
"cascade · resolved at {} · hash 0x{:016x}",
resolved_tier, h_hash
),
16.0,
h_c - 16.0,
);
let v_class = if top_score > 0.15 { "L0Retrieved" } else { "L1" };
emit_receipt_with_hash(
&sink,
"code_memory_cosine_topk",
v_class,
1,
*wall_ns,
*total_bit_ops,
*h_hash,
);
}
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Active-inference exhibit ─────────────────────────────────────────
//
// A closed-loop Friston/FEP demo: a 2-state, 2-obs, 2-action discrete
// agent drops into a deterministic environment, observes, updates its
// posterior via variational inference, scores both single-step
// policies by expected free energy, picks the lower-EFE action, and
// the environment transitions accordingly. Every frame is one full
// observe → plan → act → step round trip; the per-step VFE + EFE bar
// chart + posterior + action are drawn on the canvas, and a receipt
// is emitted to the page-side sink.
/// Mount the active-inference exhibit on `canvas_id`. Receipts emit
/// with `v_class = "L0Closed"` because the math is fully closed-form
/// inside the substrate — no model fires, no retrieval happens. Per
/// frame ≈ a few hundred FMAs over the 2-state space.
#[wasm_bindgen]
pub fn mount_active_inference(
canvas_id: &str,
sink: &js_sys::Function,
) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
// Build the canonical two-state model (same shape as the test
// fixture in mgai_active_inference): obs 0 ↔ state 0, obs 1 ↔
// state 1; action 0 = stay, action 1 = swap; the agent prefers
// obs 1 (C[1] = 2.0).
let model = AiModel::new(
vec![vec![0.9, 0.1], vec![0.1, 0.9]],
vec![
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
vec![vec![0.0, 1.0], vec![1.0, 0.0]],
],
vec![0.0, 2.0],
vec![0.5, 0.5],
)
.map_err(|e| JsValue::from_str(&format!("model: {e}")))?;
let agent = Rc::new(RefCell::new(AiAgent::new(model)));
let env_state = Rc::new(RefCell::new(0usize));
let frame_counter = Rc::new(RefCell::new(0u32));
let cumulative_uj = Rc::new(RefCell::new(0u64));
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let ag = agent.clone();
let es = env_state.clone();
let fc = frame_counter.clone();
let cu = cumulative_uj.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
let mut a = ag.borrow_mut();
let mut es_b = es.borrow_mut();
let obs = *es_b;
let step = match a.step(obs, 1) {
Ok(s) => s,
Err(_) => {
schedule(f.borrow().as_ref().unwrap());
return;
}
};
let action = step.action;
if action == 1 {
*es_b = 1 - *es_b;
}
drop(es_b);
// Title.
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("16px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("active inference · 2-state world", 28.0, 32.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
"observe → posterior update (vfe) → score policies (efe) → act",
28.0,
52.0,
)
.ok();
ctx.set_fill_style_str("rgba(120, 200, 255, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("obs = {obs}"), 28.0, 90.0).ok();
ctx.set_fill_style_str(if action == 0 {
"rgba(200, 200, 200, 1)"
} else {
"rgba(255, 200, 120, 1)"
});
let action_label = if action == 0 { "stay" } else { "swap" };
ctx.fill_text(&format!("action = {action} ({action_label})"), 140.0, 90.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(
&format!("vfe = {:.3} nats", step.observe.vfe_nats),
300.0,
90.0,
)
.ok();
// Posterior q(s) bars.
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("posterior q(s) — variational FE minimum", 28.0, 130.0).ok();
for (i, &p) in step.observe.posterior.iter().enumerate() {
let y = 145.0 + (i as f64) * 28.0;
ctx.set_fill_style_str("rgba(40, 50, 70, 1)");
ctx.fill_rect(120.0, y, 260.0, 18.0);
ctx.set_fill_style_str("rgba(120, 200, 255, 1)");
ctx.fill_rect(120.0, y, 260.0 * (p as f64).max(0.0).min(1.0), 18.0);
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.fill_text(&format!("s={i}"), 28.0, y + 13.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(&format!("{p:.3}"), 390.0, y + 13.0).ok();
}
// EFE per policy bars.
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text("EFE per policy — lower is better", 28.0, 230.0).ok();
let efe = &step.plan.efe_nats;
let efe_min = efe.iter().cloned().fold(f32::INFINITY, f32::min);
let efe_max = efe.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let span = (efe_max - efe_min).max(1e-6);
for (i, (policy, &g_pi)) in step.plan.policies.iter().zip(efe.iter()).enumerate() {
let y = 245.0 + (i as f64) * 28.0;
let label = if policy[0] == 0 { "[stay]" } else { "[swap]" };
let norm = (g_pi - efe_min) / span;
let w = 260.0 * (1.0 - norm as f64).max(0.05);
ctx.set_fill_style_str("rgba(40, 50, 70, 1)");
ctx.fill_rect(120.0, y, 260.0, 18.0);
let bar_color = if i == step.plan.best {
"rgba(150, 255, 180, 1)"
} else {
"rgba(120, 140, 160, 1)"
};
ctx.set_fill_style_str(bar_color);
ctx.fill_rect(120.0, y, w, 18.0);
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.fill_text(label, 28.0, y + 13.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(&format!("{g_pi:.3}"), 390.0, y + 13.0).ok();
}
// Cumulative footer.
let frame = {
let mut fr = fc.borrow_mut();
*fr += 1;
*fr
};
let total_uj = {
let mut t = cu.borrow_mut();
*t += step.energy_uj;
*t
};
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
&format!("step {frame} · bit-ops {} · {} µJ", step.bit_ops, step.energy_uj),
28.0,
ch - 36.0,
)
.ok();
ctx.fill_text(
&format!("cumulative: {total_uj} µJ over {frame} steps"),
28.0,
ch - 18.0,
)
.ok();
// L0-closed receipt: pure math, no model fires.
let wall_ns_per_op = 0.5;
emit_receipt(
&sink,
"active_inference_step",
"L0Closed",
1,
(step.bit_ops as f64) * wall_ns_per_op,
step.bit_ops,
);
// Cycle every ~120 steps so the page never lingers on a steady
// state.
if frame % 120 == 0 {
*fc.borrow_mut() = 0;
*cu.borrow_mut() = 0;
a.reset();
*es.borrow_mut() = 0;
}
drop(a);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Constrained-design exhibit (mgai-cea + mgai-voxel) ────────────
//
// A continuously-searching parametric design problem rendered on the
// canvas: a sphere whose centre/radius are the parameter vector, three
// constraints (max-mass, build-volume fit, min wall thickness), and a
// minimize-volume objective. Each animation frame runs a few search
// steps and redraws the cross-section, the constraint check marks,
// and the objective trajectory. Substrate's direct-to-artifact
// discipline is visible: there's no text generation in the loop —
// just numbers descending under deterministic search until the design
// is feasible.
// Thread-local handle on the latest constrained-design geometry, so
// JS can pull a printable STL after the search has settled. WASM is
// single-threaded so `thread_local!` is fine; the substrate
// discipline is "direct-to-artifact" → this is the artifact.
thread_local! {
static LATEST_CONSTRAINED_GEOMETRY: RefCell<Option<VoxelGrid>> = const { RefCell::new(None) };
}
/// Polygonise the current constrained-design geometry (the most
/// recent frame's `VoxelGrid`) and return its binary STL bytes. JS
/// glues this into a Blob URL + click-download. Returns an empty
/// array if no frame has rendered yet.
#[wasm_bindgen]
pub fn export_constrained_design_stl() -> Vec<u8> {
LATEST_CONSTRAINED_GEOMETRY.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
None => Vec::new(),
Some(g) => {
let (mesh, _) = mgai_voxel::mesh::polygonise(g, 0.0);
let (welded, _) = mesh.weld(1e-4);
welded.to_stl_binary()
}
}
})
}
#[wasm_bindgen]
pub fn mount_constrained_design(
canvas_id: &str,
sink: &js_sys::Function,
) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
// Build the problem fresh inside an Rc/RefCell so the rAF closure
// can mutate it on each frame.
fn build_problem()
-> ConstrainedProblem<ParametricSphere, Box<dyn Constraint<VoxelGrid>>, MinimizeVolume> {
let bounds = VoxAabb::new(VoxVec3::new(-1.5, -1.5, -1.5), VoxVec3::new(1.5, 1.5, 1.5));
let design = ParametricSphere::new(bounds, 0.12, VoxVec3::new(0.7, 0.7, 0.0), 0.95);
let limits = VoxAabb::new(VoxVec3::new(-0.5, -0.5, -0.5), VoxVec3::new(0.5, 0.5, 0.5));
let cs: Vec<Box<dyn Constraint<VoxelGrid>>> = vec![
Box::new(MaxMass { limit: 2.0, density: 1.0, model_bias: 1.0 }),
Box::new(mgai_cea::WithinAabb { limits }),
Box::new(mgai_cea::MinWallThickness { min_thickness: 0.05 }),
];
ConstrainedProblem::new(design, cs, MinimizeVolume).unwrap()
}
let problem = Rc::new(RefCell::new(build_problem()));
let cumulative_uj = Rc::new(RefCell::new(0u64));
let frame_counter = Rc::new(RefCell::new(0u32));
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let pb = problem.clone();
let cu = cumulative_uj.clone();
let fc = frame_counter.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
// Run one search burst per frame (4 steps × 4 neighbours so
// progress is visible without crushing the canvas thread).
let params = SearchParams {
neighbours_per_step: 4,
step_size: 0.05,
max_steps: 4,
seed: 0xC0DE_F00D,
};
let report;
{
let mut p = pb.borrow_mut();
let (_score, r) = match cea_search(&mut *p, params) {
Ok(v) => v,
Err(_) => {
schedule(f.borrow().as_ref().unwrap());
return;
}
};
report = r;
}
let score = match pb.borrow().score() {
Ok(s) => s,
Err(_) => {
schedule(f.borrow().as_ref().unwrap());
return;
}
};
// Title.
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("16px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("constrained design — parametric sphere", 28.0, 32.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
"voxel kernel + rule encoder + deterministic local search",
28.0,
52.0,
)
.ok();
// ── Cross-section: slice through z=0 of the voxel grid. ──
let grid = &score.geometry;
let slice_z = grid.size[2] / 2;
let slice_w = grid.size[0];
let slice_h = grid.size[1];
let cell_px = 4.0_f64;
let slice_x0 = 28.0;
let slice_y0 = 90.0;
for iy in 0..slice_h {
for ix in 0..slice_w {
let i = ((slice_z * slice_h + iy) * slice_w + ix) as usize;
let v = grid.data[i];
// Map signed distance to a colour band: inside =
// accent, outside = dim.
if v < 0.0 {
let t = (-v).min(0.4) / 0.4;
let r_byte = (40.0 + 80.0 * t as f64) as u8;
let g_byte = (160.0 + 60.0 * t as f64) as u8;
let b_byte = (255.0 - 30.0 * t as f64) as u8;
let style = format!("rgba({r_byte}, {g_byte}, {b_byte}, 1)");
ctx.set_fill_style_str(&style);
} else {
let t = v.min(0.6) / 0.6;
let alpha = 0.18 - 0.12 * t as f64;
let style = format!("rgba(80, 100, 130, {alpha:.3})");
ctx.set_fill_style_str(&style);
}
let x = slice_x0 + (ix as f64) * cell_px;
let y = slice_y0 + (iy as f64) * cell_px;
ctx.fill_rect(x, y, cell_px, cell_px);
}
}
// Cross-section legend.
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
"slice z = 0 · cyan = solid, faint = outside",
slice_x0,
slice_y0 + (slice_h as f64) * cell_px + 16.0,
)
.ok();
// ── Constraints column. ──
let cx0 = slice_x0 + (slice_w as f64) * cell_px + 36.0;
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("constraints", cx0, 110.0).ok();
let mut cy = 130.0;
for cr in &score.constraints {
ctx.set_fill_style_str(if cr.satisfied {
"rgba(150, 255, 180, 1)"
} else {
"rgba(255, 150, 150, 1)"
});
ctx.set_font("13px ui-monospace, SFMono-Regular, Menlo, monospace");
let mark = if cr.satisfied { "✓" } else { "✗" };
ctx.fill_text(&format!("{mark} {}", cr.name), cx0, cy).ok();
ctx.set_fill_style_str("rgba(140, 150, 170, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
&format!("violation {:.3}", cr.violation),
cx0 + 16.0,
cy + 14.0,
)
.ok();
cy += 36.0;
}
// ── Parameters + objective + footer. ──
let pcx0 = cx0;
let mut py = cy + 12.0;
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("parameters", pcx0, py).ok();
py += 16.0;
let p_vals = pb.borrow().design.parameters();
let names = ["cx", "cy", "cz", "r "];
for (n, v) in names.iter().zip(p_vals.iter()) {
ctx.set_fill_style_str("rgba(220, 220, 240, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("{n} = {v:.3}"), pcx0, py).ok();
py += 16.0;
}
py += 8.0;
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text("objective", pcx0, py).ok();
py += 16.0;
ctx.set_fill_style_str("rgba(220, 220, 240, 1)");
ctx.fill_text(
&format!("{} = {:.4}", score.objective.name, score.objective.value),
pcx0,
py,
)
.ok();
py += 16.0;
ctx.fill_text(
&format!("total_score = {:.4}", score.total_score),
pcx0,
py,
)
.ok();
// Cumulative + feasibility footer.
let frame = {
let mut fr = fc.borrow_mut();
*fr += 1;
*fr
};
let total_uj = {
let mut t = cu.borrow_mut();
*t += report.energy_uj;
*t
};
ctx.set_fill_style_str(if score.is_feasible() {
"rgba(150, 255, 180, 1)"
} else {
"rgba(255, 200, 120, 1)"
});
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
let status = if score.is_feasible() {
"FEASIBLE — search converged"
} else {
"searching — penalty active"
};
ctx.fill_text(status, 28.0, ch - 36.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(
&format!(
"burst {} · this-burst {} bit-ops · cumulative {} µJ",
frame, report.bit_ops, total_uj
),
28.0,
ch - 18.0,
)
.ok();
// Emit a receipt — L0-closed math, no model fires.
let wall_ns_per_op = 0.5;
emit_receipt(
&sink,
"constrained_design_burst",
"L0Closed",
1,
(report.bit_ops as f64) * wall_ns_per_op,
report.bit_ops,
);
// Publish the current geometry so JS can pull a printable STL
// any time the user clicks "download .stl". Cloning a 30³ grid
// is ~108K f32 = ~432 KB — cheap once per frame; no realloc
// because the thread-local holds a single VoxelGrid slot.
LATEST_CONSTRAINED_GEOMETRY.with(|cell| {
*cell.borrow_mut() = Some(score.geometry.clone());
});
// Cycle every ~240 bursts so the page never sits on a single
// converged result.
if frame > 240 {
*fc.borrow_mut() = 0;
*cu.borrow_mut() = 0;
*pb.borrow_mut() = build_problem();
}
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── PCs/NPCs spine exhibit ────────────────────────────────────────
//
// Visualises the substrate's architectural primitive: a deterministic
// spine handles the common case in picojoules; the leaf only fires
// when the spine returns Unresolved; the verifier gates every leaf
// proposal. The exhibit cycles through six representative inputs that
// hit each branch (spine hit, leaf accepted, leaf rejected, no leaf)
// so visitors can see the full state machine + receipt accounting.
use mgai_spine::gate::PredictiveGate;
use mgai_spine::{
AlwaysProposes, Compartment, EscalationPolicy, Leaf, Outcome, PcsNpcsRunner,
SpineResult, SpineVerifier, VerificationResult,
};
// Toy compartment: cached squares. The exhibit's spine knows 0² … 4².
// Inputs outside that range force escalation.
struct SquaresSpine;
impl Compartment for SquaresSpine {
type Input = u32;
type Output = u32;
fn name(&self) -> &str { "squares_cache" }
fn evaluate(&self, input: &u32) -> (SpineResult<u32>, u64) {
if *input <= 4 {
(SpineResult::Resolved(input * input), 64)
} else {
(
SpineResult::Unresolved {
reason: format!("not in cache: {input}"),
},
64,
)
}
}
}
// Toy verifier: accept iff proposed == input * input.
struct SquaresVerifier;
impl SpineVerifier<SquaresSpine> for SquaresVerifier {
fn name(&self) -> &str { "squares_check" }
fn verify(&self, input: &u32, proposed: &u32) -> (VerificationResult, u64) {
if *proposed == input * input {
(VerificationResult::Accept, 32)
} else {
(
VerificationResult::Reject {
reason: format!("proposed {proposed}, expected {}", input * input),
},
32,
)
}
}
}
#[wasm_bindgen]
pub fn mount_spine(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
// Six inputs that exercise every Outcome:
// 0, 1 → spine hits (cached)
// 5 → spine miss, leaf proposes 25 (correct) → LeafVerified
// 6 → spine miss, leaf proposes 36 (correct) → LeafVerified
// 7 → spine miss, "wrong-leaf" path → LeafRejected
// 8 → spine miss, no-leaf path → NoLeaf
const INPUTS: &[(u32, &str)] = &[
(0, "spine"),
(1, "spine"),
(5, "leaf"),
(6, "leaf"),
(7, "wrong"),
(8, "none"),
];
let cursor = Rc::new(RefCell::new(0usize));
let cumulative_uj = Rc::new(RefCell::new(0u64));
let frame_counter = Rc::new(RefCell::new(0u32));
// Pro2Guard-style predictive gate: learns a DTMC over the spine's
// own outcomes; pre-flight blocks the leaf when (risk − ε) clears
// the threshold with enough evidence behind it.
let gate = Rc::new(RefCell::new(PredictiveGate::new(0.5).with_min_evidence(40)));
let skipped_total = Rc::new(RefCell::new(0u64));
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let cur = cursor.clone();
let cu = cumulative_uj.clone();
let fc = frame_counter.clone();
let gt = gate.clone();
let sk = skipped_total.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
// Advance one input per ~30 frames so each is readable.
let frame = {
let mut fr = fc.borrow_mut();
*fr += 1;
*fr
};
if frame % 30 == 0 {
let mut c = cur.borrow_mut();
*c = (*c + 1) % INPUTS.len();
}
let (input, mode) = INPUTS[*cur.borrow()];
// Build the appropriate runner for this input's mode.
let (outcome_label, outcome_color, spine_ops, leaf_ops, verifier_ops, energy_uj, output) =
match mode {
"spine" => {
let runner: PcsNpcsRunner<_, AlwaysProposes<SquaresSpine>, _> =
PcsNpcsRunner::new(
SquaresSpine,
Some(AlwaysProposes { value: input * input, bit_ops: 800 }),
SquaresVerifier,
);
match runner.run(&input) {
Ok((o, r)) => (
outcome_text(r.outcome),
outcome_rgba(r.outcome),
r.spine_bit_ops,
r.leaf_bit_ops,
r.verifier_bit_ops,
r.energy_uj,
Some(o),
),
Err(_) => unreachable!(),
}
}
"leaf" => {
let runner = PcsNpcsRunner::new(
SquaresSpine,
Some(AlwaysProposes::<SquaresSpine> {
value: input * input,
bit_ops: 800,
}),
SquaresVerifier,
);
match runner.run(&input) {
Ok((o, r)) => (
outcome_text(r.outcome),
outcome_rgba(r.outcome),
r.spine_bit_ops,
r.leaf_bit_ops,
r.verifier_bit_ops,
r.energy_uj,
Some(o),
),
Err(_) => unreachable!(),
}
}
"wrong" => {
let runner = PcsNpcsRunner::new(
SquaresSpine,
Some(AlwaysProposes::<SquaresSpine> { value: 999, bit_ops: 800 }),
SquaresVerifier,
);
match runner.run(&input) {
Err((_, r)) => (
outcome_text(r.outcome),
outcome_rgba(r.outcome),
r.spine_bit_ops,
r.leaf_bit_ops,
r.verifier_bit_ops,
r.energy_uj,
None,
),
Ok(_) => unreachable!(),
}
}
_ => {
// "none": no leaf configured.
let runner: PcsNpcsRunner<_, AlwaysProposes<SquaresSpine>, _> =
PcsNpcsRunner::new(SquaresSpine, None, SquaresVerifier)
.with_escalation(EscalationPolicy::OnlyOnUnresolved);
match runner.run(&input) {
Err((_, r)) => (
outcome_text(r.outcome),
outcome_rgba(r.outcome),
r.spine_bit_ops,
r.leaf_bit_ops,
r.verifier_bit_ops,
r.energy_uj,
None,
),
Ok(_) => unreachable!(),
}
}
};
// Predictive gate: preflight BEFORE this call would escalate,
// then record the actual outcome into the DTMC. When the gate
// says "don't escalate" and this input would have called the
// leaf, that leaf cost is counted as saved.
let decision = gt.borrow().preflight();
let would_escalate = leaf_ops > 0;
if !decision.escalate && would_escalate {
*sk.borrow_mut() += leaf_ops;
}
{
let outcome_enum = match outcome_label {
"Spine" => Outcome::Spine,
"LeafVerified" => Outcome::LeafVerified,
"LeafRejected" => Outcome::LeafRejected,
_ => Outcome::NoLeaf,
};
gt.borrow_mut().record(outcome_enum);
}
// ── Draw ──
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("16px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("PCs/NPCs spine — Compartment + Leaf + Verifier", 28.0, 32.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
"spine resolves cheap · leaf only fires on miss · verifier gates every proposal",
28.0,
52.0,
)
.ok();
// Input + outcome
ctx.set_fill_style_str("rgba(220, 220, 240, 1)");
ctx.set_font("14px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("input = {input}"), 28.0, 100.0).ok();
ctx.set_fill_style_str(outcome_color);
ctx.fill_text(&format!("outcome: {outcome_label}"), 200.0, 100.0).ok();
if let Some(v) = output {
ctx.set_fill_style_str("rgba(220, 220, 240, 1)");
ctx.fill_text(&format!("output = {v}"), 460.0, 100.0).ok();
}
// Three-bar receipt: spine, leaf, verifier
let bars = [
("spine", spine_ops, "rgba(120, 200, 255, 1)"),
("leaf", leaf_ops, "rgba(255, 200, 120, 1)"),
("verifier", verifier_ops, "rgba(180, 255, 180, 1)"),
];
let max_ops = bars.iter().map(|b| b.1).max().unwrap_or(1).max(1) as f64;
let chart_x = 80.0;
let chart_w = 360.0;
let mut chart_y = 150.0;
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("bit-ops breakdown per call", 28.0, chart_y - 10.0).ok();
for (label, ops, color) in bars.iter() {
ctx.set_fill_style_str("rgba(40, 50, 70, 1)");
ctx.fill_rect(chart_x, chart_y, chart_w, 18.0);
let frac = (*ops as f64) / max_ops;
ctx.set_fill_style_str(color);
ctx.fill_rect(chart_x, chart_y, chart_w * frac, 18.0);
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.fill_text(label, 28.0, chart_y + 13.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(&format!("{ops}"), chart_x + chart_w + 12.0, chart_y + 13.0).ok();
chart_y += 28.0;
}
// Gate panel — the predictive pre-flight layer.
let gate_y = ch - 92.0;
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("predictive gate (DTMC over outcomes · Hoeffding-bounded)", 28.0, gate_y).ok();
let risk_txt = match decision.predicted_rejection_risk {
Some(r) => format!(
"P(reject) = {:.2} ± {:.2} · evidence {} · {}",
r,
decision.confidence_half_width,
decision.evidence,
if decision.escalate { "escalation allowed" } else { "LEAF CALL BLOCKED" }
),
None => "cold start — always escalates".to_string(),
};
ctx.set_fill_style_str(if decision.escalate {
"rgba(150, 255, 180, 1)"
} else {
"rgba(255, 200, 120, 1)"
});
ctx.fill_text(&risk_txt, 28.0, gate_y + 16.0).ok();
let saved = *sk.borrow();
if saved > 0 {
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(
&format!("leaf bit-ops saved by pre-flight blocks so far: {saved}"),
28.0,
gate_y + 32.0,
)
.ok();
}
// Footer
let total_uj = {
let mut t = cu.borrow_mut();
*t += energy_uj;
*t
};
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
&format!("this call: {} bit-ops · {} µJ", spine_ops + leaf_ops + verifier_ops, energy_uj),
28.0,
ch - 36.0,
)
.ok();
ctx.fill_text(
&format!("cumulative: {total_uj} µJ over {frame} frames"),
28.0,
ch - 18.0,
)
.ok();
// Emit a receipt. v_class reflects the outcome.
let v_class = if outcome_label == "Spine" { "L0Closed" } else { "L1" };
let total_ops = spine_ops + leaf_ops + verifier_ops;
emit_receipt(
&sink,
"spine_runner_call",
v_class,
1,
(total_ops as f64) * 0.5,
total_ops,
);
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
fn outcome_text(o: Outcome) -> &'static str {
match o {
Outcome::Spine => "Spine",
Outcome::LeafVerified => "LeafVerified",
Outcome::LeafRejected => "LeafRejected",
Outcome::NoLeaf => "NoLeaf",
}
}
fn outcome_rgba(o: Outcome) -> &'static str {
match o {
Outcome::Spine => "rgba(120, 200, 255, 1)",
Outcome::LeafVerified => "rgba(150, 255, 180, 1)",
Outcome::LeafRejected => "rgba(255, 150, 150, 1)",
Outcome::NoLeaf => "rgba(255, 200, 120, 1)",
}
}
// ── Structure-learning exhibit (AXIOM ingestion live) ─────────────
//
// A StructureLearner drops into a regime-shifting observation stream
// with ONE hidden state and grows its own state space as regimes
// appear: Dirichlet conjugate count updates, threshold expansion,
// log-Beta Bayesian-model-reduction merges. Zero gradients, zero
// sampling — every event on the canvas is closed-form and replays
// bit-identically from the same stream.
#[wasm_bindgen]
pub fn mount_structure_learning(
canvas_id: &str,
sink: &js_sys::Function,
) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
const N_OBS: usize = 3;
// Regime schedule: blocks of a dominant observation with a pinch
// of off-regime noise (deterministic: every 11th step is off by
// one). Three regimes cycle.
const BLOCK: u32 = 90;
let learner = Rc::new(RefCell::new(
StructureLearner::new(N_OBS, 1, 1.0)
.with_expansion_threshold(0.45)
.with_merge_every(150),
));
let frame_counter = Rc::new(RefCell::new(0u32));
let cumulative_uj = Rc::new(RefCell::new(0u64));
// Rolling event log: (frame, label). Holds the last 6 events.
let events = Rc::new(RefCell::new(Vec::<(u32, String)>::new()));
// State-count history for the sparkline (one sample per frame).
let history = Rc::new(RefCell::new(Vec::<u32>::new()));
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let lr = learner.clone();
let fc = frame_counter.clone();
let cu = cumulative_uj.clone();
let ev = events.clone();
let hi = history.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
let frame = {
let mut fr = fc.borrow_mut();
*fr += 1;
*fr
};
// Deterministic regime-shifting observation.
let regime = ((frame / BLOCK) as usize) % N_OBS;
let obs = if frame % 11 == 0 { (regime + 1) % N_OBS } else { regime };
let receipt_data = {
let mut l = lr.borrow_mut();
match l.step(0, obs) {
Ok(r) => r,
Err(_) => {
schedule(f.borrow().as_ref().unwrap());
return;
}
}
};
if let Some(idx) = receipt_data.grew {
ev.borrow_mut().push((frame, format!("grew state {idx}")));
}
if let Some((kept, gone)) = receipt_data.merged {
ev.borrow_mut()
.push((frame, format!("BMR merged {gone} → {kept}")));
}
{
let mut e = ev.borrow_mut();
let drop_n = e.len().saturating_sub(6);
if drop_n > 0 {
e.drain(0..drop_n);
}
}
hi.borrow_mut().push(receipt_data.n_states as u32);
// ── Draw ──
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("16px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("structure learning — the model grows its own state space", 28.0, 32.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
"Dirichlet counts + threshold expansion + log-Beta BMR merge · no gradients, no sampling",
28.0,
52.0,
)
.ok();
// Regime + observation banner.
ctx.set_fill_style_str("rgba(120, 200, 255, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("regime {regime} · obs {obs}"), 28.0, 86.0).ok();
ctx.set_fill_style_str("rgba(150, 255, 180, 1)");
ctx.fill_text(&format!("hidden states: {}", receipt_data.n_states), 220.0, 86.0).ok();
// State-count sparkline.
let hist = hi.borrow();
let spark_x = 28.0;
let spark_y = 104.0;
let spark_w = cw - 56.0;
let spark_h = 40.0;
ctx.set_fill_style_str("rgba(40, 50, 70, 0.6)");
ctx.fill_rect(spark_x, spark_y, spark_w, spark_h);
let max_states = hist.iter().copied().max().unwrap_or(1).max(1) as f64;
let n_hist = hist.len().max(1) as f64;
ctx.set_fill_style_str("rgba(120, 200, 255, 1)");
for (i, &s) in hist.iter().enumerate() {
let x = spark_x + (i as f64 / n_hist) * spark_w;
let h = (s as f64 / max_states) * (spark_h - 4.0);
ctx.fill_rect(x, spark_y + spark_h - h, (spark_w / n_hist).max(1.0), h);
}
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("state count over time", spark_x, spark_y + spark_h + 14.0).ok();
drop(hist);
// Likelihood heatmap A[o][s] from the learned counts.
let l = lr.borrow();
let n_states = l.model.n_states;
let cell = 34.0;
let hm_x = 28.0;
let hm_y = 192.0;
// Clamp displayed columns so the heatmap never collides with
// the event log at cw-280 (defensive; growth is bounded by the
// data-point seeding fix, but the canvas must not rely on it).
let max_cols = (((cw - 280.0 - 40.0 - hm_x) / (cell + 3.0)).floor() as usize).max(1);
let shown_states = n_states.min(max_cols);
for o in 0..N_OBS {
for s in 0..shown_states {
let col_sum: f64 = (0..N_OBS).map(|oo| l.model.a_counts[oo][s]).sum();
let p = (l.model.a_counts[o][s] / col_sum.max(1e-12)) as f64;
let g_byte = (60.0 + 180.0 * p) as u8;
ctx.set_fill_style_str(&format!("rgba(40, {g_byte}, 220, {:.2})", 0.25 + 0.75 * p));
ctx.fill_rect(hm_x + s as f64 * (cell + 3.0), hm_y + o as f64 * (cell + 3.0), cell, cell);
ctx.set_fill_style_str("rgba(255,255,255,0.9)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
&format!("{:.2}", p),
hm_x + s as f64 * (cell + 3.0) + 5.0,
hm_y + o as f64 * (cell + 3.0) + 20.0,
)
.ok();
}
}
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
let hm_label = if shown_states < n_states {
format!("learned P(obs | state) — showing {shown_states} of {n_states} states")
} else {
"learned P(obs | state) — rows obs, cols states".to_string()
};
ctx.fill_text(&hm_label, hm_x, hm_y + 3.0 * (cell + 3.0) + 14.0).ok();
// Posterior bars.
let post_x = hm_x;
let post_y = hm_y + 3.0 * (cell + 3.0) + 34.0;
let max_bars = 8usize;
for (s, &p) in receipt_data.posterior.iter().take(max_bars).enumerate() {
let y = post_y + s as f64 * 20.0;
ctx.set_fill_style_str("rgba(40, 50, 70, 1)");
ctx.fill_rect(post_x + 40.0, y, 180.0, 14.0);
ctx.set_fill_style_str("rgba(150, 255, 180, 1)");
ctx.fill_rect(post_x + 40.0, y, 180.0 * (p as f64).clamp(0.0, 1.0), 14.0);
ctx.set_fill_style_str("rgba(220, 220, 240, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("q(s{s})"), post_x, y + 11.0).ok();
}
drop(l);
// Event log (right column).
let ev_x = cw - 280.0;
let mut ev_y = 192.0;
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("structure events", ev_x, ev_y - 12.0).ok();
for (fr, label) in ev.borrow().iter().rev() {
ctx.set_fill_style_str(if label.starts_with("grew") {
"rgba(255, 200, 120, 1)"
} else {
"rgba(150, 255, 180, 1)"
});
ctx.fill_text(&format!("t={fr} · {label}"), ev_x, ev_y).ok();
ev_y += 18.0;
}
// Footer + receipt.
let total_uj = {
let mut t = cu.borrow_mut();
*t += receipt_data.energy_uj;
*t
};
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(
&format!(
"step {frame} · {} bit-ops · cumulative {total_uj} µJ · replaying this stream reproduces this model bit-for-bit",
receipt_data.bit_ops
),
28.0,
ch - 18.0,
)
.ok();
emit_receipt(
&sink,
"structure_learning_step",
"L0Closed",
1,
(receipt_data.bit_ops as f64) * 0.5,
receipt_data.bit_ops,
);
// Reset cycle so the growth story replays.
if frame >= BLOCK * 9 {
*fc.borrow_mut() = 0;
*cu.borrow_mut() = 0;
ev.borrow_mut().clear();
hi.borrow_mut().clear();
*lr.borrow_mut() = StructureLearner::new(N_OBS, 1, 1.0)
.with_expansion_threshold(0.45)
.with_merge_every(150);
}
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Factor-graph exhibit (reactive sum-product live) ──────────────
//
// One sweep of flooding-schedule belief propagation per animation
// frame, on the canonical one-step active-inference graph:
//
// Prior — s_prev — B[action] — s_next — A — obs (observed)
//
// The anytime property is the exhibit: marginals are proper
// distributions after EVERY sweep — watch them settle from uniform
// to the exact posterior as max_delta falls. The scenario (action +
// observation) rotates so convergence replays continuously.
use mgai_factor_graph::{Factor as FgFactor, Graph as FgGraph};
#[wasm_bindgen]
pub fn mount_factor_graph(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
// Scenarios: (action label, transition table, observed obs).
// Tables are [s_prev][s_next]; likelihood is shared.
fn build(scenario: usize) -> (FgGraph, usize, &'static str, usize) {
let (label, table, obs_val): (&'static str, Vec<Vec<f64>>, usize) = match scenario % 4 {
0 => ("stay · obs 0", vec![vec![0.9, 0.1], vec![0.1, 0.9]], 0),
1 => ("stay · obs 1", vec![vec![0.9, 0.1], vec![0.1, 0.9]], 1),
2 => ("swap · obs 0", vec![vec![0.1, 0.9], vec![0.9, 0.1]], 0),
_ => ("swap · obs 1", vec![vec![0.1, 0.9], vec![0.9, 0.1]], 1),
};
let mut g = FgGraph::new();
let s_prev = g.add_variable("s_prev", 2);
let s_next = g.add_variable("s_next", 2);
let obs = g.add_variable("obs", 2);
g.add_factor(FgFactor::Prior { var: s_prev, probs: vec![0.7, 0.3] }).unwrap();
g.add_factor(FgFactor::Pairwise { a: s_prev, b: s_next, table }).unwrap();
g.add_factor(FgFactor::Pairwise {
a: s_next,
b: obs,
table: vec![vec![0.8, 0.2], vec![0.3, 0.7]],
})
.unwrap();
g.observe(obs, obs_val).unwrap();
(g, s_next, label, obs_val)
}
let scenario = Rc::new(RefCell::new(0usize));
let state = Rc::new(RefCell::new(build(0)));
let sweep_count = Rc::new(RefCell::new(0u32));
let converged_hold = Rc::new(RefCell::new(0u32));
let delta_history = Rc::new(RefCell::new(Vec::<f64>::new()));
let cumulative_uj = Rc::new(RefCell::new(0u64));
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
let sc = scenario.clone();
let st = state.clone();
let sw = sweep_count.clone();
let chold = converged_hold.clone();
let dh = delta_history.clone();
let cu = cumulative_uj.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
// One sweep per frame (slowed: every 12th frame so the
// settling is visible at 60 fps).
let frame_gate = {
let mut s = sw.borrow_mut();
*s += 1;
*s % 12 == 0
};
let receipt_opt = if frame_gate && *chold.borrow() == 0 {
let mut s = st.borrow_mut();
let r = s.0.sweep();
dh.borrow_mut().push(r.max_delta);
if r.max_delta < 1e-9 {
*chold.borrow_mut() = 90; // hold converged ~1.5 s
}
Some(r)
} else {
None
};
// Converged-hold countdown → next scenario.
{
let mut h = chold.borrow_mut();
if *h > 0 {
*h -= 1;
if *h == 0 {
let mut scv = sc.borrow_mut();
*scv += 1;
*st.borrow_mut() = build(*scv);
dh.borrow_mut().clear();
}
}
}
let s = st.borrow();
let (ref graph, s_next, label, obs_val) = *s;
// Header.
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("16px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("reactive message passing — sum-product on a factor graph", 28.0, 32.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
"one sweep at a time · marginals stay proper mid-inference · exact on trees",
28.0,
52.0,
)
.ok();
ctx.set_fill_style_str("rgba(120, 200, 255, 1)");
ctx.set_font("12px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("scenario: {label}"), 28.0, 84.0).ok();
// Graph layout: 3 variable nodes + 3 factor squares on a line.
let node_y = 150.0;
let xs = [120.0, 400.0, 680.0];
let names = ["s_prev", "s_next", "obs"];
// Edges (factor squares between nodes + prior square left).
ctx.set_stroke_style_str("rgba(100, 120, 150, 1)");
ctx.begin_path();
ctx.move_to(60.0, node_y);
ctx.line_to(xs[2], node_y);
ctx.stroke();
// Prior factor square.
ctx.set_fill_style_str("rgba(255, 200, 120, 1)");
ctx.fill_rect(52.0, node_y - 8.0, 16.0, 16.0);
// Pairwise factor squares at midpoints.
for mid in [(xs[0] + xs[1]) / 2.0, (xs[1] + xs[2]) / 2.0] {
ctx.fill_rect(mid - 8.0, node_y - 8.0, 16.0, 16.0);
}
// Variable nodes + marginal bars.
for (i, (&x, name)) in xs.iter().zip(names.iter()).enumerate() {
let observed = i == 2;
ctx.set_fill_style_str(if observed {
"rgba(150, 255, 180, 1)"
} else {
"rgba(120, 200, 255, 1)"
});
ctx.begin_path();
ctx.arc(x, node_y, 14.0, 0.0, std::f64::consts::TAU).ok();
ctx.fill();
ctx.set_fill_style_str("rgba(220, 220, 240, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(name, x - 20.0, node_y - 24.0).ok();
if observed {
ctx.fill_text(&format!("= {obs_val}"), x - 12.0, node_y + 36.0).ok();
}
// Marginal bars under each unobserved node.
if let Ok(m) = graph.marginal(i) {
for (k, &p) in m.iter().enumerate() {
let by = node_y + 52.0 + k as f64 * 20.0;
ctx.set_fill_style_str("rgba(40, 50, 70, 1)");
ctx.fill_rect(x - 60.0, by, 120.0, 14.0);
ctx.set_fill_style_str(if i == s_next {
"rgba(150, 255, 180, 1)"
} else {
"rgba(120, 200, 255, 1)"
});
ctx.fill_rect(x - 60.0, by, 120.0 * p, 14.0);
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(&format!("{p:.3}"), x + 66.0, by + 11.0).ok();
}
}
}
// Convergence trace: log10(max_delta) per sweep.
let hist = dh.borrow();
let tr_x = 28.0;
let tr_y = ch - 130.0;
let tr_w = cw - 56.0;
let tr_h = 60.0;
ctx.set_fill_style_str("rgba(40, 50, 70, 0.6)");
ctx.fill_rect(tr_x, tr_y, tr_w, tr_h);
ctx.set_fill_style_str("rgba(120, 200, 255, 1)");
let n = hist.len().max(1) as f64;
for (i, &d) in hist.iter().enumerate() {
// map log10 d ∈ [-12, 0] to bar height
let l = (d.max(1e-12)).log10(); // [-12, 0]
let hfrac = ((l + 12.0) / 12.0).clamp(0.0, 1.0);
let bh = hfrac * (tr_h - 4.0);
let x = tr_x + (i as f64 / n) * tr_w;
ctx.fill_rect(x, tr_y + tr_h - bh, (tr_w / n).max(2.0), bh);
}
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("10px ui-monospace, SFMono-Regular, Menlo, monospace");
let conv_label = if *chold.borrow() > 0 {
format!("CONVERGED in {} sweeps — exact posterior; next scenario shortly", hist.len())
} else {
format!("log₁₀(max message Δ) per sweep — {} so far", hist.len())
};
ctx.fill_text(&conv_label, tr_x, tr_y + tr_h + 14.0).ok();
drop(hist);
drop(s);
// Receipt per executed sweep.
if let Some(r) = receipt_opt {
let total_uj = {
let mut t = cu.borrow_mut();
*t += r.energy_uj;
*t
};
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(
&format!(
"sweep: {} messages · {} bit-ops · cumulative {} µJ",
r.messages_updated, r.bit_ops, total_uj
),
28.0,
ch - 18.0,
)
.ok();
emit_receipt(
&sink,
"factor_graph_sweep",
"L0Closed",
1,
(r.bit_ops as f64) * 0.5,
r.bit_ops,
);
} else {
let total_uj = *cu.borrow();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(&format!("cumulative {total_uj} µJ"), 28.0, ch - 18.0).ok();
}
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
// ── Twin sim surface (twin program Phase 2) ───────────────────────
//
// The deterministic kernel (mgai-sim-core) running live in the tab,
// with the program's headline interaction: a replay scrubber where
// the past is RECOMPUTED from the transcript — not stored — and the
// recomputed state hash is checked against the recorded one in front
// of the visitor. Spawns and kicks are typed SimEvents recorded into
// the same transcript, so user input is replayable too.
use mgai_quad_env::{HoverPolicy, QuadObs};
use mgai_sim_core::{replay, replay_to, SimConfig, SimEvent as TwinEvent, SimWorld as TwinWorld};
use mgai_world_model::conservation::{ConservationGate, Law as TwinLaw};
/// The live PD pilot. Its SetMotors decisions are recorded into the
/// kernel transcript like any other event, so a flight scrubs
/// bit-exact with NO controller running at replay time.
struct TwinPilot {
policy: HoverPolicy,
body: usize,
mass: f32,
}
struct TwinState {
live: TwinWorld,
recorded_hashes: Vec<u64>,
/// Some = scrub view: (step, frozen replayed world, hash matched).
view: Option<(u64, TwinWorld, bool)>,
cumulative_uj: u64,
spawn_counter: u32,
pilot: Option<TwinPilot>,
wind_on: bool,
wind_x: f32,
/// A counterfactual timeline: same transcript prefix (hash-verified
/// at fork time), exactly one divergent event, then independent
/// deterministic evolution. fork = (world, fork_step, verified).
fork: Option<(TwinWorld, u64, bool)>,
/// Vehicle electrical energy (J) accumulated since the fork, per
/// timeline — the counterfactual priced in joules.
elec_live: f64,
elec_fork: f64,
}
thread_local! {
static TWIN: RefCell<Option<TwinState>> = const { RefCell::new(None) };
}
fn twin_canonical() -> TwinState {
let mut live = TwinWorld::new(SimConfig::default())
.unwrap()
.with_gate(ConservationGate::new(vec![TwinLaw::EnergyNonIncreasing { tol: 0.08 }]));
let ball = |pos: [f32; 3], vel: [f32; 3]| TwinEvent::SpawnSphere {
pos,
vel,
radius: 0.2,
density: 1000.0,
restitution: 0.6,
friction: 0.5,
};
live.apply(ball([0.0, 3.0, 0.0], [0.5, 0.0, 0.0])).unwrap();
live.apply(ball([0.6, 5.0, 0.0], [-0.3, 0.0, 0.0])).unwrap();
live.apply(ball([-0.8, 6.5, 0.0], [0.2, 0.0, 0.0])).unwrap();
TwinState {
live,
recorded_hashes: Vec::new(),
view: None,
cumulative_uj: 0,
spawn_counter: 0,
pilot: None,
wind_on: false,
wind_x: 0.0,
fork: None,
elec_live: 0.0,
elec_fork: 0.0,
}
}
/// Spawn a ball. Position derives from the spawn counter — varied but
/// deterministic, so the transcript stays the only source of truth.
#[wasm_bindgen]
pub fn twin_spawn() {
TWIN.with(|t| {
if let Some(state) = t.borrow_mut().as_mut() {
state.spawn_counter += 1;
let k = state.spawn_counter as f32;
let x = ((k * 0.737).fract() - 0.5) * 3.0;
let z = 0.0;
let _ = state.live.apply(TwinEvent::SpawnSphere {
pos: [x, 6.0 + (k * 0.39).fract() * 2.0, z],
vel: [((k * 0.531).fract() - 0.5) * 2.0, 0.0, 0.0],
radius: 0.15 + (k * 0.211).fract() * 0.15,
density: 1000.0,
restitution: 0.55 + (k * 0.173).fract() * 0.3,
friction: 0.5,
});
state.view = None;
}
});
}
/// Kick a body (round-robin) with an upward-lateral impulse.
#[wasm_bindgen]
pub fn twin_kick() {
TWIN.with(|t| {
if let Some(state) = t.borrow_mut().as_mut() {
let n = state.live.n_bodies();
if n == 0 {
return;
}
let body = (state.live.step_index() as usize) % n;
let s = ((state.live.step_index() as f32) * 0.317).fract() - 0.5;
let _ = state.live.apply(TwinEvent::ApplyImpulse {
body,
impulse: [s * 12.0, 18.0, 0.0],
});
state.view = None;
}
});
}
/// Train the hover pilot's gains IN THE TAB (called from a worker —
/// it blocks for a few seconds). Deterministic seeded search on a
/// compact curriculum; returns the TrainingReport as JSON. Same seed,
/// same browser, same gains — the training run is a replayable
/// artifact, which is the twin program's headline sentence.
#[wasm_bindgen]
pub fn twin_train(seed: u32, iterations: u32, neighbours: u32) -> String {
use mgai_quad_env::train::{train_gains, Task, TrainConfig};
use mgai_quad_env::QuadConfig;
let curriculum = vec![
Task::Hover { target: [0.5, 2.5, -0.3], steps: 600, tol_m: 0.18 },
Task::Waypoints {
targets: vec![[0.0, 2.0, 0.3], [0.8, 3.0, -0.4]],
steps_per_leg: 600,
tol_m: 0.2,
},
];
match train_gains(
&QuadConfig::default(),
&curriculum,
TrainConfig { seed: seed as u64, iterations, neighbours, step_frac: 0.25, entropy_budget_bits: None },
) {
Ok(r) => serde_json::to_string(&r).unwrap_or_default(),
Err(e) => format!("{{\"error\":\"{e}\"}}"),
}
}
/// Apply trained gains (a HoverPolicy JSON, target ignored) to the
/// live pilot. The pilot's future SetMotors decisions — now flown
/// with the trained gains — remain typed transcript events, so the
/// trained flight replays bit-exact like everything else.
#[wasm_bindgen]
pub fn twin_apply_gains(policy_json: &str) -> bool {
let Ok(trained) = serde_json::from_str::<HoverPolicy>(policy_json) else {
return false;
};
TWIN.with(|t| {
if let Some(state) = t.borrow_mut().as_mut() {
if let Some(pilot) = state.pilot.as_mut() {
let target = pilot.policy.target;
pilot.policy = HoverPolicy { target, ..trained };
return true;
}
}
false
})
}
/// Fork the timeline: rebuild a second world from the live
/// transcript (the shared past is verified by hash equality, not
/// assumed), then apply ONE divergent event — an opposite crosswind.
/// Both timelines then evolve deterministically side by side. This is
/// the event-keyed counterfactual made visible: the divergence you
/// see is DERIVED from one changed event, not hallucinated.
/// Returns true if the shared past verified bit-exact.
#[wasm_bindgen]
pub fn twin_fork() -> bool {
TWIN.with(|t| {
let mut borrow = t.borrow_mut();
let Some(state) = borrow.as_mut() else { return false };
if state.fork.is_some() {
state.fork = None; // toggle off
return true;
}
// Fork point: the scrubbed step if the visitor rewound the
// timeline, otherwise the present. Rewinding + forking makes
// the scrubbed moment the new shared present: both futures
// grow from it, and the old future is released.
let step = match &state.view {
Some((scrub_step, _, _)) => *scrub_step,
None => state.live.step_index(),
}
.max(1);
let Ok((live_at, hashes)) = replay_to(state.live.transcript(), step) else {
return false;
};
let verified = hashes.last().copied()
== state.recorded_hashes.get(step as usize - 1).copied();
let Ok((mut fork_world, _)) = replay_to(state.live.transcript(), step) else {
return false;
};
// The one divergent event: opposite wind.
let wx = if state.wind_on { -state.wind_x } else { 3.0 };
let _ = fork_world.apply(TwinEvent::SetWind { force: [wx, 0.0, 0.0] });
// Rewind the live timeline to the fork point (its transcript is
// now the shared prefix) and restart the energy accounts —
// the comparison is over the two futures.
state.live = live_at;
state.recorded_hashes.truncate(step as usize);
state.elec_live = 0.0;
state.elec_fork = 0.0;
state.fork = Some((fork_world, step, verified));
state.view = None;
verified
})
}
/// Verify an EXTERNAL continuity snapshot — the live-host rejoin.
/// Takes the host's atomic snapshot JSON `{step, state_hash,
/// transcript}`, replays the transcript in this tab on the same
/// deterministic kernel, and confirms the recomputed final hash
/// equals the host's claimed hash. True = you have independently
/// reproduced a world that ran on the server without you. Returns
/// JSON {ok, step, bytes, hash}.
#[wasm_bindgen]
pub fn twin_verify_external(snapshot_json: &str) -> String {
let v: serde_json::Value = match serde_json::from_str(snapshot_json) {
Ok(v) => v,
Err(e) => return format!("{{\"ok\":false,\"reason\":\"{e}\"}}"),
};
let claimed = v.get("state_hash").and_then(|h| h.as_u64()).unwrap_or(0);
let step = v.get("step").and_then(|s| s.as_u64()).unwrap_or(0);
let Some(tr) = v.get("transcript") else {
return "{\"ok\":false,\"reason\":\"no transcript\"}".into();
};
let parsed: mgai_sim_core::Transcript = match serde_json::from_value(tr.clone()) {
Ok(p) => p,
Err(e) => return format!("{{\"ok\":false,\"reason\":\"{e}\"}}"),
};
let bytes = snapshot_json.len();
let rebuilt = match replay(&parsed) {
Ok(r) => r,
Err(e) => return format!("{{\"ok\":false,\"reason\":\"{e}\"}}"),
};
let ok = rebuilt.final_hash == claimed;
format!(
"{{\"ok\":{ok},\"step\":{step},\"bytes\":{bytes},\"hash\":\"{:#018x}\"}}",
rebuilt.final_hash
)
}
/// Rejoin: prove provable continuity in-tab. Serialize the live
/// transcript to a portable string (the artifact you'd carry to any
/// device), rebuild a fresh world from it, and verify the recomputed
/// final hash matches the run you watched. Ona's persistence is
/// trapped in a cloud session you must trust; this is a string you
/// hold and check. Returns JSON {ok, steps, bytes, hash}.
#[wasm_bindgen]
pub fn twin_rejoin() -> String {
TWIN.with(|t| {
let borrow = t.borrow();
let Some(state) = borrow.as_ref() else { return "{}".into() };
let Some(&recorded) = state.recorded_hashes.last() else {
return "{\"ok\":false,\"reason\":\"no run yet\"}".into();
};
let json = match serde_json::to_string(state.live.transcript()) {
Ok(j) => j,
Err(e) => return format!("{{\"ok\":false,\"reason\":\"{e}\"}}"),
};
let bytes = json.len();
let parsed: mgai_sim_core::Transcript = match serde_json::from_str(&json) {
Ok(p) => p,
Err(e) => return format!("{{\"ok\":false,\"reason\":\"{e}\"}}"),
};
let rebuilt = match replay(&parsed) {
Ok(r) => r,
Err(e) => return format!("{{\"ok\":false,\"reason\":\"{e}\"}}"),
};
let ok = rebuilt.final_hash == recorded;
format!(
"{{\"ok\":{ok},\"steps\":{},\"bytes\":{bytes},\"hash\":\"{:#018x}\"}}",
state.recorded_hashes.len(),
rebuilt.final_hash
)
})
}
/// Toggle a crosswind gust: a typed SetWind event (deterministic
/// force derived from the step index), so the disturbed flight
/// replays bit-exact. Returns the new wind x-force in newtons.
#[wasm_bindgen]
pub fn twin_gust() -> f32 {
TWIN.with(|t| {
let mut borrow = t.borrow_mut();
let Some(state) = borrow.as_mut() else { return 0.0 };
let on = state.wind_on;
let force = if on {
[0.0, 0.0, 0.0]
} else {
// Deterministic strength from the step index — varied but
// replayable (the transcript records the event, not RNG).
let k = (state.live.step_index() as f32 * 0.317).fract();
[2.0 + k * 2.5, 0.0, 0.0]
};
let _ = state.live.apply(TwinEvent::SetWind { force });
state.wind_on = !on;
state.wind_x = force[0];
state.view = None;
force[0]
})
}
/// Stack-hunt probe: like twin_episode but with a step count.
#[wasm_bindgen]
pub fn twin_episode_n(seed: u32, steps: u32) -> String {
use mgai_quad_env::{run_hover, QuadConfig, QuadEnv};
let k = seed as f32;
let target = [
((k * 0.737).fract() - 0.5) * 2.0,
2.0 + (k * 0.39).fract() * 2.0,
((k * 0.531).fract() - 0.5) * 1.5,
];
let mut env = match QuadEnv::new(QuadConfig::default()) {
Ok(e) => e,
Err(e) => return format!("{{\"error\":\"{e}\"}}"),
};
match run_hover(&mut env, &HoverPolicy::new(target), steps as u64, 0.15) {
Ok(r) => serde_json::to_string(&r).unwrap_or_default(),
Err(e) => format!("{{\"error\":\"{e}\"}}"),
}
}
/// Run one complete quad hover episode (for Worker-batched training:
/// each Web Worker loads this module and calls this with its own
/// seed). Returns the EpisodeReport as JSON — final_hash included, so
/// the page can verify that identical seeds produce identical bits
/// across workers running in parallel.
#[wasm_bindgen]
pub fn twin_episode(seed: u32) -> String {
use mgai_quad_env::{run_hover, QuadConfig, QuadEnv};
let k = seed as f32;
let target = [
((k * 0.737).fract() - 0.5) * 2.0,
2.0 + (k * 0.39).fract() * 2.0,
((k * 0.531).fract() - 0.5) * 1.5,
];
let mut env = match QuadEnv::new(QuadConfig::default()) {
Ok(e) => e,
Err(e) => return format!("{{\"error\":\"{e}\"}}"),
};
match run_hover(&mut env, &HoverPolicy::new(target), 900, 0.15) {
Ok(r) => serde_json::to_string(&r).unwrap_or_default(),
Err(e) => format!("{{\"error\":\"{e}\"}}"),
}
}
/// Launch the quadrotor (once) with the PD pilot targeting (0, 3, 0).
/// The spawn is a typed event; every subsequent SetMotors the pilot
/// issues is too.
#[wasm_bindgen]
pub fn twin_launch_quad() {
TWIN.with(|t| {
if let Some(state) = t.borrow_mut().as_mut() {
if state.pilot.is_some() {
return;
}
let mass_before = state.live.snapshot().mass;
let body = state.live.n_bodies();
if state
.live
.apply(TwinEvent::SpawnQuad {
pos: [-2.5, 0.5, 0.0],
vel: [0.0; 3],
half_extents: [0.12, 0.03, 0.12],
density: 580.0,
arm: 0.16,
})
.is_err()
{
return;
}
let mass = state.live.snapshot().mass - mass_before;
state.pilot = Some(TwinPilot {
policy: HoverPolicy::new([0.0, 3.0, 0.0]),
body,
mass,
});
state.view = None;
}
});
}
/// Retarget the pilot from a canvas click (pixel coords + canvas
/// size, inverted through the same projection the renderer uses).
#[wasm_bindgen]
pub fn twin_set_target(px: f64, py: f64, cw: f64, ch: f64) {
TWIN.with(|t| {
if let Some(state) = t.borrow_mut().as_mut() {
if let Some(pilot) = state.pilot.as_mut() {
let x = ((px - 40.0) / (cw - 80.0)) * 8.0 - 4.0;
let y = (ch - 60.0 - py) / (ch - 160.0) * 8.0;
pilot.policy.target =
[x.clamp(-3.8, 3.8) as f32, y.clamp(0.5, 7.5) as f32, 0.0];
state.view = None;
}
}
});
}
/// Scrub to a recorded step: recompute the past from the transcript
/// and report whether the recomputed hash matches the recorded one.
/// Returns the match flag (true = bit-exact).
#[wasm_bindgen]
pub fn twin_scrub(step: u32) -> bool {
TWIN.with(|t| {
let mut borrow = t.borrow_mut();
let Some(state) = borrow.as_mut() else { return false };
state.fork = None;
let target = (step as u64).clamp(1, state.recorded_hashes.len() as u64);
match replay_to(state.live.transcript(), target) {
Ok((world, hashes)) => {
let matched = hashes.last().copied()
== state.recorded_hashes.get(target as usize - 1).copied();
state.view = Some((target, world, matched));
matched
}
Err(_) => false,
}
})
}
/// Leave scrub view; the live world resumes stepping.
#[wasm_bindgen]
pub fn twin_resume() {
TWIN.with(|t| {
if let Some(state) = t.borrow_mut().as_mut() {
state.view = None;
}
});
}
/// Total recorded steps (the scrubber's max).
#[wasm_bindgen]
pub fn twin_total_steps() -> u32 {
TWIN.with(|t| {
t.borrow()
.as_ref()
.map(|s| s.recorded_hashes.len() as u32)
.unwrap_or(0)
})
}
fn twin_draw_world(
ctx: &CanvasRenderingContext2d,
world: &TwinWorld,
cw: f64,
ch: f64,
dim: bool,
quad: Option<usize>,
target: Option<[f32; 3]>,
) {
// Side elevation: world x ∈ [−4, 4] → canvas width; y ∈ [0, 8] →
// canvas height (inverted).
let map_x = |x: f32| ((x as f64 + 4.0) / 8.0) * (cw - 80.0) + 40.0;
let map_y = |y: f32| ch - 60.0 - (y as f64 / 8.0) * (ch - 160.0);
let scale = (cw - 80.0) / 8.0;
// Ground.
ctx.set_stroke_style_str("rgba(100, 120, 150, 1)");
ctx.begin_path();
ctx.move_to(40.0, map_y(0.0));
ctx.line_to(cw - 40.0, map_y(0.0));
ctx.stroke();
let alpha = if dim { 0.55 } else { 1.0 };
if let Some(tg) = target {
let tx = map_x(tg[0]);
let ty = map_y(tg[1]);
ctx.set_stroke_style_str(&format!("rgba(255, 200, 100, {})", alpha * 0.9));
ctx.begin_path();
ctx.move_to(tx - 10.0, ty);
ctx.line_to(tx + 10.0, ty);
ctx.move_to(tx, ty - 10.0);
ctx.line_to(tx, ty + 10.0);
ctx.stroke();
ctx.begin_path();
ctx.arc(tx, ty, 14.0, 0.0, std::f64::consts::TAU).ok();
ctx.stroke();
}
for (i, (pos, vel, radius)) in world.body_states().iter().enumerate() {
let x = map_x(pos[0]);
let y = map_y(pos[1]);
let r = (*radius as f64) * scale;
if quad == Some(i) {
// Quadrotor: rotor bar perpendicular to the body up-axis,
// in the elevation plane.
let up = world.body_up(i).unwrap_or([0.0, 1.0, 0.0]);
let (bx, by) = (up[1] as f64, up[0] as f64); // ⟂ to up, canvas-y flipped
let arm = r.max(8.0) * 1.6;
ctx.set_stroke_style_str(&format!("rgba(120, 230, 255, {alpha})"));
ctx.set_line_width(3.0);
ctx.begin_path();
ctx.move_to(x - bx * arm, y + by * arm);
ctx.line_to(x + bx * arm, y - by * arm);
ctx.stroke();
ctx.set_line_width(1.0);
ctx.set_fill_style_str(&format!("rgba(120, 230, 255, {alpha})"));
for s in [-1.0f64, 1.0] {
ctx.begin_path();
ctx.arc(x + s * bx * arm, y - s * by * arm, 4.5, 0.0, std::f64::consts::TAU).ok();
ctx.fill();
}
ctx.begin_path();
ctx.arc(x, y, 3.5, 0.0, std::f64::consts::TAU).ok();
ctx.fill();
continue;
}
let hue_shift = (i * 47) % 120;
ctx.set_fill_style_str(&format!(
"rgba({}, {}, 255, {alpha})",
100 + hue_shift,
180 + (i * 23) % 60
));
ctx.begin_path();
ctx.arc(x, y, r.max(3.0), 0.0, std::f64::consts::TAU).ok();
ctx.fill();
// Velocity vector.
ctx.set_stroke_style_str(&format!("rgba(255, 220, 150, {alpha})"));
ctx.begin_path();
ctx.move_to(x, y);
ctx.line_to(x + (vel[0] as f64) * 10.0, y - (vel[1] as f64) * 10.0);
ctx.stroke();
}
}
#[wasm_bindgen]
pub fn mount_twin(canvas_id: &str, sink: &js_sys::Function) -> Result<(), JsValue> {
let (canvas, ctx) =
ctx_for(canvas_id).ok_or_else(|| JsValue::from_str("canvas not found"))?;
let sink = sink.clone();
TWIN.with(|t| {
let mut b = t.borrow_mut();
if b.is_none() {
*b = Some(twin_canonical());
}
});
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let g = f.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let cw = canvas.width() as f64;
let ch = canvas.height() as f64;
ctx.set_fill_style_str("rgba(8, 12, 20, 1)");
ctx.fill_rect(0.0, 0.0, cw, ch);
TWIN.with(|t| {
let mut borrow = t.borrow_mut();
let Some(state) = borrow.as_mut() else { return };
// Header.
ctx.set_fill_style_str("rgba(255, 255, 255, 1)");
ctx.set_font("16px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text("twin kernel — deterministic, transcripted, joule-metered", 28.0, 32.0).ok();
match &state.view {
Some((step, world, matched)) => {
// Frozen scrub view: the recomputed past.
let quad = state.pilot.as_ref().map(|p| p.body);
twin_draw_world(&ctx, world, cw, ch, true, quad, None);
ctx.set_font("13px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.set_fill_style_str(if *matched {
"rgba(150, 255, 180, 1)"
} else {
"rgba(255, 120, 120, 1)"
});
let verdict = if *matched {
format!("REPLAY @ step {step} — recomputed from the transcript · hash MATCHES the recorded run ≡ bit-exact")
} else {
format!("REPLAY @ step {step} — HASH MISMATCH (this would be a determinism bug)")
};
ctx.fill_text(&verdict, 28.0, 58.0).ok();
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
"the past is not stored — it is recomputed on every scrub · resume to continue the live run",
28.0,
ch - 18.0,
)
.ok();
}
None => {
// The counterfactual timeline: the same pilot flies
// the fork world from ITS state — same policy, one
// changed event, derived divergence.
if state.fork.is_some() {
if let Some(pilot) = &state.pilot {
let (fw, _, _) = state.fork.as_mut().unwrap();
let states = fw.body_states();
if let Some((pos, vel, _)) = states.get(pilot.body) {
let obs = QuadObs {
pos: *pos,
vel: *vel,
up: fw.body_up(pilot.body).unwrap_or([0.0, 1.0, 0.0]),
angvel: fw.body_angvel(pilot.body).unwrap_or([0.0; 3]),
step: fw.step_index(),
};
let thrusts =
pilot.policy.act(&obs, pilot.mass).map(|t| t.clamp(0.0, 6.0));
let dt = 1.0 / 120.0f64;
state.elec_fork += thrusts
.iter()
.map(|&t| (t as f64).powf(1.5) / 0.35 * dt)
.sum::<f64>();
let _ = fw.apply(TwinEvent::SetMotors { body: pilot.body, thrusts });
}
}
let (fw, _, _) = state.fork.as_mut().unwrap();
let _ = fw.step();
}
// The pilot flies: PD action from the current
// observation, recorded as a typed SetMotors event.
if let Some(pilot) = &state.pilot {
let states = state.live.body_states();
if let Some((pos, vel, _)) = states.get(pilot.body) {
let obs = QuadObs {
pos: *pos,
vel: *vel,
up: state.live.body_up(pilot.body).unwrap_or([0.0, 1.0, 0.0]),
angvel: state
.live
.body_angvel(pilot.body)
.unwrap_or([0.0; 3]),
step: state.live.step_index(),
};
let thrusts = pilot.policy.act(&obs, pilot.mass)
.map(|t| t.clamp(0.0, 6.0));
if state.fork.is_some() {
let dt = 1.0 / 120.0f64;
state.elec_live += thrusts
.iter()
.map(|&t| (t as f64).powf(1.5) / 0.35 * dt)
.sum::<f64>();
}
let _ = state.live.apply(TwinEvent::SetMotors {
body: pilot.body,
thrusts,
});
}
}
// Live stepping.
if let Ok(receipt) = state.live.step() {
state.recorded_hashes.push(receipt.state_hash);
state.cumulative_uj += receipt.energy_uj;
let quad = state.pilot.as_ref().map(|p| p.body);
let target = state.pilot.as_ref().map(|p| p.policy.target);
twin_draw_world(&ctx, &state.live, cw, ch, false, quad, target);
if let Some((fw, fstep, verified)) = &state.fork {
let quad = state.pilot.as_ref().map(|p| p.body);
twin_draw_world(&ctx, fw, cw, ch, true, quad, None);
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.set_fill_style_str(if *verified {
"rgba(150, 255, 180, 1)"
} else {
"rgba(255, 120, 120, 1)"
});
ctx.fill_text(
&format!(
"FORK @ step {fstep} — shared past {} · one changed event (opposite wind) · dim = counterfactual, derived not dreamed",
if *verified { "verified ≡ bit-exact" } else { "UNVERIFIED" }
),
28.0,
112.0,
)
.ok();
ctx.set_fill_style_str("rgba(255, 210, 130, 1)");
ctx.fill_text(
&format!(
"energy since fork — yours {:.1} J · counterfactual {:.1} J · the other choice costs {:+.1} J",
state.elec_live,
state.elec_fork,
state.elec_fork - state.elec_live
),
28.0,
130.0,
)
.ok();
}
if state.wind_on {
ctx.set_stroke_style_str("rgba(140, 200, 255, 0.7)");
ctx.set_fill_style_str("rgba(140, 200, 255, 0.9)");
let phase = (receipt.step % 40) as f64 / 40.0;
for lane in 0..3 {
let y = 100.0 + lane as f64 * 26.0;
let x = 60.0 + ((phase + lane as f64 * 0.33) % 1.0) * (cw - 160.0);
ctx.begin_path();
ctx.move_to(x, y);
ctx.line_to(x + 26.0, y);
ctx.line_to(x + 19.0, y - 4.0);
ctx.move_to(x + 26.0, y);
ctx.line_to(x + 19.0, y + 4.0);
ctx.stroke();
}
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.fill_text(
&format!("crosswind {:.1} N — a typed event; the pilot leans into it; the books still balance", state.wind_x),
28.0,
94.0,
)
.ok();
}
ctx.set_font("11px ui-monospace, SFMono-Regular, Menlo, monospace");
ctx.set_fill_style_str("rgba(120, 200, 255, 1)");
ctx.fill_text(
&format!(
"step {} · bodies {} · state hash {:#018x}",
receipt.step, receipt.n_bodies, receipt.state_hash
),
28.0,
58.0,
)
.ok();
if let Some(gate) = &receipt.gate {
ctx.set_fill_style_str(if gate.passed {
"rgba(150, 255, 180, 1)"
} else {
"rgba(255, 120, 120, 1)"
});
let lab = if gate.passed {
"conservation gate: energy books balanced".to_string()
} else {
format!("conservation gate VIOLATION: {:?}", gate.laws)
};
ctx.fill_text(&lab, 28.0, 76.0).ok();
}
ctx.set_fill_style_str("rgba(160, 170, 190, 1)");
ctx.fill_text(
&format!(
"cumulative {} µJ · every spawn + kick is a typed event in the transcript",
state.cumulative_uj
),
28.0,
ch - 18.0,
)
.ok();
emit_receipt(
&sink,
"twin_sim_step",
"L0Closed",
1,
(receipt.bit_ops as f64) * 0.5,
receipt.bit_ops,
);
}
}
}
});
schedule(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
schedule(g.borrow().as_ref().unwrap());
std::mem::forget(g);
Ok(())
}
This is the exact Rust file compiled into the WASM module the page just loaded. Every line that runs on your device is here. The receipt is a function of this code, not a bespoke benchmark.
What the ≡ / ≠ indicator means
- ≡ byte-reproducible:
the kernel produced the same bytes on every one of the 1000 runs.
The receipt grammar can carry this as a first-class field
(
output_hash: Option<u64>); any consumer can detect drift without trusting the kernel's authors. - ≠ diverged: the substrate is confessing, not lying. Joule receipts that don't ship a hash can be honest about energy and still hide non-determinism behind the decimal point. This exhibit forces that property into the open.
- ∎ first observation: the substrate hasn't seen this primitive before; no commitment made yet. The next run flips it to ≡ or ≠.
Lineage
Thinking Machines Lab opened their Connectionism blog with
Defeating Nondeterminism in LLM Inference
(Horace He, 2025-09): batch-size variance, not concurrent atomic
adds, is the real source of LLM nondeterminism. They open-sourced
batch-invariant-ops
— fixed reductions, fixed tile sizes, fixed split sizes — and
reproduced 1000 / 1000 identical completions on Qwen 3-235B at a
~1.6× cost.
Mathground takes a parallel position: rather than fix non-determinism at the kernel author's layer, publish a 64-bit fingerprint of what the kernel produced on every receipt. Consumers detect drift themselves. The two stances are complementary — TM commits to the kernel being batch-invariant; mathground commits the receipt to being honest about whether it was.
See /receipts · 05 — the reproducibility receipt for the grammar.