|
| 1 | +//! Probe 2 — the **witness arc as a standing wave** (METHODS §11). |
| 2 | +//! |
| 3 | +//! A *witness arc* is a Markov #1 reference chain over the SoA — a path that reads |
| 4 | +//! a field by accumulating it along the chain. METHODS §11 says the same arc can be |
| 5 | +//! evaluated two ways, and they must agree: |
| 6 | +//! |
| 7 | +//! - **Particle view (pointer chasing).** Walk the arc hop by hop, accumulating the |
| 8 | +//! field along it: `∑ field[i]·arc[i]`. One dependent load per hop — `O(hops)` |
| 9 | +//! sequential dereferences (the `CausalEdge64` W-slot → witness chain walk). |
| 10 | +//! - **Wave view (standing wave).** The arc's reading IS an inner product against |
| 11 | +//! the field, and by **Parseval for the Walsh–Hadamard transform** (`Hᵀ H = N·I`, |
| 12 | +//! the involution-up-to-`N` the pyramid already provides via [`fwht`]): |
| 13 | +//! `⟨field, arc⟩ = (1/N)·⟨Ĥfield, Ĥarc⟩`. Transform the field **once** |
| 14 | +//! (`O(N log N)`); then every witness arc is an `O(N)` dot against its spectrum — |
| 15 | +//! the whole arc is evaluated at once, no walk. |
| 16 | +//! |
| 17 | +//! **The identity `particle == wave` is what this probe proves** ([`particle_equals_wave`]). |
| 18 | +//! The payoff is amortization: [`field_spectrum`] computes the standing wave once, |
| 19 | +//! and [`witness_from_spectrum`] reads many arcs off it — `O(N log N) + q·O(N)` for |
| 20 | +//! `q` arcs, vs the particle view's `q` independent pointer-chasing walks. |
| 21 | +//! |
| 22 | +//! Self-contained, in perturbation-sim — this demonstrates the pyramid/field |
| 23 | +//! mechanism on a grid field (e.g. the [`crate::inertia_buffer_column`] field). |
| 24 | +//! Wiring it as the *actual* `witness_table` evaluator in the contract is a separate, |
| 25 | +//! gated step: the witness/SoA types are the cognitive spine — additive only, behind |
| 26 | +//! the iron rules. |
| 27 | +
|
| 28 | +use crate::sketch::fwht; |
| 29 | + |
| 30 | +/// Pad a field to the next power-of-two length (zero-filled), the length [`fwht`] |
| 31 | +/// requires. Returns the padded buffer. |
| 32 | +fn pad_pow2(v: &[f64]) -> Vec<f64> { |
| 33 | + let mut n = 1usize; |
| 34 | + while n < v.len().max(1) { |
| 35 | + n <<= 1; |
| 36 | + } |
| 37 | + let mut a = vec![0.0; n]; |
| 38 | + a[..v.len()].copy_from_slice(v); |
| 39 | + a |
| 40 | +} |
| 41 | + |
| 42 | +/// **Particle view.** The witness arc read by walking it: `∑ field[i]·arc[i]`, the |
| 43 | +/// pointer-chase accumulation (`O(len)` sequential). `field` and `arc` are read up to |
| 44 | +/// their shared length. |
| 45 | +pub fn witness_particle(field: &[f64], arc: &[f64]) -> f64 { |
| 46 | + field.iter().zip(arc).map(|(f, a)| f * a).sum() |
| 47 | +} |
| 48 | + |
| 49 | +/// The field's **standing-wave spectrum** — the Walsh–Hadamard pyramid of the field, |
| 50 | +/// computed ONCE. Reuse across many witness arcs via [`witness_from_spectrum`]. |
| 51 | +pub fn field_spectrum(field: &[f64]) -> Vec<f64> { |
| 52 | + let mut a = pad_pow2(field); |
| 53 | + fwht(&mut a); |
| 54 | + a |
| 55 | +} |
| 56 | + |
| 57 | +/// Read a witness arc off a precomputed field spectrum: `(1/N)·⟨spectrum, Ĥarc⟩` |
| 58 | +/// (Parseval). Equals [`witness_particle`] up to floating-point. `spectrum` must come |
| 59 | +/// from [`field_spectrum`] (length `N`, a power of two). |
| 60 | +pub fn witness_from_spectrum(spectrum: &[f64], arc: &[f64]) -> f64 { |
| 61 | + let n = spectrum.len(); |
| 62 | + if n == 0 { |
| 63 | + return 0.0; |
| 64 | + } |
| 65 | + let mut a = vec![0.0; n]; |
| 66 | + let take = arc.len().min(n); |
| 67 | + a[..take].copy_from_slice(&arc[..take]); |
| 68 | + fwht(&mut a); |
| 69 | + spectrum.iter().zip(&a).map(|(x, y)| x * y).sum::<f64>() / n as f64 |
| 70 | +} |
| 71 | + |
| 72 | +/// **Wave view.** The same reading as [`witness_particle`], via Parseval on the Walsh |
| 73 | +/// pyramid — transform field + arc, then `(1/N)·∑ Ĥfield·Ĥarc`. Self-contained |
| 74 | +/// (transforms both); for many arcs prefer [`field_spectrum`] + [`witness_from_spectrum`]. |
| 75 | +pub fn witness_wave(field: &[f64], arc: &[f64]) -> f64 { |
| 76 | + witness_from_spectrum(&field_spectrum(field), arc) |
| 77 | +} |
| 78 | + |
| 79 | +/// Convenience: does the wave view reproduce the particle view for this `(field, |
| 80 | +/// arc)` within `tol`? The probe's pass/fail predicate. |
| 81 | +pub fn particle_equals_wave(field: &[f64], arc: &[f64], tol: f64) -> bool { |
| 82 | + (witness_particle(field, arc) - witness_wave(field, arc)).abs() <= tol |
| 83 | +} |
| 84 | + |
| 85 | +#[cfg(test)] |
| 86 | +mod tests { |
| 87 | + use super::*; |
| 88 | + |
| 89 | + /// Parseval: the standing wave reproduces the pointer-chase walk exactly (fp). |
| 90 | + #[test] |
| 91 | + fn particle_equals_wave_parseval() { |
| 92 | + let field = [0.3, -1.2, 0.7, 2.1, -0.4, 0.9, 1.5, -0.8]; |
| 93 | + // A witness arc: a signed Markov reference chain over the nodes. |
| 94 | + let arc = [1.0, 0.0, -1.0, 1.0, 0.0, 0.0, -1.0, 1.0]; |
| 95 | + let p = witness_particle(&field, &arc); |
| 96 | + let w = witness_wave(&field, &arc); |
| 97 | + assert!((p - w).abs() < 1e-9, "particle {p} vs wave {w}"); |
| 98 | + assert!(particle_equals_wave(&field, &arc, 1e-9)); |
| 99 | + } |
| 100 | + |
| 101 | + /// The amortization claim: ONE field transform, then many arcs read off it — each |
| 102 | + /// matching its particle walk. |
| 103 | + #[test] |
| 104 | + fn standing_wave_reuses_one_transform() { |
| 105 | + let field = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; |
| 106 | + let spectrum = field_spectrum(&field); // computed once |
| 107 | + let arcs = [ |
| 108 | + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], // single-hop witness |
| 109 | + [1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0], // coarse dyadic arc |
| 110 | + [0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0], // alternating chain |
| 111 | + ]; |
| 112 | + for arc in &arcs { |
| 113 | + let p = witness_particle(&field, arc); |
| 114 | + let w = witness_from_spectrum(&spectrum, arc); |
| 115 | + assert!( |
| 116 | + (p - w).abs() < 1e-9, |
| 117 | + "arc {arc:?}: particle {p} vs wave {w}" |
| 118 | + ); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + /// Non-power-of-two fields are padded transparently; the identity still holds. |
| 123 | + #[test] |
| 124 | + fn handles_non_power_of_two_and_ragged_arc() { |
| 125 | + let field = [0.5, -0.5, 2.0, 1.0, -1.0]; // length 5 → pads to 8 |
| 126 | + let arc = [1.0, 1.0, -1.0]; // shorter arc → zero-extended |
| 127 | + let p = witness_particle(&field, &arc); |
| 128 | + let w = witness_wave(&field, &arc); |
| 129 | + assert!((p - w).abs() < 1e-9, "ragged: particle {p} vs wave {w}"); |
| 130 | + } |
| 131 | + |
| 132 | + /// Degenerate inputs never panic / never NaN. |
| 133 | + #[test] |
| 134 | + fn degenerate_is_safe() { |
| 135 | + assert_eq!(witness_particle(&[], &[]), 0.0); |
| 136 | + assert_eq!(witness_wave(&[], &[]), 0.0); |
| 137 | + assert!(field_spectrum(&[]).len().is_power_of_two()); |
| 138 | + assert_eq!(witness_from_spectrum(&[], &[1.0]), 0.0); |
| 139 | + } |
| 140 | +} |
0 commit comments