|
| 1 | +//! Measurement-instrument hypothesis probe (MTMM) — syntax=angle, semantics= |
| 2 | +//! location-residue, episodic-basin=phase. Tests the THREE as an instrument, not |
| 3 | +//! as assumed fact, via the reliability suite (Spearman + ICC). |
| 4 | +//! |
| 5 | +//! The hypothesis: these are three ORTHOGONAL measurement axes. The probe builds |
| 6 | +//! a synthetic population where three latent factors vary independently — `syn` |
| 7 | +//! (a pairwise relation = the angle imposed between s and o), `sem` (a semantic |
| 8 | +//! location = residue magnitude from a centroid), and `epi` (an episode index → |
| 9 | +//! golden phase) — then measures each axis from the fingerprints and asks the |
| 10 | +//! reliability suite four multitrait-multimethod questions: |
| 11 | +//! |
| 12 | +//! - CONVERGENT: does each measure track its own factor? (diagonal high) |
| 13 | +//! - DISCRIMINANT: does it stay flat on the others? (off-diagonal ~0) |
| 14 | +//! - RELIABLE: is it stable under observation noise? (ICC test-retest) |
| 15 | +//! - PHASE SEPARATES: does the golden phase spread episodes? (anti-collapse) |
| 16 | +//! |
| 17 | +//! A clean positive = the reframing is a real instrument. Any failed cell = the |
| 18 | +//! operationalization conflates and must be corrected (not assumed). |
| 19 | +//! |
| 20 | +//! cargo run --release --example instrument_mtmm_probe --features std |
| 21 | +
|
| 22 | +use ndarray::hpc::reliability::{icc_a1, spearman}; |
| 23 | + |
| 24 | +const D: usize = 24; |
| 25 | +const K: usize = 8; |
| 26 | + |
| 27 | +fn splitmix(s: &mut u64) -> f64 { |
| 28 | + *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); |
| 29 | + let mut z = *s; |
| 30 | + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); |
| 31 | + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); |
| 32 | + z ^= z >> 31; |
| 33 | + (z >> 11) as f64 / (1u64 << 53) as f64 |
| 34 | +} |
| 35 | + |
| 36 | +fn randn(s: &mut u64) -> f64 { |
| 37 | + // Box-Muller. |
| 38 | + let u1 = splitmix(s).max(1e-12); |
| 39 | + let u2 = splitmix(s); |
| 40 | + (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos() |
| 41 | +} |
| 42 | + |
| 43 | +fn dot(a: &[f64], b: &[f64]) -> f64 { |
| 44 | + a.iter().zip(b).map(|(x, y)| x * y).sum() |
| 45 | +} |
| 46 | +fn norm(a: &[f64]) -> f64 { |
| 47 | + dot(a, a).sqrt() |
| 48 | +} |
| 49 | +fn unit(s: &mut u64) -> Vec<f64> { |
| 50 | + let v: Vec<f64> = (0..D).map(|_| randn(s)).collect(); |
| 51 | + let n = norm(&v).max(1e-12); |
| 52 | + v.iter().map(|x| x / n).collect() |
| 53 | +} |
| 54 | + |
| 55 | +fn angle_between(a: &[f64], b: &[f64]) -> f64 { |
| 56 | + (dot(a, b) / (norm(a) * norm(b)).max(1e-12)) |
| 57 | + .clamp(-1.0, 1.0) |
| 58 | + .acos() |
| 59 | +} |
| 60 | + |
| 61 | +fn residue_to_nearest(v: &[f64], centroids: &[Vec<f64>]) -> f64 { |
| 62 | + centroids |
| 63 | + .iter() |
| 64 | + .map(|c| { |
| 65 | + v.iter() |
| 66 | + .zip(c) |
| 67 | + .map(|(x, y)| (x - y) * (x - y)) |
| 68 | + .sum::<f64>() |
| 69 | + .sqrt() |
| 70 | + }) |
| 71 | + .fold(f64::INFINITY, f64::min) |
| 72 | +} |
| 73 | + |
| 74 | +/// Min circular gap of phases in [0, 2π) — the basin-separation metric. |
| 75 | +fn min_circular_gap(phases: &[f64]) -> f64 { |
| 76 | + let mut p: Vec<f64> = phases.to_vec(); |
| 77 | + p.sort_by(|a, b| a.partial_cmp(b).unwrap()); |
| 78 | + let mut min = f64::INFINITY; |
| 79 | + for i in 0..p.len() { |
| 80 | + let gap = if i + 1 < p.len() { |
| 81 | + p[i + 1] - p[i] |
| 82 | + } else { |
| 83 | + p[0] + std::f64::consts::TAU - p[i] |
| 84 | + }; |
| 85 | + min = min.min(gap); |
| 86 | + } |
| 87 | + min |
| 88 | +} |
| 89 | + |
| 90 | +fn main() { |
| 91 | + println!("== Measurement-instrument MTMM probe: syntax=angle · semantics=residue · basin=phase ==\n"); |
| 92 | + let mut s = 0x5EED_4A2B_u64; |
| 93 | + let golden = std::f64::consts::PI * (3.0 - 5.0_f64.sqrt()); |
| 94 | + |
| 95 | + // Well-separated semantic centroids (spacing >> residue range R so the |
| 96 | + // nearest-centroid stays the generating one — keeps residue = a clean |
| 97 | + // location measure). |
| 98 | + let centroids: Vec<Vec<f64>> = (0..K) |
| 99 | + .map(|_| unit(&mut s).iter().map(|x| x * 6.0).collect()) |
| 100 | + .collect(); |
| 101 | + let r_max = 1.5; |
| 102 | + let noise = 0.18; // observation noise for the test-retest reliability leg |
| 103 | + |
| 104 | + let n = 6000usize; |
| 105 | + let (mut f_syn, mut f_sem, mut f_epi) = (vec![], vec![], vec![]); |
| 106 | + let (mut m_angle, mut m_res, mut m_phase) = (vec![], vec![], vec![]); |
| 107 | + let (mut m_angle2, mut m_res2) = (vec![], vec![]); // noisy retest |
| 108 | + let mut golden_phase = vec![]; |
| 109 | + let mut random_phase = vec![]; |
| 110 | + |
| 111 | + for i in 0..n { |
| 112 | + let c = (splitmix(&mut s) * K as f64) as usize % K; |
| 113 | + let sem = splitmix(&mut s) * r_max; // semantic residue magnitude |
| 114 | + let syn = 0.15 + splitmix(&mut s) * (std::f64::consts::PI - 0.30); // relation angle |
| 115 | + let epi = i as f64; |
| 116 | + |
| 117 | + // s_fp = centroid + sem·dir (its residue from the nearest centroid = sem). |
| 118 | + let dir = unit(&mut s); |
| 119 | + let s_fp: Vec<f64> = centroids[c] |
| 120 | + .iter() |
| 121 | + .zip(&dir) |
| 122 | + .map(|(cc, d)| cc + sem * d) |
| 123 | + .collect(); |
| 124 | + // o_fp imposes EXACTLY angle `syn` to s_fp, independent of s_fp's location: |
| 125 | + // o = cos(syn)·s + sin(syn)·|s|·perp, perp ⊥ s. |
| 126 | + let rnd = unit(&mut s); |
| 127 | + let proj = dot(&rnd, &s_fp) / dot(&s_fp, &s_fp).max(1e-12); |
| 128 | + let mut perp: Vec<f64> = rnd.iter().zip(&s_fp).map(|(r, sf)| r - proj * sf).collect(); |
| 129 | + let pn = norm(&perp).max(1e-12); |
| 130 | + for x in perp.iter_mut() { |
| 131 | + *x /= pn; |
| 132 | + } |
| 133 | + let sn = norm(&s_fp); |
| 134 | + let o_fp: Vec<f64> = s_fp |
| 135 | + .iter() |
| 136 | + .zip(&perp) |
| 137 | + .map(|(sf, pp)| syn.cos() * sf + syn.sin() * sn * pp) |
| 138 | + .collect(); |
| 139 | + |
| 140 | + // Clean measures. |
| 141 | + m_angle.push(angle_between(&s_fp, &o_fp)); |
| 142 | + m_res.push(residue_to_nearest(&s_fp, ¢roids)); |
| 143 | + let ph = (epi * golden).rem_euclid(std::f64::consts::TAU); |
| 144 | + m_phase.push(ph); |
| 145 | + |
| 146 | + // Noisy retest (observation noise on both fingerprints). |
| 147 | + let s2: Vec<f64> = s_fp.iter().map(|x| x + noise * randn(&mut s)).collect(); |
| 148 | + let o2: Vec<f64> = o_fp.iter().map(|x| x + noise * randn(&mut s)).collect(); |
| 149 | + m_angle2.push(angle_between(&s2, &o2)); |
| 150 | + m_res2.push(residue_to_nearest(&s2, ¢roids)); |
| 151 | + |
| 152 | + f_syn.push(syn); |
| 153 | + f_sem.push(sem); |
| 154 | + f_epi.push(epi); |
| 155 | + golden_phase.push(ph); |
| 156 | + random_phase.push(splitmix(&mut s) * std::f64::consts::TAU); |
| 157 | + } |
| 158 | + |
| 159 | + // ── MTMM Spearman matrix (factor × measure) ── |
| 160 | + let sa = spearman(&f_syn, &m_angle); |
| 161 | + let sr = spearman(&f_syn, &m_res); |
| 162 | + let ea = spearman(&f_sem, &m_angle); |
| 163 | + let er = spearman(&f_sem, &m_res); |
| 164 | + let pa = spearman(&f_epi, &m_angle); |
| 165 | + let pr = spearman(&f_epi, &m_res); |
| 166 | + |
| 167 | + println!("MTMM Spearman ρ (factor ↓ × measure →): [convergent=diagonal, discriminant=off-diagonal]"); |
| 168 | + println!(" angle residue"); |
| 169 | + println!(" syn (relation) {sa:+.3} {sr:+.3}"); |
| 170 | + println!(" sem (location) {ea:+.3} {er:+.3}"); |
| 171 | + println!(" epi (episode) {pa:+.3} {pr:+.3}"); |
| 172 | + |
| 173 | + // Phase independence + reliability + separation. |
| 174 | + let ph_syn = spearman(&f_syn, &m_phase); |
| 175 | + let ph_sem = spearman(&f_sem, &m_phase); |
| 176 | + let icc_angle = icc_a1(&[&m_angle, &m_angle2]); |
| 177 | + let icc_res = icc_a1(&[&m_res, &m_res2]); |
| 178 | + let prefix = 64usize; |
| 179 | + let g_gap = min_circular_gap(&golden_phase[..prefix]); |
| 180 | + let r_gap = min_circular_gap(&random_phase[..prefix]); |
| 181 | + |
| 182 | + println!("\nphase independence: ρ(syn,phase)={ph_syn:+.3} ρ(sem,phase)={ph_sem:+.3} (≈0 expected)"); |
| 183 | + println!("reliability (ICC test-retest @ noise {noise}): angle={icc_angle:.3} residue={icc_res:.3} phase=1.000 (deterministic)"); |
| 184 | + println!( |
| 185 | + "phase separation (min circular gap, first {prefix}): golden={g_gap:.4} random={r_gap:.4} ({:.1}× better)", |
| 186 | + g_gap / r_gap.max(1e-9) |
| 187 | + ); |
| 188 | + |
| 189 | + // ── Verdict ── |
| 190 | + let convergent = sa >= 0.9 && er >= 0.9; |
| 191 | + let discriminant = ea.abs() <= 0.2 && sr.abs() <= 0.2; |
| 192 | + let phase_indep = ph_syn.abs() <= 0.2 && ph_sem.abs() <= 0.2; |
| 193 | + let reliable = icc_angle >= 0.7 && icc_res >= 0.7; |
| 194 | + let separates = g_gap > 1.5 * r_gap; |
| 195 | + let mark = |b: bool| if b { "PASS" } else { "FAIL" }; |
| 196 | + println!("\nVERDICT:"); |
| 197 | + println!(" convergent (each measure tracks its factor) ......... {}", mark(convergent)); |
| 198 | + println!(" discriminant (axes do not leak into each other) ..... {}", mark(discriminant)); |
| 199 | + println!(" phase independent of content .........................{}", mark(phase_indep)); |
| 200 | + println!(" reliable under noise (ICC ≥ 0.7) .................... {}", mark(reliable)); |
| 201 | + println!(" golden phase separates episodes ..................... {}", mark(separates)); |
| 202 | + let all = convergent && discriminant && phase_indep && reliable && separates; |
| 203 | + println!( |
| 204 | + "\n ⇒ instrument {}", |
| 205 | + if all { |
| 206 | + "VALIDATED — syntax=angle ⊥ semantics=residue ⊥ basin=phase is a real 3-axis basis" |
| 207 | + } else { |
| 208 | + "NEEDS CORRECTION — at least one axis conflates; see failed cells" |
| 209 | + } |
| 210 | + ); |
| 211 | +} |
0 commit comments