|
| 1 | +//! Helix ⊥ HHTL probe — the load-bearing claim of the 16-byte spec: |
| 2 | +//! `PLACE (HHTL) + RESIDUE (HELIX) = REALITY`, with the two declared ORTHOGONAL. |
| 3 | +//! |
| 4 | +//! Tests a spherical/polar decomposition of a point around its cluster centroid: |
| 5 | +//! PLACE = which centroid (the HHTL semantic basin); RADIAL = ‖point − centroid‖ |
| 6 | +//! (depth from the basin); HELIX = the residue direction, encoded to the nearest |
| 7 | +//! of 256 golden-spiral hemisphere samples (the spec's PALETTE256) + a 1-bit sign. |
| 8 | +//! |
| 9 | +//! Four questions on the reliability instrument: |
| 10 | +//! |
| 11 | +//! - ORTHOGONAL: is the helix direction independent of PLACE and RADIAL? (ρ≈0) |
| 12 | +//! - ADDS INFO: does centroid+radial+helix reconstruct far better than centroid alone? |
| 13 | +//! - SEPARATES: do golden hemisphere samples spread better than random? (min gap) |
| 14 | +//! - RELIABLE: is the encoded direction stable under observation noise? (ICC) |
| 15 | +//! |
| 16 | +//! Finding (measured): helix is a HEMISPHERE code (axis, ±v equivalent). A full |
| 17 | +//! SIGNED direction needs the 256-sample index PLUS a 1-bit sign — included here; |
| 18 | +//! WITHOUT it, reconstruction fails for half the sphere (fine_err ≈ coarse_err). |
| 19 | +//! |
| 20 | +//! cargo run --release --example helix_orthogonality_probe --features std |
| 21 | +
|
| 22 | +use ndarray::hpc::reliability::{icc_a1, spearman}; |
| 23 | + |
| 24 | +fn splitmix(s: &mut u64) -> f64 { |
| 25 | + *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); |
| 26 | + let mut z = *s; |
| 27 | + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); |
| 28 | + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); |
| 29 | + z ^= z >> 31; |
| 30 | + (z >> 11) as f64 / (1u64 << 53) as f64 |
| 31 | +} |
| 32 | + |
| 33 | +type V3 = [f64; 3]; |
| 34 | +fn dot(a: V3, b: V3) -> f64 { |
| 35 | + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] |
| 36 | +} |
| 37 | +fn sub(a: V3, b: V3) -> V3 { |
| 38 | + [a[0] - b[0], a[1] - b[1], a[2] - b[2]] |
| 39 | +} |
| 40 | +fn norm(a: V3) -> f64 { |
| 41 | + dot(a, a).sqrt() |
| 42 | +} |
| 43 | +fn unit(a: V3) -> V3 { |
| 44 | + let n = norm(a).max(1e-12); |
| 45 | + [a[0] / n, a[1] / n, a[2] / n] |
| 46 | +} |
| 47 | +fn rand_unit(s: &mut u64) -> V3 { |
| 48 | + // Marsaglia: uniform on the sphere. |
| 49 | + loop { |
| 50 | + let x = 2.0 * splitmix(s) - 1.0; |
| 51 | + let y = 2.0 * splitmix(s) - 1.0; |
| 52 | + let r2 = x * x + y * y; |
| 53 | + if r2 < 1.0 && r2 > 1e-9 { |
| 54 | + let f = 2.0 * (1.0 - r2).sqrt(); |
| 55 | + return [x * f, y * f, 1.0 - 2.0 * r2]; |
| 56 | + } |
| 57 | + } |
| 58 | +} |
| 59 | +/// Fold to the upper hemisphere (helix encodes an axis, not a signed vector). |
| 60 | +fn fold(v: V3) -> V3 { |
| 61 | + if v[2] < 0.0 { |
| 62 | + [-v[0], -v[1], -v[2]] |
| 63 | + } else { |
| 64 | + v |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +/// The 256 golden-spiral hemisphere sample directions (the spec's PALETTE256). |
| 69 | +fn golden_hemisphere(n: usize) -> Vec<V3> { |
| 70 | + let gamma = std::f64::consts::PI * (3.0 - 5.0_f64.sqrt()); // golden angle |
| 71 | + (0..n) |
| 72 | + .map(|i| { |
| 73 | + // θ = ½·arccos(1 − 2(i+0.5)/N) → equal-area on the hemisphere. |
| 74 | + let theta = 0.5 |
| 75 | + * (1.0 - 2.0 * (i as f64 + 0.5) / n as f64) |
| 76 | + .clamp(-1.0, 1.0) |
| 77 | + .acos(); |
| 78 | + let phi = (i as f64 * gamma).rem_euclid(std::f64::consts::TAU); |
| 79 | + [theta.sin() * phi.cos(), theta.sin() * phi.sin(), theta.cos()] |
| 80 | + }) |
| 81 | + .collect() |
| 82 | +} |
| 83 | + |
| 84 | +/// Encode a hemisphere direction to the nearest golden sample index. |
| 85 | +fn encode(dir: V3, palette: &[V3]) -> usize { |
| 86 | + let mut best = -2.0; |
| 87 | + let mut bi = 0; |
| 88 | + for (i, &p) in palette.iter().enumerate() { |
| 89 | + let d = dot(dir, p); |
| 90 | + if d > best { |
| 91 | + best = d; |
| 92 | + bi = i; |
| 93 | + } |
| 94 | + } |
| 95 | + bi |
| 96 | +} |
| 97 | + |
| 98 | +fn min_angular_gap(dirs: &[V3]) -> f64 { |
| 99 | + let mut min = std::f64::consts::PI; |
| 100 | + for i in 0..dirs.len() { |
| 101 | + for j in (i + 1)..dirs.len() { |
| 102 | + let ang = dot(dirs[i], dirs[j]).clamp(-1.0, 1.0).acos(); |
| 103 | + if ang < min { |
| 104 | + min = ang; |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + min |
| 109 | +} |
| 110 | + |
| 111 | +fn main() { |
| 112 | + println!("== Helix ⊥ HHTL probe: PLACE + RADIAL + HELIX(direction) spherical decomposition ==\n"); |
| 113 | + let mut s = 0x4E11_5EED_u64; |
| 114 | + let palette = golden_hemisphere(256); |
| 115 | + |
| 116 | + let k = 16usize; // PLACE: number of cluster centroids |
| 117 | + let centroids: Vec<V3> = (0..k) |
| 118 | + .map(|_| { |
| 119 | + [ |
| 120 | + 6.0 * (2.0 * splitmix(&mut s) - 1.0), |
| 121 | + 6.0 * (2.0 * splitmix(&mut s) - 1.0), |
| 122 | + 6.0 * (2.0 * splitmix(&mut s) - 1.0), |
| 123 | + ] |
| 124 | + }) |
| 125 | + .collect(); |
| 126 | + let r_max = 2.0; |
| 127 | + let noise = 0.10; |
| 128 | + let n = 6000usize; |
| 129 | + |
| 130 | + let (mut f_place, mut f_radial, mut m_helix) = (vec![], vec![], vec![]); |
| 131 | + let mut coarse_err = 0.0; |
| 132 | + let mut fine_err = 0.0; |
| 133 | + let (mut dir_clean, mut dir_noisy) = (vec![], vec![]); |
| 134 | + let mut ang_quant_err = 0.0; |
| 135 | + |
| 136 | + for _ in 0..n { |
| 137 | + let c = (splitmix(&mut s) * k as f64) as usize % k; |
| 138 | + let radial = splitmix(&mut s) * r_max; |
| 139 | + let orient = rand_unit(&mut s); // INDEPENDENT of place & radial by construction |
| 140 | + let point = [ |
| 141 | + centroids[c][0] + radial * orient[0], |
| 142 | + centroids[c][1] + radial * orient[1], |
| 143 | + centroids[c][2] + radial * orient[2], |
| 144 | + ]; |
| 145 | + |
| 146 | + // Decode the spherical code from the point: hemisphere index + 1-bit sign. |
| 147 | + let residue = sub(point, centroids[c]); |
| 148 | + let rmag = norm(residue); |
| 149 | + let ud = unit(residue); |
| 150 | + let sign_z = if ud[2] < 0.0 { -1.0 } else { 1.0 }; // the 1-bit sign |
| 151 | + let dir = fold(ud); // hemisphere (z ≥ 0) |
| 152 | + let hidx = encode(dir, &palette); |
| 153 | + let h = palette[hidx]; |
| 154 | + let recon_dir = [sign_z * h[0], sign_z * h[1], sign_z * h[2]]; // re-apply sign |
| 155 | + |
| 156 | + // Reconstruction: centroid (coarse) vs centroid + radial·helix-dir (fine). |
| 157 | + let recon = [ |
| 158 | + centroids[c][0] + rmag * recon_dir[0], |
| 159 | + centroids[c][1] + rmag * recon_dir[1], |
| 160 | + centroids[c][2] + rmag * recon_dir[2], |
| 161 | + ]; |
| 162 | + coarse_err += rmag; // ‖point − centroid‖ |
| 163 | + fine_err += norm(sub(point, recon)); |
| 164 | + ang_quant_err += dot(ud, recon_dir).clamp(-1.0, 1.0).acos(); |
| 165 | + |
| 166 | + // Reliability: re-encode under observation noise. |
| 167 | + let pn = [ |
| 168 | + point[0] + noise * (2.0 * splitmix(&mut s) - 1.0), |
| 169 | + point[1] + noise * (2.0 * splitmix(&mut s) - 1.0), |
| 170 | + point[2] + noise * (2.0 * splitmix(&mut s) - 1.0), |
| 171 | + ]; |
| 172 | + let ud2 = unit(sub(pn, centroids[c])); |
| 173 | + let sign2 = if ud2[2] < 0.0 { -1.0 } else { 1.0 }; |
| 174 | + let h2 = palette[encode(fold(ud2), &palette)]; |
| 175 | + let recon_dir2 = [sign2 * h2[0], sign2 * h2[1], sign2 * h2[2]]; |
| 176 | + for t in 0..3 { |
| 177 | + dir_clean.push(recon_dir[t]); |
| 178 | + dir_noisy.push(recon_dir2[t]); |
| 179 | + } |
| 180 | + |
| 181 | + f_place.push(c as f64); |
| 182 | + f_radial.push(radial); |
| 183 | + m_helix.push(hidx as f64); |
| 184 | + } |
| 185 | + |
| 186 | + // Orthogonality: helix index vs PLACE and vs RADIAL (expect ρ≈0). |
| 187 | + let rho_place = spearman(&f_place, &m_helix); |
| 188 | + let rho_radial = spearman(&f_radial, &m_helix); |
| 189 | + let coarse_rel = coarse_err / n as f64; |
| 190 | + let fine_rel = fine_err / n as f64; |
| 191 | + let icc_dir = icc_a1(&[&dir_clean, &dir_noisy]); |
| 192 | + let g_gap = min_angular_gap(&palette); |
| 193 | + let rand_dirs: Vec<V3> = (0..256).map(|_| fold(rand_unit(&mut s))).collect(); |
| 194 | + let r_gap = min_angular_gap(&rand_dirs); |
| 195 | + |
| 196 | + println!("orthogonality (helix direction vs the other two axes):"); |
| 197 | + println!(" ρ(PLACE, helix) = {rho_place:+.3} ρ(RADIAL, helix) = {rho_radial:+.3} (≈0 ⇒ orthogonal)"); |
| 198 | + println!("\nadds info (mean reconstruction error to the true point):"); |
| 199 | + println!(" coarse PLACE-only {coarse_rel:.4}"); |
| 200 | + println!( |
| 201 | + " + RADIAL + HELIX(dir) {fine_rel:.4} ({:.1}× lower; mean angular quant {:.1}°)", |
| 202 | + coarse_rel / fine_rel.max(1e-9), |
| 203 | + (ang_quant_err / n as f64).to_degrees() |
| 204 | + ); |
| 205 | + println!( |
| 206 | + "\nseparation (min angular gap of 256 samples): golden={:.4} rad random={:.4} rad ({:.1}× better)", |
| 207 | + g_gap, |
| 208 | + r_gap, |
| 209 | + g_gap / r_gap.max(1e-9) |
| 210 | + ); |
| 211 | + println!("reliability (ICC of reconstructed direction @ noise {noise}): {icc_dir:.3}"); |
| 212 | + |
| 213 | + let orthogonal = rho_place.abs() <= 0.1 && rho_radial.abs() <= 0.1; |
| 214 | + let adds_info = fine_rel < 0.5 * coarse_rel; |
| 215 | + let separates = g_gap > 1.5 * r_gap; |
| 216 | + let reliable = icc_dir >= 0.7; |
| 217 | + let mark = |b: bool| if b { "PASS" } else { "FAIL" }; |
| 218 | + println!("\nVERDICT:"); |
| 219 | + println!(" orthogonal (helix ⊥ place, ⊥ radial) ............... {}", mark(orthogonal)); |
| 220 | + println!(" adds info (helix recovers the residual direction) .. {}", mark(adds_info)); |
| 221 | + println!(" golden separation > random ......................... {}", mark(separates)); |
| 222 | + println!(" reliable under noise (ICC ≥ 0.7) ................... {}", mark(reliable)); |
| 223 | + let all = orthogonal && adds_info && separates && reliable; |
| 224 | + println!( |
| 225 | + "\n ⇒ {}", |
| 226 | + if all { |
| 227 | + "PLACE ⊥ RADIAL ⊥ HELIX confirmed — helix is a real orthogonal residue axis on top of HHTL (spec claim holds)" |
| 228 | + } else { |
| 229 | + "spec claim PARTIAL — see failed cells" |
| 230 | + } |
| 231 | + ); |
| 232 | +} |
0 commit comments