diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 34dcebd71..b384f7d6a 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -54,8 +54,8 @@ Plan: `.claude/plans/2026-10-08-resident-projection-fold-mask-v1.md`. Mask = adm | **D-RPF-6** | Tile skip on zero gate measured on resident-row predicates (capstone pt 3) | Queued | identical answers; dead-tile beside dead-word fraction | | **D-RPF-7** | Rank spectrum of the 256×256 palette distance LUTs | Queued (measurement only) | reported; never a fold | | **D-RPF-CONF-0** | Confidence surface: Rust vs exact integer form, (255,255), zero-weight identity, c=255 candidates, census of live revision laws | Shipped (#1424; measured) | T1 three one-code f32 deviations; (255,255) → (0,0); 24 defs → 7 laws → 4 behaviours; G1 open (`entries/2026-10-09-d-rpf-conf-0-confidence-surface.md`) | -| **D-RPF-TABLE-1** | Cost of `NarsTables` resolutions B1..B16 vs exact revision A: surface, replay drift, decision flips, throughput | Shipped (#1426; measured); G2 follow-up: law-A load-bearing fixture + frequency-bar rationale without B1's constant (this PR) | B1 2.2–4.4× hot replay, 37.9% verdict flips; no Bn decision-stable; A wins cold; law-A fixture 210 → 64 flips, B1–B8 miss it, B16 agrees (`entries/2026-10-09-d-rpf-table-1-fast-path-cost.md`) | -| **D-RPF-P5** | NARS ↔ Beta correspondence on the CE64 surface: Jeffreys identity, unit-evidence stall, duplicate inflation, variance coarseness, 255 unreachable | In PR (tests only) | E1 exact for k = 1, k = 2 distinguishable; E2 stall at c = 244 after 20 unit obs; E2b 12-row stall table; E3 4 copies → sd 2.04× too small; E5 max revision code 254; ceil-rounding disable turns E2/E2b/E5 red (`entries/2026-10-10-d-rpf-p5-nars-beta-correspondence.md`) | +| **D-RPF-TABLE-1** | Cost of `NarsTables` resolutions B1..B16 vs exact revision A: surface, replay drift, decision flips, throughput | Shipped (#1426; measured); G2 follow-up: law-A load-bearing fixture + frequency-bar rationale without B1's constant (#1440) | B1 2.2–4.4× hot replay, 37.9% verdict flips; no Bn decision-stable; A wins cold; law-A fixture 210 → 64 flips, B1–B8 miss it, B16 agrees (`entries/2026-10-09-d-rpf-table-1-fast-path-cost.md`) | +| **D-RPF-P5** | NARS ↔ Beta correspondence on the CE64 surface: Jeffreys identity, unit-evidence stall, duplicate inflation, variance coarseness, 255 unreachable | Shipped (#1440; tests only) | E1 exact for k = 1, k = 2 distinguishable; E2 stall at c = 244 after 20 unit obs; E2b 12-row stall table; E3 4 copies → sd 2.04× too small; E5 max revision code 254; ceil-rounding disable turns E2/E2b/E5 red (`entries/2026-10-10-d-rpf-p5-nars-beta-correspondence.md`) | | **D-RPF-8** | Boundary: mask-risc never executes CE64 instructions; no VSA on this path | Queued (doc) | review-enforced | | **D-RPF-9** | Fold-Join deforestation probe: resident ∧ resident, predicate chains gated/ungated, CE64 in place, strided pattern merge | Shipped (probe; R2/R3/G1–G3 proposed) | every arm equals the oracle; care-shift fires, non-care rewrite silent; merge precondition refuses conflicting cares | @@ -105,8 +105,9 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **D-MOORE-NARS-0** | Recipe learning from reasoning consequences on Gomoku (`cognitive-shader-driver/examples/moore_nars_gomoku_probe.rs`): census of the 34 recipes, 9 realized on Moore lines, structural `situation → recipe` NARS learner | Shipped (#1400) | persistent structural learner 0.500 → 0.535 vs best fixed recipe 0.498 and reset-every-game ~0.47; literal-board memory and immediate credit learn nothing; W-delayed one-reply credit is the spine; transfer to unseen openings 0.526 vs 0.493; "less work" not achieved (~280 folds/step); unlearning after switch not demonstrated (`entries/2026-10-08-moore-nars-0-recipe-learning-gomoku.md`) | | **D-MOORE-PERT-AB-0** | Moore perturbation field A/B probe with an independent arm and statistical comparison (`examples/MoorePertubationsfeld.rs`) | Shipped (#1427; probe) | CI line; percentile fix; timings dropped from asserts; falsifiable tests | | **D-MOORE-OBSERVABLE-FIRST-0** | Certify N-1 overload decisions before field convergence (linear flow model) | Shipped (#1428; measured) | solver-agnostic ‖r‖₁ bound + warm start certifies most decisions early; PCG/LODF beat Jacobi; bound proven for the linear flow model only (`entries/2026-10-09-d-moore-observable-first-0.md`) | -| **D-LAB-P1** | Certified argmax on Gomoku minimax: deterministic `L_best ≥ U_j` cutoff (`minimax_certified`) | In PR (probe) | same pick as `minimax` on 400 positions at k 4/4, 5/5, 8/8; folds −36.2 / −44.8 / −61.0 %; cut at `L + 1` changes a pick (`entries/2026-10-10-d-lab-p1-certified-argmax-gomoku.md`) | -| **D-SELF-CALIBRATING-LAB-0** | Hypothesis lab v0: shared paired/Holm/verdict/reliability statistics (`examples/shared/lab.rs`) + Gomoku arm (`moore_nars_gomoku_probe --lab`) | In PR (probe; Gomoku + crossword) | crossword (16 fresh puzzles): C1 Support −36× nodes SUPPORTED, C2–C4 ops FALSIFIED (Support 13×, Single 2.9×, Pair 2.2× more ops); Gomoku fresh-seed confirmation, 16 blocks: H2 learned > fixed HPM +0.024 SUPPORTED; H4 certified −29 folds/step, identical games at cost 0 SUPPORTED; H7 cost term −143 folds/step SUPPORTED (−0.026 score); H3 INCONCLUSIVE; H5 FALSIFIED; e ECE 0.05 over non-neutral outcomes (`entries/2026-10-10-d-self-calibrating-lab-0.md`) | +| **D-LAB-P1** | Certified argmax on Gomoku minimax: deterministic `L_best ≥ U_j` cutoff (`minimax_certified`) | Shipped (#1440; probe) | same pick as `minimax` on 400 positions at k 4/4, 5/5, 8/8; folds −36.2 / −44.8 / −61.0 %; cut at `L + 1` changes a pick (`entries/2026-10-10-d-lab-p1-certified-argmax-gomoku.md`) | +| **D-SELF-CALIBRATING-LAB-0** | Hypothesis lab v0: shared paired/Holm/verdict/reliability statistics (`examples/shared/lab.rs`) + Gomoku arm (`moore_nars_gomoku_probe --lab`) | Shipped (#1440; probe; Gomoku + crossword) | crossword (16 fresh puzzles): C1 Support −36× nodes SUPPORTED, C2–C4 ops FALSIFIED (Support 13×, Single 2.9×, Pair 2.2× more ops); Gomoku fresh-seed confirmation, 16 blocks: H2 learned > fixed HPM +0.024 SUPPORTED; H4 certified −29 folds/step, identical games at cost 0 SUPPORTED; H7 cost term −143 folds/step SUPPORTED (−0.026 score); H3 INCONCLUSIVE; H5 FALSIFIED; e ECE 0.05 over non-neutral outcomes (`entries/2026-10-10-d-self-calibrating-lab-0.md`) | +| **D-SELF-CALIBRATING-LAB-1** | Lab v1 on Gomoku: Thompson chooser (Jeffreys Beta posterior) and a context-free global key, second pre-registered family | In PR (probe) | fresh seeds 4001..: T1 Thompson −0.050 score FALSIFIED; T2 adaptation −0.015 FALSIFIED; T3 structural > global +0.025 SUPPORTED; T4 Thompson folds FALSIFIED (`entries/2026-10-10-d-self-calibrating-lab-1.md`) | | **D-CE64-NEXTSTATE-0** | EpistemicState5 as a sparse next-state mutation of the CE64 register | In PR | `cognitive-shader-driver/examples/ce64_nextstate_probe.rs`: N1 only bits 59..63 move, all 32 codes on an edge with every field set; N2 written in cycle k through `MailboxSoA::write_row`, read in k+1 through `MailboxSoaView`; N3 the mailbox does not double-buffer (a same-cycle read sees the write), and next-cycle authority is a read discipline: start-of-cycle copy plus `last_write_cycle == cycle` treated as pending; N4 canonical LE round trip (bits 59..63 in byte 7, bits 3..7); N5 a fresh mailbox from the persisted word continues identically; N6 the shipped `with_spare` + `with_truth` compile to `bzhi; shl; or`, identical to the masked insert (LLVM folded the two), so no 5-bit writer is added. Production change: `CausalEdge64::to_le_bytes` / `from_le_bytes` (mirroring `EpisodicEdges64`) and the register contract on the type. 6 tests, 7 disable runs red | | **D-ANCESTRY-K1-0** | StreamDto / PerturbationDto / BusDto / p64 / shader-driver ancestry vs code | Shipped (this PR) | `entries/2026-10-06-streamdto-circuit-ancestry.md`: v3 docs first, then code (git history is shallow at #1327). The k → k+1 loop was intended (`EmitMode::Persist`, `persist_cycle`, `mailbox-thoughtspace` read shim, W4a cast pairing) and left open: the driver owned its mailbox read-only. Path B | | **D-STREAMDTO-0** | A real `StreamDto` through the shipped path to next-cycle #1370 eligibility | In PR | `cognitive-shader-driver/examples/streamdto_circuit_probe.rs`: `StreamDto` → the new `ingest_codebook_indices_soa` (straight into the SoA, cycle-gated `write_row`, encoding shared with the superseded BindSpace arm) → `dispatch` k → `persisted_row` gets `emitted_edges[0]` via the new `ShaderDriver::mailbox_mut` → `tick` → `dispatch` k+1 reads it and changes → `measure`. The edge steers k+1 only through `s_idx / 4`; the shader writes code 0, so eligibility is unchanged by the circuit (`0x20`, same as an unwritten word; code 7 gives `0x24`). Timestamp ≠ cycle; restart from LE bytes reproduces k+1. CI step added (feature-gated targets incl. `w2_differential` were never run). 6 tests + 1 lib test, 9 disable runs red | diff --git a/.claude/board/entries/2026-10-10-d-self-calibrating-lab-1.md b/.claude/board/entries/2026-10-10-d-self-calibrating-lab-1.md new file mode 100644 index 000000000..8437b2e35 --- /dev/null +++ b/.claude/board/entries/2026-10-10-d-self-calibrating-lab-1.md @@ -0,0 +1,50 @@ +# D-SELF-CALIBRATING-LAB-1 — Thompson sampling and context, a second pre-registered family (2026-10-10) + +**Status:** MEASURED. Probe only (`moore_nars_gomoku_probe --lab-v1`). +**Builds on:** `entries/2026-10-10-d-self-calibrating-lab-0.md` (same +statistics, same horizon: 300 games, endpoint = last 100-game window). +Fresh seeds 4001–4016; a separate Holm family (α = 0.05); 16.4 s wall. + +## New arms + +- `Chooser::Thompson`: per recipe, a draw from Beta(pos + ½, neg + ½), the + Jeffreys posterior D-RPF-P5 identified, minus the same fold-cost term as + the argmax chooser. Sampler: `lab::beta_sample` (Gamma via + Marsaglia–Tsang, boost for shape < 1), moment-tested on Beta(½, ½), + Beta(2.5, 7.5), Beta(30.5, 10.5). +- `Memory::Global`: every situation shares one key (context-free baseline). + +## Ledger (statistical) + +| id | hypothesis | effect (+ = treatment better) | 95 % CI | p | Holm | verdict | +|---|---|---|---|---|---|---| +| T1 | Thompson beats expectation argmax, score | −0.0500 | [−0.0620, −0.0380] | <0.0001 | reject | **FALSIFIED** | +| T2 | Thompson adapts faster after Threat → Aggressor | −0.0153 | [−0.0244, −0.0062] | 0.0040 | reject | **FALSIFIED** | +| T3 | structural context beats one global key, score | +0.0247 | [+0.0050, +0.0444] | 0.0186 | reject | **SUPPORTED** | +| T4 | Thompson reduces folds/step | −18.5 (Thompson costs more) | [−38.4, +1.4] | 0.0596 | keep | **FALSIFIED** (the interval cannot reach the 5-fold minimum) | + +Pareto (empirical): argmax e 0.5062 score / 244.8 folds per step; Thompson +0.4562 / 263.3; global 0.4816 / 32.3. + +## Reading + +- Thompson is worse here in the learned window and after a switch. A likely + mechanism, not tested: evidence arrives in fractional units (activation/7 ≤ 1 + per update) spread over many structural keys, so posteriors stay wide and + sampling keeps exploring. Testing it needs an evidence-scale arm, not a + re-reading of this run. +- Context matters (T3). The global key collapses onto cheap recipes (32 + folds/step) and loses 0.025 score: the structural signature buys quality + at about 8× the folds. +- Expectation argmax stays the chooser. + +Falsifiers: `beta_sample_has_the_beta_moments`, +`thompson_is_seeded_and_differs_from_argmax`; replacing the sampler with the +posterior mean turns both red. + +``` +PR (this) | STATUS: measured | OUTCOME: Thompson falsified on score (-0.050) +and adaptation (-0.015); structural context supported (+0.025 over a global +key) | OPEN: evidence-scale arm for Thompson; eligibility traces; +cost-aware selector; held-out domain +``` diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 3dca73b0e..f7b164586 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,7 +25,7 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -291 entries, 2026-08-06 .. 2026-10-10. +292 entries, 2026-08-06 .. 2026-10-10. | date | entry id | finding | file | |---|---|---|---| @@ -33,6 +33,7 @@ exactly one of them, never both. | 2026-10-10 | `D-XGP-2` | | [2026-10-10-d-xgp-2-hotplug-billable-work-entry.md](2026-10-10-d-xgp-2-hotplug-billable-work-entry.md) | | 2026-10-10 | `D-XGP-1` | | [2026-10-10-d-xgp-1-cats-binder-canonical-front.md](2026-10-10-d-xgp-1-cats-binder-canonical-front.md) | | 2026-10-10 | `D-XGP-0` | SAP and Odoo share a type, not a meaning | [2026-10-10-d-xgp-0-cross-glove-inventory.md](2026-10-10-d-xgp-0-cross-glove-inventory.md) | +| 2026-10-10 | `D-SELF-CALIBRATING-LAB-1` | | [2026-10-10-d-self-calibrating-lab-1.md](2026-10-10-d-self-calibrating-lab-1.md) | | 2026-10-10 | `D-SELF-CALIBRATING-LAB-0` | | [2026-10-10-d-self-calibrating-lab-0.md](2026-10-10-d-self-calibrating-lab-0.md) | | 2026-10-10 | `D-RPF-P5` | | [2026-10-10-d-rpf-p5-nars-beta-correspondence.md](2026-10-10-d-rpf-p5-nars-beta-correspondence.md) | | 2026-10-10 | `D-RPF-G2` | | [2026-10-10-d-rpf-g2-law-a-boundary.md](2026-10-10-d-rpf-g2-law-a-boundary.md) | diff --git a/crates/cognitive-shader-driver/examples/moore_nars_gomoku_probe.rs b/crates/cognitive-shader-driver/examples/moore_nars_gomoku_probe.rs index d11708beb..07842682f 100644 --- a/crates/cognitive-shader-driver/examples/moore_nars_gomoku_probe.rs +++ b/crates/cognitive-shader-driver/examples/moore_nars_gomoku_probe.rs @@ -755,6 +755,8 @@ enum Memory { Structural, /// The literal board. Literal, + /// One key for every situation: a context-free baseline. + Global, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -765,6 +767,16 @@ enum Chooser { Uniform, /// Learned NARS expectation. Learned, + /// Thompson sampling: a draw from each recipe's Beta(pos + ½, neg + ½) + /// posterior (the Jeffreys prior P5 identified), minus the same cost term. + Thompson, +} + +impl Chooser { + /// Whether the chooser reads and writes learned evidence. + fn learns(self) -> bool { + matches!(self, Chooser::Learned | Chooser::Thompson) + } } #[derive(Debug, Clone, Copy)] @@ -858,7 +870,7 @@ impl Learner { match cfg.chooser { Chooser::Fixed(op) => (op, false), Chooser::Uniform => (OPS[rng.below(NO)], false), - Chooser::Learned => { + Chooser::Learned | Chooser::Thompson => { let (row, hit) = match self.stats.get(&key) { Some(r) => (*r, true), None => match (cfg.memory, self.stats.get(&back)) { @@ -870,7 +882,13 @@ impl Learner { for (k, &op) in OPS.iter().enumerate() { let (n, f) = self.folds[k]; let mean = if n == 0 { 0.0 } else { f as f64 / n as f64 }; - let v = row[k].expectation() - cfg.cost_weight * (1.0 + mean).ln(); + let value = if cfg.chooser == Chooser::Thompson { + let mut u = || (rng.next() >> 11) as f64 / (1u64 << 53) as f64; + lab::beta_sample(row[k].pos + 0.5, row[k].neg + 0.5, &mut u) + } else { + row[k].expectation() + }; + let v = value - cfg.cost_weight * (1.0 + mean).ln(); if v > best.0 { best = (v, op); } @@ -883,7 +901,7 @@ impl Learner { let mut out = Ev::default(); let keys: &[u64] = match cfg.memory { Memory::Structural => &[p.key, p.back], - Memory::Literal => &[p.key], + Memory::Literal | Memory::Global => &[p.key], }; for &k in keys { let row = self.stats.entry(k).or_insert([Ev::default(); NO]); @@ -1166,7 +1184,7 @@ fn game( Signal::Contradiction => contradiction.then_some(-7), Signal::Final => None, }; - if let (Some(a), true, Chooser::Learned) = (act, cfg.learn, cfg.chooser) { + if let (Some(a), true, true) = (act, cfg.learn, cfg.chooser.learns()) { let epi = l.credit(cfg, &p, a); let expect = 2.0 * epi_expectation(l, cfg, &p) - 1.0; let surprise = ((f64::from(a) / 7.0 - expect).abs() * 3.5).round() as u8; @@ -1182,16 +1200,17 @@ fn game( let key = match cfg.memory { Memory::Structural => signature(&s), Memory::Literal => b.hash(me), + Memory::Global => 0, }; let back = coarse(&s); let (op, hit) = l.choose(cfg, key, back, &mut rng); let pred = match cfg.chooser { - Chooser::Learned => l + Chooser::Learned | Chooser::Thompson => l .stats .get(&key) .or_else(|| match cfg.memory { Memory::Structural => l.stats.get(&back), - Memory::Literal => None, + Memory::Literal | Memory::Global => None, }) .map_or(0.5, |row| row[op.index()].expectation()), _ => 0.5, @@ -1270,11 +1289,11 @@ fn game( Signal::Contradiction => (result == -1).then_some(-7), Signal::Final => None, }; - if let (Some(a), true, Chooser::Learned) = (act, cfg.learn, cfg.chooser) { + if let (Some(a), true, true) = (act, cfg.learn, cfg.chooser.learns()) { l.credit(cfg, &p, a); } } - if let (Signal::Final, true, Chooser::Learned) = (cfg.signal, cfg.learn, cfg.chooser) { + if let (Signal::Final, true, true) = (cfg.signal, cfg.learn, cfg.chooser.learns()) { for p in &game_steps { l.credit(cfg, p, 7 * result); } @@ -1594,11 +1613,115 @@ fn lab_stage() { println!("\nlab wall time {:.1}s", t0.elapsed().as_secs_f64()); } +/// Lab v1: a second family, pre-registered before its run, on fresh seeds +/// 4001.. (v0 confirmation used 2001..). Same horizon and endpoint as v0. +const LAB_V1_SEED0: u64 = 4001; +const LAB_V1_PREREG: [lab::Prereg; 4] = [ + lab::Prereg { + id: "T1", + hypothesis: "Thompson sampling beats expectation argmax", + baseline: "learned (argmax e)", + treatment: "Thompson", + endpoint: "score", + higher_is_better: true, + min_effect: 0.02, + blocks: LAB_BLOCKS, + }, + lab::Prereg { + id: "T2", + hypothesis: "Thompson adapts faster after Threat -> Aggressor", + baseline: "learned (argmax e)", + treatment: "Thompson", + endpoint: "post-switch score", + higher_is_better: true, + min_effect: 0.02, + blocks: LAB_BLOCKS, + }, + lab::Prereg { + id: "T3", + hypothesis: "structural context beats one global key", + baseline: "global", + treatment: "structural", + endpoint: "score", + higher_is_better: true, + min_effect: 0.02, + blocks: LAB_BLOCKS, + }, + lab::Prereg { + id: "T4", + hypothesis: "Thompson reduces folds per step", + baseline: "learned (argmax e)", + treatment: "Thompson", + endpoint: "folds/step", + higher_is_better: false, + min_effect: 5.0, + blocks: LAB_BLOCKS, + }, +]; + +fn lab_v1_stage() { + let t0 = std::time::Instant::now(); + println!("D-SELF-CALIBRATING-LAB-0 v1 — Gomoku (9x9 five vs Threat, oracle off)"); + println!( + "blocks {LAB_BLOCKS} (seeds {LAB_V1_SEED0}..), {LAB_GAMES} games, endpoint = last {LAB_WINDOW}\n" + ); + let seeds: Vec = (0..LAB_BLOCKS as u64).map(|i| LAB_V1_SEED0 + i).collect(); + let thompson = Cfg { + chooser: Chooser::Thompson, + ..BASE + }; + let global = Cfg { + memory: Memory::Global, + ..BASE + }; + let timed = |cfg: &Cfg, f: fn(&Cfg, u64, usize) -> Stats| { + let t = std::time::Instant::now(); + let v: Vec = seeds.iter().map(|&sd| f(cfg, sd, LAB_GAMES)).collect(); + (v, t.elapsed().as_secs_f64()) + }; + let (base, t_base) = timed(&BASE, lab_block); + let (thom, t_thom) = timed(&thompson, lab_block); + let (glob, t_glob) = timed(&global, lab_block); + let (sw_base, _) = timed(&BASE, lab_switch_block); + let (sw_thom, _) = timed(&thompson, lab_switch_block); + let score = |v: &[Stats]| v.iter().map(Stats::score).collect::>(); + let fps = |v: &[Stats]| v.iter().map(Stats::folds_per_step).collect::>(); + let results = [ + lab::paired(&score(&base), &score(&thom), true), + lab::paired(&score(&sw_base), &score(&sw_thom), true), + lab::paired(&score(&glob), &score(&base), true), + lab::paired(&fps(&base), &fps(&thom), false), + ]; + let rejected = lab::holm(&results.map(|r| r.p), 0.05); + println!("Family (Holm, alpha 0.05):"); + for i in 0..LAB_V1_PREREG.len() { + println!( + " {}", + lab::report(&LAB_V1_PREREG[i], &results[i], rejected[i]) + ); + } + println!("\nPareto (mean over blocks; empirical; wall = whole arm):"); + for (name, v, t) in [ + ("learned", &base, t_base), + ("Thompson", &thom, t_thom), + ("global", &glob, t_glob), + ] { + let sc = score(v).iter().sum::() / LAB_BLOCKS as f64; + let f = fps(v).iter().sum::() / LAB_BLOCKS as f64; + println!(" {name:<9} score {sc:.4} folds/step {f:7.1} wall {t:6.2}s"); + } + println!("\nlab v1 wall time {:.1}s", t0.elapsed().as_secs_f64()); +} + fn main() { if std::env::args().any(|a| a == "--lab") { lab_stage(); return; } + if std::env::args().any(|a| a == "--lab-v1") { + lab_v1_stage(); + return; + } let t0 = std::time::Instant::now(); println!("D-MOORE-NARS-0: recipe learning on Gomoku\n"); println!("Census of the 34 catalogue recipes:"); @@ -1958,6 +2081,29 @@ mod tests { ); } + /// Lab v1: the Thompson arm is deterministic under a seed (the sampler + /// draws from the probe's own seeded stream) and it is a different policy + /// from expectation argmax, so T1/T2 compare two things, not one. + #[test] + fn thompson_is_seeded_and_differs_from_argmax() { + let thompson = Cfg { + chooser: Chooser::Thompson, + ..BASE + }; + let a = lab_block(&thompson, 5001, 120); + let b = lab_block(&thompson, 5001, 120); + assert_eq!( + (a.wins, a.losses, a.moves, a.folds), + (b.wins, b.losses, b.moves, b.folds) + ); + let c = lab_block(&BASE, 5001, 120); + assert_ne!( + (a.moves, a.folds, a.ops), + (c.moves, c.folds, c.ops), + "Thompson must not collapse onto argmax" + ); + } + /// D-LAB-P1: the certificate never changes the pick, and it saves folds. #[test] fn certified_minimax_picks_what_minimax_picks_with_fewer_folds() { diff --git a/crates/cognitive-shader-driver/examples/shared/lab.rs b/crates/cognitive-shader-driver/examples/shared/lab.rs index 56bdf94a4..e7c32103d 100644 --- a/crates/cognitive-shader-driver/examples/shared/lab.rs +++ b/crates/cognitive-shader-driver/examples/shared/lab.rs @@ -224,10 +224,76 @@ pub fn report(pr: &Prereg, r: &Paired, rejected: bool) -> String { ) } +/// A standard normal draw from two uniforms in (0, 1] (Box–Muller). +fn normal(u: &mut impl FnMut() -> f64) -> f64 { + let (a, b) = (u().max(f64::MIN_POSITIVE), u()); + (-2.0 * a.ln()).sqrt() * (std::f64::consts::TAU * b).cos() +} + +/// A Gamma(shape, 1) draw (Marsaglia–Tsang; shape < 1 by the `U^(1/a)` +/// boost). `u` yields uniforms in [0, 1). +pub fn gamma_sample(shape: f64, u: &mut impl FnMut() -> f64) -> f64 { + assert!(shape > 0.0, "gamma shape must be positive"); + if shape < 1.0 { + let g = gamma_sample(shape + 1.0, u); + return g * u().max(f64::MIN_POSITIVE).powf(1.0 / shape); + } + let d = shape - 1.0 / 3.0; + let c = 1.0 / (9.0 * d).sqrt(); + loop { + let x = normal(u); + let v = 1.0 + c * x; + if v <= 0.0 { + continue; + } + let v = v * v * v; + let w = u().max(f64::MIN_POSITIVE); + if w.ln() < 0.5 * x * x + d - d * v + d * v.ln() { + return d * v; + } + } +} + +/// A Beta(a, b) draw, the posterior a Thompson-sampling chooser samples. +pub fn beta_sample(a: f64, b: f64, u: &mut impl FnMut() -> f64) -> f64 { + let x = gamma_sample(a, u); + let y = gamma_sample(b, u); + x / (x + y) +} + #[cfg(test)] mod lab_tests { use super::*; + struct SplitMix(u64); + impl SplitMix { + fn unit(&mut self) -> f64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + ((z ^ (z >> 31)) >> 11) as f64 / (1u64 << 53) as f64 + } + } + + /// The sampler's moments match Beta(a, b), including the a < 1 boost + /// path a Jeffreys prior uses. + #[test] + fn beta_sample_has_the_beta_moments() { + let mut r = SplitMix(7); + let mut u = || r.unit(); + for (a, b) in [(0.5, 0.5), (2.5, 7.5), (30.5, 10.5)] { + let n = 200_000; + let xs: Vec = (0..n).map(|_| beta_sample(a, b, &mut u)).collect(); + let mean = xs.iter().sum::() / n as f64; + let var = xs.iter().map(|x| (x - mean).powi(2)).sum::() / n as f64; + let (m, v) = (a / (a + b), a * b / ((a + b).powi(2) * (a + b + 1.0))); + assert!((mean - m).abs() < 0.005, "Beta({a},{b}) mean {mean} vs {m}"); + assert!((var - v).abs() / v < 0.03, "Beta({a},{b}) var {var} vs {v}"); + assert!(xs.iter().all(|&x| (0.0..=1.0).contains(&x))); + } + } + #[test] fn sign_flip_p_is_exact_on_a_small_case() { // All four diffs positive: only the all-plus and all-minus flips are diff --git a/crates/lance-graph-mask-risc/examples/phasor_trig_probe.rs b/crates/lance-graph-mask-risc/examples/phasor_trig_probe.rs index 0e72ff6d5..2bf16f41c 100644 --- a/crates/lance-graph-mask-risc/examples/phasor_trig_probe.rs +++ b/crates/lance-graph-mask-risc/examples/phasor_trig_probe.rs @@ -28,6 +28,8 @@ use std::f64::consts::{GOLDEN_RATIO, TAU}; use std::hint::black_box; use std::time::Instant; +use ndarray::simd::PhaseLut; + // ───────────────────────── rng ───────────────────────── struct Rng(u64); @@ -52,48 +54,16 @@ fn turns_to_rad(p: u32) -> f64 { // ───────────────────────── the four phasor evaluators ───────────────────────── -/// A `(cos, sin)` table over `2^bits` phases, `f32`. -struct Lut { - bits: u32, - t: Vec<(f32, f32)>, -} +/// The `(cos, sin)` table is `ndarray::simd::PhaseLut` (#343); this probe used +/// to carry its own copy. +type Lut = PhaseLut; -impl Lut { - fn new(bits: u32) -> Self { - let n = 1usize << bits; - let t = (0..n) - .map(|i| { - let a = TAU * i as f64 / n as f64; - (a.cos() as f32, a.sin() as f32) - }) - .collect(); - Lut { bits, t } - } - /// Nearest entry. - #[inline] - fn nearest(&self, p: u32) -> (f32, f32) { - let sh = 32 - self.bits; - let i = (p.wrapping_add(1 << (sh - 1)) >> sh) as usize; - self.t[i & (self.t.len() - 1)] - } - /// Linear interpolation between the two neighbouring entries. - #[inline] - fn lerp(&self, p: u32) -> (f32, f32) { - let sh = 32 - self.bits; - let i = (p >> sh) as usize; - let f = (p & ((1 << sh) - 1)) as f32 / (1u32 << sh) as f32; - let m = self.t.len() - 1; - let (a, b) = (self.t[i & m], self.t[(i + 1) & m]); - (a.0 + (b.0 - a.0) * f, a.1 + (b.1 - a.1) * f) - } - fn bytes(&self) -> usize { - self.t.len() * 8 - } - /// A conservative bound on `|lut(p) − e^{ip}|` for `nearest`: half a step - /// of arc plus the `f32` rounding of both components. - fn nearest_bound(&self) -> f64 { - std::f64::consts::PI / (1u64 << self.bits) as f64 + 2.0 * f64::from(f32::EPSILON) - } +/// A conservative bound on the COMPLEX distance `|lut(p) − e^{ip}|` for +/// `nearest`: half a step of arc plus the `f32` rounding of both components. +/// Not `PhaseLut::nearest_error_bound`, which bounds each component +/// separately; the early exit below sums complex terms, so it needs this one. +fn nearest_bound(lut: &PhaseLut) -> f64 { + std::f64::consts::PI / (1u64 << lut.bits()) as f64 + 2.0 * f64::from(f32::EPSILON) } /// Circular CORDIC in rotation mode, `i64` fixed point Q30, angle in turns. @@ -597,7 +567,7 @@ fn interference(n_src: usize, side: usize, heavy_tail: bool) { }; let mut sorted = truth.clone(); sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); - let eps = l12.nearest_bound(); + let eps = nearest_bound(&l12); for q in [0.5, 0.9, 0.99] { let t_thr = sorted[((sorted.len() - 1) as f64 * q) as usize]; // Exact path: f64 reference phasors.