diff --git a/examples/randomized_signature_bench.rs b/examples/randomized_signature_bench.rs index 11f23b6f..cb02f999 100644 --- a/examples/randomized_signature_bench.rs +++ b/examples/randomized_signature_bench.rs @@ -19,6 +19,9 @@ use std::time::Instant; struct SplitMix64(u64); impl SplitMix64 { + /// One raw 64-bit draw, advancing the state by the SplitMix64 gamma + /// constant. Deterministic given the seed, so every run of this bench + /// measures the same projections and the same path. fn next_u64(&mut self) -> u64 { self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); let mut z = self.0; @@ -26,9 +29,13 @@ impl SplitMix64 { z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); z ^ (z >> 31) } + /// A draw in `[0, 1)`, taking the top 53 bits so every value is exactly + /// representable in `f64`. fn uniform(&mut self) -> f64 { (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64 } + /// A standard-normal draw via Box-Muller (cosine branch), clamping `u1` + /// away from zero so `ln` never returns `-inf`. fn normal(&mut self) -> f64 { let u1 = self.uniform().max(1e-300); let u2 = self.uniform(); @@ -36,6 +43,10 @@ impl SplitMix64 { } } +/// Build the `(matrices, biases)` pair for a `d`-dimensional path and a `k` +/// -wide state: `d` stacked `k×k` Gaussian blocks plus `d` bias vectors of +/// length `k`, all scaled by `1/sqrt(k)` — the builder's own recipe, so the +/// benchmarked operand magnitudes match the consumer's. fn projections(d: usize, k: usize, seed: u64) -> (Vec, Vec) { let scale = (k as f64).recip().sqrt(); let mut rng = SplitMix64(seed); @@ -44,6 +55,12 @@ fn projections(d: usize, k: usize, seed: u64) -> (Vec, Vec) { (matrices, biases) } +/// A `t`-step, `d`-dimensional benchmark path: a linear ramp per coordinate +/// plus a small out-of-phase cosine wobble. +/// +/// The wobble matters for timing, not just realism — every increment stays +/// clear of the `1e-15` skip threshold, so no step is short-circuited and +/// both implementations do the full `O(T·d·k²)` of work being compared. fn path(t: usize, d: usize, seed: f64) -> Vec> { (0..=t) .map(|i| { @@ -94,6 +111,12 @@ fn scalar_encode(path: &[Vec], matrices: &[f64], biases: &[f64], k: usize) z } +/// Max relative error between two states, with the denominator floored at +/// `1.0` so near-zero rows report absolute rather than exploding error. +/// +/// Reported alongside every timing row: a speedup is only meaningful if the +/// two implementations still agree, and this is what makes a "fast but wrong" +/// kernel visible in the bench output instead of silently applauded. fn rel_err(a: &[f64], b: &[f64]) -> f64 { a.iter() .zip(b.iter()) @@ -101,6 +124,13 @@ fn rel_err(a: &[f64], b: &[f64]) -> f64 { .fold(0.0f64, f64::max) } +/// Time one `(T, d, k)` shape both ways and print the row: scalar seconds, +/// SIMD seconds, speedup, and max relative error. +/// +/// Single-shot timing, not a statistical harness — the shapes here run long +/// enough (milliseconds and up) that run-to-run noise stays well below the +/// effect being reported. Both implementations see the identical projections +/// and path, so the comparison is like-for-like. fn bench_one(t: usize, d: usize, k: usize) { let (matrices, biases) = projections(d, k, 0xBEEF); let p = path(t, d, 0.3); @@ -121,6 +151,14 @@ fn bench_one(t: usize, d: usize, k: usize) { ); } +/// Sweep the state widths `k = 32 … 512` against the scalar baseline, then +/// run sigker's headline envelope shape (`T = 64, d = 8, k = 4096`) +/// SIMD-only and report its achieved GFLOP/s. +/// +/// The envelope row omits a scalar baseline deliberately: at `k = 4096` the +/// row-major loop runs for minutes, and the row is there to show the shape is +/// reachable at all, not to claim a speedup. Its `|z|_inf` is printed so the +/// result is visibly finite rather than a NaN that timed well. fn main() { println!("== randomized_signature_sweep — SIMD GEMV+axpy vs. row-major scalar ==\n"); diff --git a/src/hpc/mod.rs b/src/hpc/mod.rs index 8e6f8f34..4d7df139 100644 --- a/src/hpc/mod.rs +++ b/src/hpc/mod.rs @@ -327,6 +327,15 @@ mod e2e_tests { use super::node::{Node, _P_, __O, SPO, S__}; use super::seal::Seal; + /// Fingerprint -> Node -> Seal: two independently seeded nodes are + /// *comparable* (non-zero overlap) and *distinct* (non-zero disagreement), + /// and a plane's Merkle root detects mutation. + /// + /// The seal half is the discriminating one. `verify` returns `Wisdom` against + /// the root it was built from, then `Staunen` after a single further + /// `encounter` -- so a `merkle()` that ignored later writes, or a `verify` + /// that always agreed, fails here. The plane is rebuilt from scratch rather + /// than reusing the nodes above, to keep the root deterministic. #[test] fn pipeline_fingerprint_to_node_to_seal() { // 1. Create two nodes and accumulate evidence @@ -357,6 +366,13 @@ mod e2e_tests { assert_eq!(p.verify(&root), Seal::Staunen); } + /// Cascade search over a packed database of 50 random 256-byte fingerprints + /// must find the query itself at Hamming distance 0. + /// + /// The query *is* `database[0]`, so an exact self-hit is the falsifiable + /// minimum: a cascade whose band thresholds rejected the foveal band, or + /// whose stride walked the packed buffer at the wrong vector width, would + /// return results that never contain `index == 0`. #[test] fn pipeline_cascade_search() { let vec_bytes = 256; @@ -384,6 +400,15 @@ mod e2e_tests { assert!(results.iter().any(|r| r.index == 0 && r.hamming == 0)); } + /// CLAM k-NN over one-hot vectors: brute force returns exactly `k` hits with + /// the self-match ranked first at distance 0, and `ClamTree::build` covers + /// the whole input. + /// + /// The data is deliberately one-hot (vector `i` sets byte `i % vec_len`), so + /// distances are uniform apart from the self-match and the ranking has a + /// single unambiguous winner. `root().cardinality == n` is the coverage + /// check -- a tree that dropped points during partitioning still builds + /// non-empty, so `!nodes.is_empty()` alone would not catch it. #[test] fn pipeline_clam_knn() { let vec_len = 32; @@ -409,6 +434,13 @@ mod e2e_tests { assert_eq!(tree.root().cardinality, n); } + /// Causality decomposition reads the *sign* of each qualia channel: + /// positive resonance decomposes to `Forward`, negative to `Backward`. + /// + /// Three channels are set with mixed signs (warmth +, social -, sacredness +) + /// against a zero baseline, so the test discriminates in a way a single + /// channel could not: a decomposition that ignored sign, or applied one + /// direction uniformly across channels, matches on at most two of the three. #[test] fn pipeline_causality_decomposition() { let mut a = PackedQualia::zero(); @@ -423,6 +455,12 @@ mod e2e_tests { assert_eq!(dec.sacredness_dir, CausalityDirection::Forward); } + /// Binary neural dot over 16384-bit fingerprints, pinned at both poles: + /// all-ones against all-ones is a full 16384 matches scoring `+1.0`, and + /// all-zeros against all-ones is 0 matches scoring `-1.0`. + /// + /// Testing both ends fixes the whole affine mapping from match count to + /// score. Either pole alone would admit a wrong scale or offset. #[test] fn pipeline_bnn_inference() { let act = Fingerprint::<256>::ones(); @@ -437,6 +475,13 @@ mod e2e_tests { assert!((result2.score - (-1.0)).abs() < 1e-6); } + /// Blackboard typed-slot arena: allocate two differently typed slots, mutate + /// each through `get_mut`, and read the mutations back through `get`. + /// + /// Both a present and an absent key are asserted against `contains`, so the + /// membership check cannot pass by unconditionally returning `true` -- and + /// the read-back is what proves `get_mut` hands out a reference into the + /// arena rather than a copy that is dropped. #[test] fn pipeline_blackboard_arena() { let mut bb = Blackboard::new(); @@ -458,6 +503,15 @@ mod e2e_tests { assert!(!bb.contains("nonexistent")); } + /// The whole chain in one pass: `Node` -> truth -> per-plane distance -> + /// Merkle seal -> cascade band -> BNN inference. + /// + /// Where the tests above each pin one stage, this one pins that the stages + /// *compose* on the same pair of nodes -- every plane mask (`S__`, `_P_`, + /// `__O`, `SPO`) yields `Measured` rather than `Incomparable`, and the + /// fingerprints that come out the far end still score strictly inside + /// `(-1, 1)`, which a stage that silently zeroed or saturated its output + /// would violate. #[test] fn pipeline_full_e2e() { // Full pipeline: Node → truth → causality → cascade → BNN diff --git a/src/hpc/randomized_signature.rs b/src/hpc/randomized_signature.rs index 5bd4c144..f7465a86 100644 --- a/src/hpc/randomized_signature.rs +++ b/src/hpc/randomized_signature.rs @@ -345,6 +345,9 @@ mod tests { struct SplitMix64(u64); impl SplitMix64 { + /// One raw 64-bit draw, advancing the state by the SplitMix64 gamma + /// constant. Deterministic given the seed — every corpus below is + /// reproducible from its seed alone. fn next_u64(&mut self) -> u64 { self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); let mut z = self.0; @@ -352,9 +355,13 @@ mod tests { z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); z ^ (z >> 31) } + /// A draw in `[0, 1)`, taking the top 53 bits so every value is + /// exactly representable in `f64`. fn uniform(&mut self) -> f64 { (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64 } + /// A standard-normal draw via Box-Muller (cosine branch), clamping + /// `u1` away from zero so `ln` never returns `-inf`. fn normal(&mut self) -> f64 { let u1 = self.uniform().max(1e-300); let u2 = self.uniform(); @@ -371,6 +378,10 @@ mod tests { (matrices, biases) } + /// A `t`-step, `d`-dimensional test path: a linear ramp per coordinate + /// plus a small out-of-phase cosine wobble, so consecutive increments are + /// non-zero, non-constant, and differ across coordinates. `seed` shifts + /// the phase, giving independent-looking paths without an RNG. fn wiggly_path(t: usize, d: usize, seed: f64) -> Vec> { (0..=t) .map(|i| { @@ -385,6 +396,15 @@ mod tests { .collect() } + /// The parity harness: run `path` through both the SIMD sweep and the + /// scalar oracle over the same seeded projections and assert they agree + /// row-wise to the module's declared `1e-9` relative tolerance. + /// + /// The tolerance is a contract, not a fudge factor — the SIMD GEMV + /// reduces eight partial sums and fuses its products, so it is not + /// bit-identical to the oracle by construction (see the module docs). + /// Also asserts the returned width is `state_dim`, so a silently + /// truncated state cannot pass by matching on a prefix. fn assert_matches_reference(path: &[Vec], d: usize, k: usize, seed: u64) { let (matrices, biases) = projections(d, k, seed); let expected = scalar_reference(path, &matrices, &biases, k); @@ -401,14 +421,22 @@ mod tests { } } + /// Parity with the scalar oracle across state widths that straddle the + /// `F64x8` lane boundary in both directions — `1, 7` (under), `8, 16, 64` + /// (exact multiples), `9, 33` (one past). This is the test that catches a + /// mishandled remainder loop: a kernel that only walked whole lanes would + /// pass at `k = 8` and fail at `k = 9`. #[test] fn parity_across_state_widths() { - // Widths straddling the lane boundary in both directions. for &k in &[1usize, 7, 8, 9, 16, 33, 64] { assert_matches_reference(&wiggly_path(12, 3, 0.4), 3, k, 0xDEAD_BEEF); } } + /// Parity across path dimensions `d`, the axis that selects which of the + /// `d` stacked `k×k` blocks of `matrices` a step reads. Fixes `k` and + /// varies `d` alone, so a wrong `i * k * k` block stride shows up here + /// rather than hiding behind a state-width failure. #[test] fn parity_across_path_dimensions() { for &d in &[1usize, 2, 5, 9] { @@ -416,10 +444,12 @@ mod tests { } } + /// Parity over 60 distinct `(seed, T, d, k)` draws — the consumer + /// contract's "hand-roll 50+ inputs" corpus, every case carrying a fresh + /// Gaussian projection. Fixed root seed, so a failure names a + /// reproducible case rather than a flake. #[test] fn parity_over_seeded_corpus() { - // 60 distinct (seed, T, d, k) draws — the contract's "hand-roll 50+ - // inputs" corpus, every one of them a fresh Gaussian projection. let mut rng = SplitMix64(0xC0FF_EE00); for case in 0..60u64 { let t = 2 + (rng.next_u64() % 9) as usize; @@ -429,12 +459,19 @@ mod tests { } } + /// Parity on a case that is past one lane in every dimension with no + /// dimension a multiple of 8 (`T = 37`, `k = 67`): enough accumulated + /// steps for a drifting reduction order to exceed tolerance, and a + /// remainder tail on every GEMV row. #[test] fn parity_with_long_path_and_wide_state() { - // Past one lane in every dimension, none of them a multiple of 8. assert_matches_reference(&wiggly_path(37, 3, 0.9), 3, 67, 7); } + /// A path with no increments — a single point, or empty — yields the zero + /// state rather than panicking. `windows(2)` is empty in both cases, so + /// `z` never leaves `z_0 = 0`; this pins that as documented behaviour and + /// not an accident of the loop shape. #[test] fn degenerate_single_point_path_is_zero_state() { let (matrices, biases) = projections(3, 11, 5); @@ -444,26 +481,31 @@ mod tests { assert_eq!(randomized_signature_sweep(&empty, &matrices, &biases, 11), vec![0.0; 11]); } + /// A constant path has `|dx_i| = 0 < 1e-15` on every coordinate, so every + /// GEMV is skipped and `z` never leaves `z_0 = 0`. The invariant is + /// hand-derived from the skip rule, not read off the code under test. + /// + /// On its own this test is weak — drop the skip entirely and the result + /// is still zero here, because `dx = 0` scales the axpy to nothing. What + /// makes the guard falsifiable is the paired supra-epsilon half in + /// [`sub_epsilon_increment_is_skipped_but_supra_epsilon_is_not`]. #[test] fn zero_increment_path_leaves_state_at_zero() { - // A constant path has |dx_i| = 0 < 1e-15 on every coordinate, so - // every GEMV is skipped and z never leaves z_0 = 0. This is a real - // invariant of the recurrence (hand-derived from the skip rule, not - // read off the code under test) and it discriminates: drop the skip - // and the result is still 0 here, but *flip the skip into an - // unconditional apply with a non-zero dx* and it is not — which the - // next test covers. let (matrices, biases) = projections(2, 20, 11); let path: Vec> = (0..15).map(|_| vec![7.0, -3.0]).collect(); assert_eq!(randomized_signature_sweep(&path, &matrices, &biases, 20), vec![0.0; 20]); } + /// The [`INCREMENT_EPSILON`] guard must actually gate, in both + /// directions: an increment just under `1e-15` leaves the state + /// untouched, one just over it moves it. + /// + /// Both halves are required. A can-it-fire assertion alone would pass for + /// a primitive with no guard; a can-it-stay-silent assertion alone (the + /// constant-path test) would pass for one that skipped everything. Only + /// the pair proves the threshold discriminates. #[test] fn sub_epsilon_increment_is_skipped_but_supra_epsilon_is_not() { - // The 1e-15 guard must actually gate: an increment just under it - // leaves the state untouched, one just over it does not. Without - // both halves the constant-path test above would pass for a - // primitive that had no guard at all. let (matrices, biases) = projections(1, 12, 3); let below = vec![vec![0.0], vec![1e-16]]; let above = vec![vec![0.0], vec![1e-13]]; @@ -473,11 +515,16 @@ mod tests { assert!(z_above.iter().any(|v| *v != 0.0), "supra-epsilon increment must move the state"); } + /// The caller's activation closure is genuinely applied, not decoration + /// over a hard-wired `tanh`. + /// + /// With `sigma == identity` the first step collapses to `z_1 = b * dx` + /// exactly — `z_0 = 0`, so `A · z_0 = 0` — which is checkable in closed + /// form against `biases`. The second half then asserts `tanh` gives a + /// *different* answer on the same inputs, so the test cannot pass by the + /// two activations happening to agree. #[test] fn custom_activation_is_the_one_applied() { - // sigma == identity turns the first step into z_1 = b * dx exactly - // (z_0 = 0, so A . z_0 = 0), which is checkable in closed form — - // proving the closure is used rather than tanh being hard-wired. let (matrices, biases) = projections(1, 9, 17); let path = vec![vec![0.0], vec![2.0]]; let z = randomized_signature_sweep_with(&path, &matrices, &biases, 9, |x| x); @@ -492,6 +539,13 @@ mod tests { .any(|(a, b)| (a - b).abs() > 1e-9)); } + /// The two public entry points agree: [`randomized_signature_step`] from + /// the zero state reproduces [`randomized_signature_sweep`] over a + /// two-point path with the same increment, bit for bit. + /// + /// Asserted with `assert_eq!` rather than a tolerance — both paths run + /// the identical `step_into` kernel, so any drift here means the sweep + /// wrapper has diverged from the step wrapper, not a numerics issue. #[test] fn step_matches_one_sweep_iteration() { let (matrices, biases) = projections(2, 13, 23); @@ -501,12 +555,20 @@ mod tests { assert_eq!(swept, stepped); } + /// A `matrices` buffer that is not `d * k * k` long panics with a message + /// naming the field, instead of silently indexing a short buffer or + /// inferring `k` from the length it happens to have. Lengths are checked, + /// never inferred. #[test] #[should_panic(expected = "matrices must hold")] fn wrong_matrix_length_panics() { let _ = randomized_signature_sweep(&[vec![0.0], vec![1.0]], &[0.0; 3], &[0.0; 2], 2); } + /// The `biases` counterpart to [`wrong_matrix_length_panics`]: a correct + /// `matrices` with a short `biases` must still be caught, so the two + /// length checks are separately falsifiable rather than one guard that + /// happens to cover both. #[test] #[should_panic(expected = "biases must hold")] fn wrong_bias_length_panics() {