|
| 1 | +//! PROBE-MANTISSA-FILL + PROBE-PHASE-1 — Wave-0 probes against shipped code. |
| 2 | +//! |
| 3 | +//! Per `OGAR/docs/INTEGRATION-TEST-PLAN.md` §1 (the probe-first rule: no |
| 4 | +//! integration brick lands before its probe is green) and the |
| 5 | +//! volumetric-field-edge proposal (implicit centroids placed by the golden |
| 6 | +//! mantissa, pairwise-weighted by the 256×256 attention LUT, splat-ranked). |
| 7 | +//! |
| 8 | +//! ## PROBE-MANTISSA-FILL |
| 9 | +//! |
| 10 | +//! Question: does the shipped golden-mantissa generator |
| 11 | +//! ([`HemispherePoint::lift`] — azimuth `n·φ`, equal-area `r = √u`) place k |
| 12 | +//! implicit centroids over a 256×256 tile MORE uniformly than seeded |
| 13 | +//! uniform-random placement on the same support (the unit disk)? |
| 14 | +//! |
| 15 | +//! Metric (discrepancy proxy, 16×16 binning over the tile, in-disk bins only): |
| 16 | +//! - `occupied`: distinct in-disk bins hit (higher = better coverage) |
| 17 | +//! - `max_bin`: worst-case pile-up (lower = better spread) |
| 18 | +//! |
| 19 | +//! PASS: at k ∈ {256, 1024}, golden beats EVERY one of three independently |
| 20 | +//! seeded uniform-random baselines on BOTH metrics (no cherry-picked seed). |
| 21 | +//! |
| 22 | +//! KILL (per the proposal's kill-condition): golden loses → the "golden |
| 23 | +//! mantissa places implicit centroids" leg falls back to an explicit grid. |
| 24 | +//! |
| 25 | +//! ## PROBE-PHASE-1 (regeneration determinism) |
| 26 | +//! |
| 27 | +//! Question: is the deterministic-phase generator bit-exact — same address ⟹ |
| 28 | +//! same sequence, across independent constructions? ([`CurveRuler`] is the |
| 29 | +//! D-QUANTGATE-mandated coprime-integer walk; a float recurrence could drift, |
| 30 | +//! an integer walk must not.) |
| 31 | +//! |
| 32 | +//! PASS: two independent `CurveRuler`s from the same `(path, depth)` produce |
| 33 | +//! identical full arcs; the arc is a full permutation of 0..17; and the |
| 34 | +//! permutation property holds for every one of the 17 possible offsets. |
| 35 | +
|
| 36 | +use helix::{CurveRuler, HemispherePoint}; |
| 37 | + |
| 38 | +const TILE: usize = 256; |
| 39 | +const BINS: usize = 16; // 16×16 bins over the 256×256 tile |
| 40 | +const BIN_W: usize = TILE / BINS; |
| 41 | + |
| 42 | +/// xorshift64 — the workspace's zero-dep seeded RNG test pattern. |
| 43 | +struct XorShift64(u64); |
| 44 | +impl XorShift64 { |
| 45 | + fn next(&mut self) -> u64 { |
| 46 | + let mut x = self.0; |
| 47 | + x ^= x << 13; |
| 48 | + x ^= x >> 7; |
| 49 | + x ^= x << 17; |
| 50 | + self.0 = x; |
| 51 | + x |
| 52 | + } |
| 53 | + /// Uniform f64 in [0, 1). |
| 54 | + fn unit(&mut self) -> f64 { |
| 55 | + (self.next() >> 11) as f64 / (1u64 << 53) as f64 |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +/// Map a unit-disk point (x, z ∈ [-1, 1]) to a tile bin index, or None if the |
| 60 | +/// pixel falls outside the 256×256 tile after scaling. |
| 61 | +fn disk_to_bin(x: f64, z: f64) -> Option<usize> { |
| 62 | + let ux = (x + 1.0) / 2.0; |
| 63 | + let uz = (z + 1.0) / 2.0; |
| 64 | + if !(0.0..1.0).contains(&ux) || !(0.0..1.0).contains(&uz) { |
| 65 | + return None; |
| 66 | + } |
| 67 | + let px = (ux * TILE as f64) as usize; |
| 68 | + let pz = (uz * TILE as f64) as usize; |
| 69 | + Some((pz / BIN_W) * BINS + (px / BIN_W)) |
| 70 | +} |
| 71 | + |
| 72 | +/// Whether a bin's center lies inside the unit disk (both generators share |
| 73 | +/// the disk as support; corner bins are unreachable for both, so the metric |
| 74 | +/// only counts bins both COULD hit). |
| 75 | +fn bin_in_disk(bin: usize) -> bool { |
| 76 | + let bx = (bin % BINS) as f64; |
| 77 | + let bz = (bin / BINS) as f64; |
| 78 | + let cx = (bx + 0.5) / BINS as f64 * 2.0 - 1.0; |
| 79 | + let cz = (bz + 0.5) / BINS as f64 * 2.0 - 1.0; |
| 80 | + cx * cx + cz * cz <= 1.0 |
| 81 | +} |
| 82 | + |
| 83 | +/// (occupied in-disk bins, max single-bin count) for a set of disk points. |
| 84 | +fn fill_metrics(points: impl Iterator<Item = (f64, f64)>) -> (usize, usize) { |
| 85 | + let mut counts = [0usize; BINS * BINS]; |
| 86 | + for (x, z) in points { |
| 87 | + if let Some(b) = disk_to_bin(x, z) { |
| 88 | + counts[b] += 1; |
| 89 | + } |
| 90 | + } |
| 91 | + let occupied = (0..BINS * BINS) |
| 92 | + .filter(|&b| bin_in_disk(b) && counts[b] > 0) |
| 93 | + .count(); |
| 94 | + let max_bin = counts.iter().copied().max().unwrap_or(0); |
| 95 | + (occupied, max_bin) |
| 96 | +} |
| 97 | + |
| 98 | +fn golden_points(k: usize) -> Vec<(f64, f64)> { |
| 99 | + (0..k) |
| 100 | + .map(|n| { |
| 101 | + let p = HemispherePoint::lift(n, k); |
| 102 | + let (x, z, _y) = p.cartesian(); |
| 103 | + (x, z) |
| 104 | + }) |
| 105 | + .collect() |
| 106 | +} |
| 107 | + |
| 108 | +fn random_disk_points(k: usize, seed: u64) -> Vec<(f64, f64)> { |
| 109 | + // Rejection-sample uniform points in the unit disk (same support as the |
| 110 | + // golden generator) so the comparison is geometry-fair. |
| 111 | + let mut rng = XorShift64(seed); |
| 112 | + let mut out = Vec::with_capacity(k); |
| 113 | + while out.len() < k { |
| 114 | + let x = rng.unit() * 2.0 - 1.0; |
| 115 | + let z = rng.unit() * 2.0 - 1.0; |
| 116 | + if x * x + z * z < 1.0 { |
| 117 | + out.push((x, z)); |
| 118 | + } |
| 119 | + } |
| 120 | + out |
| 121 | +} |
| 122 | + |
| 123 | +#[test] |
| 124 | +fn probe_mantissa_fill_golden_beats_uniform_random() { |
| 125 | + // Three independent baseline seeds — golden must beat ALL of them on |
| 126 | + // BOTH metrics at BOTH sample counts; no cherry-picking. |
| 127 | + const SEEDS: [u64; 3] = [0x9E37_79B9_7F4A_7C15, 0xD1B5_4A32_D192_ED03, 0x2545_F491_4F6C_DD1D]; |
| 128 | + |
| 129 | + for &k in &[256usize, 1024] { |
| 130 | + let (g_occ, g_max) = fill_metrics(golden_points(k).into_iter()); |
| 131 | + for &seed in &SEEDS { |
| 132 | + let (r_occ, r_max) = fill_metrics(random_disk_points(k, seed).into_iter()); |
| 133 | + // STRICT inequalities — Codex P2 on #485 caught the wording mismatch: |
| 134 | + // the doc says golden BEATS every seed, but `>=`/`<=` silently |
| 135 | + // admit ties; a future regression that merely ties would still |
| 136 | + // pass under the loose form and be reported as a win. Strict |
| 137 | + // gate matches the prose. (The measured numbers strictly |
| 138 | + // satisfy this form, so tightening loses no data — see receipts |
| 139 | + // in EPIPHANIES E-PROBE-MANTISSA-1.) |
| 140 | + assert!( |
| 141 | + g_occ > r_occ, |
| 142 | + "k={k} seed={seed:#x}: golden occupied {g_occ} did not STRICTLY beat random {r_occ} — \ |
| 143 | + MANTISSA-FILL RED: golden mantissa does not strictly out-cover uniform random" |
| 144 | + ); |
| 145 | + assert!( |
| 146 | + g_max < r_max, |
| 147 | + "k={k} seed={seed:#x}: golden max-bin {g_max} did not STRICTLY beat random {r_max} — \ |
| 148 | + MANTISSA-FILL RED: golden mantissa does not strictly out-spread uniform random" |
| 149 | + ); |
| 150 | + } |
| 151 | + // Print the receipt numbers so the probe run is quotable. |
| 152 | + println!("MANTISSA-FILL k={k}: golden occupied={g_occ} max_bin={g_max}"); |
| 153 | + for &seed in &SEEDS { |
| 154 | + let (r_occ, r_max) = fill_metrics(random_disk_points(k, seed).into_iter()); |
| 155 | + println!(" random seed={seed:#x}: occupied={r_occ} max_bin={r_max}"); |
| 156 | + } |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +#[test] |
| 161 | +fn probe_mantissa_fill_no_empty_inner_region_at_1024() { |
| 162 | + // Stronger coverage claim at k=1024: every in-disk bin whose center is |
| 163 | + // comfortably interior (radius ≤ 0.9) must be occupied by golden points. |
| 164 | + let mut counts = [0usize; BINS * BINS]; |
| 165 | + for (x, z) in golden_points(1024) { |
| 166 | + if let Some(b) = disk_to_bin(x, z) { |
| 167 | + counts[b] += 1; |
| 168 | + } |
| 169 | + } |
| 170 | + for b in 0..BINS * BINS { |
| 171 | + let bx = (b % BINS) as f64; |
| 172 | + let bz = (b / BINS) as f64; |
| 173 | + let cx = (bx + 0.5) / BINS as f64 * 2.0 - 1.0; |
| 174 | + let cz = (bz + 0.5) / BINS as f64 * 2.0 - 1.0; |
| 175 | + if cx * cx + cz * cz <= 0.81 { |
| 176 | + assert!( |
| 177 | + counts[b] > 0, |
| 178 | + "interior bin {b} (center {cx:.2},{cz:.2}) EMPTY at k=1024 — \ |
| 179 | + golden mantissa leaves interior holes" |
| 180 | + ); |
| 181 | + } |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +#[test] |
| 186 | +fn probe_phase1_curve_ruler_regeneration_is_bit_exact() { |
| 187 | + // Same address ⟹ same sequence, across independent constructions. |
| 188 | + for path in [0u64, 1, 0x1234, u64::MAX, 0xDEAD_BEEF_CAFE_F00D] { |
| 189 | + for depth in [0u8, 1, 7, 16] { |
| 190 | + let a = CurveRuler::from_hhtl(path, depth); |
| 191 | + let b = CurveRuler::from_hhtl(path, depth); |
| 192 | + assert_eq!(a.arc(), b.arc(), "regeneration drift at ({path:#x},{depth})"); |
| 193 | + } |
| 194 | + } |
| 195 | +} |
| 196 | + |
| 197 | +#[test] |
| 198 | +fn probe_phase1_full_permutation_for_every_offset() { |
| 199 | + // The stride-4-over-17 arc must be a full permutation of 0..17 from every |
| 200 | + // possible start offset (coprimality must hold everywhere, not just at 0). |
| 201 | + for place in 0u64..17 { |
| 202 | + let ruler = CurveRuler::from_place(place); |
| 203 | + let arc = ruler.arc(); |
| 204 | + let mut seen = [false; 17]; |
| 205 | + for &v in &arc { |
| 206 | + assert!(v < 17, "residue {v} out of range"); |
| 207 | + assert!(!seen[v as usize], "residue {v} repeated at place {place}"); |
| 208 | + seen[v as usize] = true; |
| 209 | + } |
| 210 | + assert!(seen.iter().all(|&s| s), "incomplete permutation at place {place}"); |
| 211 | + } |
| 212 | +} |
0 commit comments