use std::f64::consts::TAU;
use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
pub const RD_CROSSOVER_FACTOR: f64 = 1.8137993642342178;
const CURL_Z: f64 = 2.0;
#[derive(Debug, Clone)]
pub struct CurlVerdict {
pub kappa: f64,
pub kappa_se: f64,
pub z_below_gaussian: f64,
pub resultant1: f64,
pub resultant2: f64,
pub radius: f64,
pub gain_nats_per_row: f64,
pub net_evidence_nats: f64,
pub recommend_curl: bool,
}
struct RadiusLaw {
n: usize,
m2: f64,
kappa: f64,
kappa_se: f64,
}
fn radius_law(alpha: ArrayView1<f64>, beta: ArrayView1<f64>) -> Result<RadiusLaw, String> {
let n = alpha.len();
if beta.len() != n {
return Err(format!("curl: α len {n} != β len {}", beta.len()));
}
if n < 2 {
return Err("curl: need at least 2 rows for the radius law".to_string());
}
let inv = 1.0 / n as f64;
let mut m2 = 0.0_f64; let mut m4 = 0.0_f64; let mut m8 = 0.0_f64; for i in 0..n {
let r2 = alpha[i] * alpha[i] + beta[i] * beta[i];
let r4 = r2 * r2;
m2 += r2;
m4 += r4;
m8 += r4 * r4;
}
m2 *= inv;
m4 *= inv;
m8 *= inv;
if !(m2 > 0.0) {
return Err("curl: zero in-plane energy (m₂ = 0)".to_string());
}
let kappa = m4 / (m2 * m2);
let mut m6 = 0.0_f64;
for i in 0..n {
let r2 = alpha[i] * alpha[i] + beta[i] * beta[i];
m6 += r2 * r2 * r2;
}
m6 *= inv;
let v22 = (m4 - m2 * m2).max(0.0) / n as f64;
let v44 = (m8 - m4 * m4).max(0.0) / n as f64;
let c24 = (m6 - m2 * m4) / n as f64;
let g2 = -2.0 * m4 / (m2 * m2 * m2);
let g4 = 1.0 / (m2 * m2);
let var_kappa = g2 * g2 * v22 + g4 * g4 * v44 + 2.0 * g2 * g4 * c24;
let kappa_se = var_kappa.max(0.0).sqrt();
Ok(RadiusLaw {
n,
m2,
kappa,
kappa_se,
})
}
fn circular_resultants(alpha: ArrayView1<f64>, beta: ArrayView1<f64>) -> (f64, f64) {
let n = alpha.len();
let inv = 1.0 / n as f64;
let (mut c1, mut s1, mut c2, mut s2) = (0.0, 0.0, 0.0, 0.0);
for i in 0..n {
let th = beta[i].atan2(alpha[i]);
c1 += th.cos();
s1 += th.sin();
c2 += (2.0 * th).cos();
s2 += (2.0 * th).sin();
}
let r1 = ((c1 * inv).powi(2) + (s1 * inv).powi(2)).sqrt();
let r2 = ((c2 * inv).powi(2) + (s2 * inv).powi(2)).sqrt();
(r1, r2)
}
pub fn curl_verdict(
alpha: ArrayView1<f64>,
beta: ArrayView1<f64>,
sigma: f64,
n_eff: f64,
delta_charge: f64,
) -> Result<CurlVerdict, String> {
if !(sigma > 0.0 && sigma.is_finite()) {
return Err(format!("curl: sigma must be finite and > 0, got {sigma}"));
}
let law = radius_law(alpha, beta)?;
let (resultant1, resultant2) = circular_resultants(alpha, beta);
let radius = (law.m2 - 2.0 * sigma * sigma).max(0.0).sqrt();
let gain_nats_per_row = {
use std::f64::consts::PI;
0.5 * (3.0 * radius * radius / (PI * PI * sigma * sigma)).ln()
};
let net_evidence_nats = n_eff * gain_nats_per_row - delta_charge;
let z_below_gaussian = if law.kappa_se > 0.0 {
(2.0 - law.kappa) / law.kappa_se
} else if law.kappa < 2.0 {
f64::INFINITY
} else {
0.0
};
let res_se = 1.0 / (law.n as f64).sqrt();
let coverage_ok = resultant1 < CURL_Z * res_se + 0.15; let not_diameter = resultant2 < 0.5;
let rd_pays = radius > sigma * RD_CROSSOVER_FACTOR && net_evidence_nats > 0.0;
let recommend_curl = z_below_gaussian > CURL_Z && rd_pays && coverage_ok && not_diameter;
Ok(CurlVerdict {
kappa: law.kappa,
kappa_se: law.kappa_se,
z_below_gaussian,
resultant1,
resultant2,
radius,
gain_nats_per_row,
net_evidence_nats,
recommend_curl,
})
}
#[derive(Debug, Clone)]
pub struct CircleSeed {
pub theta_turns: Array1<f64>,
pub radius: f64,
pub decoder: Array2<f64>,
pub center: Array1<f64>,
}
impl CircleSeed {
pub fn reconstruct(&self, t: f64) -> Array1<f64> {
let p = self.decoder.ncols();
let harmonics = (self.decoder.nrows() - 1) / 2;
let mut out = self.center.clone();
let phase = TAU * t;
for h in 1..=harmonics {
let sin_row = 2 * h - 1;
let cos_row = 2 * h;
let (sh, ch) = ((h as f64 * phase).sin(), (h as f64 * phase).cos());
for j in 0..p {
out[j] += sh * self.decoder[[sin_row, j]] + ch * self.decoder[[cos_row, j]];
}
}
out
}
}
pub fn curl_seed(
u: ArrayView1<f64>,
v: ArrayView1<f64>,
alpha: ArrayView1<f64>,
beta: ArrayView1<f64>,
harmonics: usize,
center: ArrayView1<f64>,
) -> Result<CircleSeed, String> {
let p = u.len();
if v.len() != p || center.len() != p {
return Err(format!(
"curl_seed: u/v/center length mismatch (p from u = {p}, v = {}, center = {})",
v.len(),
center.len()
));
}
if harmonics == 0 {
return Err("curl_seed: need at least 1 harmonic".to_string());
}
let n = alpha.len();
if beta.len() != n {
return Err("curl_seed: α/β length mismatch".to_string());
}
let mut theta_turns = Array1::<f64>::zeros(n);
let mut m2 = 0.0_f64;
for i in 0..n {
theta_turns[i] = beta[i].atan2(alpha[i]) / TAU;
if theta_turns[i] < 0.0 {
theta_turns[i] += 1.0;
}
m2 += alpha[i] * alpha[i] + beta[i] * beta[i];
}
let radius = if n > 0 { (m2 / n as f64).sqrt() } else { 0.0 };
let mut decoder = Array2::<f64>::zeros((1 + 2 * harmonics, p));
for j in 0..p {
decoder[[0, j]] = center[j];
decoder[[1, j]] = radius * v[j]; decoder[[2, j]] = radius * u[j]; }
Ok(CircleSeed {
theta_turns,
radius,
decoder,
center: center.to_owned(),
})
}
#[derive(Debug, Clone)]
pub struct FlattenVerdict {
pub kappa: f64,
pub resultant2: f64,
pub residual_rank: usize,
pub recommend_flatten: bool,
}
pub fn flatten_verdict(
radii: ArrayView1<f64>,
angles: ArrayView1<f64>,
) -> Result<FlattenVerdict, String> {
let n = radii.len();
if angles.len() != n {
return Err("flatten_verdict: radii/angles length mismatch".to_string());
}
if n < 2 {
return Err("flatten_verdict: need at least 2 rows".to_string());
}
let alpha: Array1<f64> = (0..n).map(|i| radii[i] * angles[i].cos()).collect();
let beta: Array1<f64> = (0..n).map(|i| radii[i] * angles[i].sin()).collect();
let law = radius_law(alpha.view(), beta.view())?;
let (_r1, resultant2) = circular_resultants(alpha.view(), beta.view());
let diameter = resultant2 > 0.7;
let gaussian_fill = law.kappa > 1.5;
let (recommend_flatten, residual_rank) = if diameter {
(true, 1)
} else if gaussian_fill {
(true, 2)
} else {
(false, 2)
};
Ok(FlattenVerdict {
kappa: law.kappa,
resultant2,
residual_rank,
recommend_flatten,
})
}
pub fn orthonormal_pair_coords(
x: ArrayView2<f64>,
u: ArrayView1<f64>,
v: ArrayView1<f64>,
mean: ArrayView1<f64>,
) -> Result<(Array1<f64>, Array1<f64>, Array1<f64>, Array1<f64>), String> {
let (n, p) = x.dim();
if u.len() != p || v.len() != p || mean.len() != p {
return Err("orthonormal_pair_coords: frame/mean dim mismatch with x".to_string());
}
let un = u.dot(&u).sqrt();
if !(un > 0.0) {
return Err("orthonormal_pair_coords: u is zero".to_string());
}
let e1: Array1<f64> = u.mapv(|z| z / un);
let vproj = v.dot(&e1);
let mut e2: Array1<f64> = (0..p).map(|j| v[j] - vproj * e1[j]).collect();
let e2n = e2.dot(&e2).sqrt();
if !(e2n > 0.0) {
return Err("orthonormal_pair_coords: u,v are collinear".to_string());
}
e2.mapv_inplace(|z| z / e2n);
let mut alpha = Array1::<f64>::zeros(n);
let mut beta = Array1::<f64>::zeros(n);
for i in 0..n {
let (mut a, mut b) = (0.0, 0.0);
for j in 0..p {
let xi = x[[i, j]] - mean[j];
a += xi * e1[j];
b += xi * e2[j];
}
alpha[i] = a;
beta[i] = b;
}
Ok((alpha, beta, e1, e2))
}
#[derive(Debug, Clone)]
pub struct SignedDirection {
pub dir: Array1<f64>,
pub members: Vec<usize>,
pub active: Vec<bool>,
}
fn cosine(a: ArrayView1<f64>, b: ArrayView1<f64>) -> f64 {
let na = a.dot(&a).sqrt();
let nb = b.dot(&b).sqrt();
if na <= 0.0 || nb <= 0.0 {
return 0.0;
}
a.dot(&b) / (na * nb)
}
fn mask_overlap(a: &[bool], b: &[bool]) -> f64 {
let mut inter = 0usize;
let mut union = 0usize;
for (x, y) in a.iter().zip(b.iter()) {
if *x || *y {
union += 1;
}
if *x && *y {
inter += 1;
}
}
if union == 0 {
0.0
} else {
inter as f64 / union as f64
}
}
pub fn coalesce_antipodal(
dirs: &[ArrayView1<f64>],
active: &[Vec<bool>],
atom_ids: &[usize],
cos_threshold: f64,
max_overlap: f64,
) -> Vec<SignedDirection> {
let k = dirs.len();
assert_eq!(active.len(), k, "coalesce: dirs/active length mismatch");
assert_eq!(atom_ids.len(), k, "coalesce: dirs/atom_ids length mismatch");
let mut merges: Vec<(f64, usize, usize)> = Vec::new();
for i in 0..k {
for j in (i + 1)..k {
let c = cosine(dirs[i], dirs[j]);
if c <= cos_threshold && mask_overlap(&active[i], &active[j]) <= max_overlap {
merges.push((c, i, j));
}
}
}
merges.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
let mut used = vec![false; k];
let mut out: Vec<SignedDirection> = Vec::new();
for (_c, i, j) in merges {
if used[i] || used[j] {
continue;
}
used[i] = true;
used[j] = true;
let ni = dirs[i].dot(&dirs[i]).sqrt().max(1e-300);
let nj = dirs[j].dot(&dirs[j]).sqrt().max(1e-300);
let mut e: Array1<f64> = dirs[i].mapv(|x| x / ni);
for (idx, val) in dirs[j].iter().enumerate() {
e[idx] -= val / nj;
}
let en = e.dot(&e).sqrt();
if en <= 0.0 {
e = dirs[i].mapv(|x| x / ni);
} else {
e.mapv_inplace(|x| x / en);
}
let union: Vec<bool> = active[i]
.iter()
.zip(active[j].iter())
.map(|(a, b)| *a || *b)
.collect();
out.push(SignedDirection {
dir: e,
members: vec![atom_ids[i], atom_ids[j]],
active: union,
});
}
for i in 0..k {
if used[i] {
continue;
}
let ni = dirs[i].dot(&dirs[i]).sqrt().max(1e-300);
out.push(SignedDirection {
dir: dirs[i].mapv(|x| x / ni),
members: vec![atom_ids[i]],
active: active[i].clone(),
});
}
out
}
pub fn cooccurrence_pairs(
active: &[Vec<bool>],
rows: &[usize],
min_cooccur: usize,
) -> Vec<(usize, usize, usize)> {
let k = active.len();
let mut out: Vec<(usize, usize, usize)> = Vec::new();
for i in 0..k {
for j in (i + 1)..k {
let mut count = 0usize;
for &r in rows {
if active[i].get(r).copied().unwrap_or(false)
&& active[j].get(r).copied().unwrap_or(false)
{
count += 1;
}
}
if count >= min_cooccur {
out.push((i, j, count));
}
}
}
out.sort_by(|a, b| b.2.cmp(&a.2).then(a.0.cmp(&b.0)).then(a.1.cmp(&b.1)));
out
}
#[derive(Debug, Clone, Default)]
pub struct CurlCooldownLedger {
entries: std::collections::HashMap<u64, usize>,
}
pub fn atom_set_hash(atoms: &[usize]) -> u64 {
let mut sorted: Vec<usize> = atoms.to_vec();
sorted.sort_unstable();
sorted.dedup();
let mut h = 0xcbf29ce484222325u64;
for a in sorted {
for b in (a as u64).to_le_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
}
h
}
impl CurlCooldownLedger {
pub fn new() -> Self {
Self::default()
}
pub fn blocked(&self, atoms: &[usize]) -> bool {
self.entries
.get(&atom_set_hash(atoms))
.is_some_and(|&r| r > 0)
}
pub fn record(&mut self, atoms: &[usize], cooldown: usize) {
if cooldown == 0 {
return;
}
self.entries.insert(atom_set_hash(atoms), cooldown);
}
pub fn tick(&mut self) {
self.entries.retain(|_, r| {
*r = r.saturating_sub(1);
*r > 0
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array1, Array2};
use std::f64::consts::PI;
fn lcg(s: &mut u64) -> f64 {
*s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((*s >> 11) as f64) / ((1u64 << 53) as f64)
}
fn lcg_normal(s: &mut u64) -> f64 {
let u1 = lcg(s).max(1e-12);
let u2 = lcg(s);
(-2.0 * u1.ln()).sqrt() * (TAU * u2).cos()
}
#[test]
fn ring_is_recommended_kappa_near_one() {
let mut s = 0x51A9_u64;
let n = 4000usize;
let radius = 3.0_f64;
let sigma = 0.05_f64;
let mut alpha = Array1::<f64>::zeros(n);
let mut beta = Array1::<f64>::zeros(n);
for i in 0..n {
let th = TAU * lcg(&mut s);
alpha[i] = radius * th.cos() + sigma * lcg_normal(&mut s);
beta[i] = radius * th.sin() + sigma * lcg_normal(&mut s);
}
let v = curl_verdict(alpha.view(), beta.view(), sigma, n as f64, 50.0).unwrap();
assert!((v.kappa - 1.0).abs() < 0.1, "ring κ≈1, got {}", v.kappa);
assert!(v.recommend_curl, "clean ring must be recommended");
}
#[test]
fn gaussian_fill_is_rejected_kappa_near_two() {
let mut s = 0x6A2_u64;
let n = 4000usize;
let sigma = 1.0_f64;
let mut alpha = Array1::<f64>::zeros(n);
let mut beta = Array1::<f64>::zeros(n);
for i in 0..n {
alpha[i] = 2.0 * lcg_normal(&mut s);
beta[i] = 2.0 * lcg_normal(&mut s);
}
let v = curl_verdict(alpha.view(), beta.view(), sigma, n as f64, 50.0).unwrap();
assert!(
(v.kappa - 2.0).abs() < 0.15,
"Gaussian fill κ≈2, got {}",
v.kappa
);
assert!(!v.recommend_curl, "Gaussian fill must be rejected");
}
#[test]
fn seed_reconstructs_nonorthogonal_frame_ring_to_1e10() {
let mut s = 0x1234_u64;
let n = 200usize;
let p = 5usize;
let u = Array1::from_vec(vec![1.0, 0.5, 0.0, -0.2, 0.0]);
let v = Array1::from_vec(vec![0.3, 1.0, 0.4, 0.0, 0.1]);
let center = Array1::from_vec(vec![0.7, -0.3, 0.2, 1.1, 0.0]);
let radius = 2.0_f64;
let mut alpha = Array1::<f64>::zeros(n);
let mut beta = Array1::<f64>::zeros(n);
let mut pts = Array2::<f64>::zeros((n, p));
for i in 0..n {
let th = TAU * lcg(&mut s);
let (a, b) = (radius * th.cos(), radius * th.sin());
alpha[i] = a;
beta[i] = b;
for j in 0..p {
pts[[i, j]] = center[j] + a * u[j] + b * v[j];
}
}
let seed = curl_seed(
u.view(),
v.view(),
alpha.view(),
beta.view(),
3,
center.view(),
)
.unwrap();
assert!(
(seed.radius - radius).abs() < 1e-9,
"radius {}",
seed.radius
);
let mut max_err = 0.0_f64;
for i in 0..n {
let rec = seed.reconstruct(seed.theta_turns[i]);
for j in 0..p {
max_err = max_err.max((rec[j] - pts[[i, j]]).abs());
}
}
assert!(max_err < 1e-10, "reconstruction max err {max_err:.3e}");
}
#[test]
fn flatten_demotes_gaussian_fill_to_rank2() {
let mut s = 0x9F1_u64;
let n = 3000usize;
let mut radii = Array1::<f64>::zeros(n);
let mut angles = Array1::<f64>::zeros(n);
for i in 0..n {
let a = 2.0 * lcg_normal(&mut s);
let b = 2.0 * lcg_normal(&mut s);
radii[i] = (a * a + b * b).sqrt();
angles[i] = b.atan2(a);
}
let v = flatten_verdict(radii.view(), angles.view()).unwrap();
assert!(v.recommend_flatten);
assert_eq!(v.residual_rank, 2);
}
#[test]
fn flatten_demotes_diameter_to_rank1() {
let mut s = 0x33A_u64;
let n = 3000usize;
let mut radii = Array1::<f64>::zeros(n);
let mut angles = Array1::<f64>::zeros(n);
for i in 0..n {
let g = 3.0 * lcg_normal(&mut s);
radii[i] = g.abs();
angles[i] = if g >= 0.0 { 0.0 } else { PI };
}
let v = flatten_verdict(radii.view(), angles.view()).unwrap();
assert!(v.recommend_flatten);
assert_eq!(v.residual_rank, 1);
}
#[test]
fn healthy_ring_not_flattened() {
let mut s = 0x77C_u64;
let n = 3000usize;
let mut radii = Array1::<f64>::zeros(n);
let mut angles = Array1::<f64>::zeros(n);
for i in 0..n {
radii[i] = 2.0 + 0.02 * lcg_normal(&mut s);
angles[i] = TAU * lcg(&mut s);
}
let v = flatten_verdict(radii.view(), angles.view()).unwrap();
assert!(
!v.recommend_flatten,
"healthy ring must not flatten (κ={:.3} R2={:.3})",
v.kappa, v.resultant2
);
}
#[test]
fn coalesce_merges_four_rectified_halves_into_two_signed_axes() {
let p = 6usize;
let n = 400usize;
let mut up = Array1::<f64>::zeros(p);
up[0] = 1.0;
let un = up.mapv(|x| -x); let mut vp = Array1::<f64>::zeros(p);
vp[1] = 1.0;
let vn = vp.mapv(|x| -x); let mask =
|lo: usize, hi: usize| -> Vec<bool> { (0..n).map(|r| r >= lo && r < hi).collect() };
let dirs = [up.view(), un.view(), vp.view(), vn.view()];
let active = vec![mask(0, 100), mask(200, 300), mask(100, 200), mask(300, 400)];
let ids = [10usize, 11, 12, 13];
let signed = coalesce_antipodal(&dirs, &active, &ids, -0.9, 0.1);
assert_eq!(
signed.len(),
2,
"four halves must coalesce into two signed axes"
);
for sd in &signed {
assert_eq!(sd.members.len(), 2, "each signed axis merges a ± pair");
let active_count = sd.active.iter().filter(|b| **b).count();
assert_eq!(active_count, 200);
}
}
#[test]
fn coalesce_leaves_a_lone_signed_atom_unmerged() {
let p = 3usize;
let n = 10usize;
let mut a = Array1::<f64>::zeros(p);
a[0] = 1.0;
let dirs = [a.view()];
let active = vec![vec![true; n]];
let ids = [7usize];
let signed = coalesce_antipodal(&dirs, &active, &ids, -0.9, 0.1);
assert_eq!(signed.len(), 1);
assert_eq!(signed[0].members, vec![7]);
}
#[test]
fn coalesce_refuses_overlapping_antipodal_gates() {
let p = 3usize;
let n = 10usize;
let mut a = Array1::<f64>::zeros(p);
a[0] = 1.0;
let b = a.mapv(|x| -x);
let dirs = [a.view(), b.view()];
let active = vec![vec![true; n], vec![true; n]];
let ids = [1usize, 2];
let signed = coalesce_antipodal(&dirs, &active, &ids, -0.9, 0.1);
assert_eq!(signed.len(), 2, "overlapping gates must not coalesce");
}
#[test]
fn cooccurrence_counts_and_ranks() {
let active = vec![
(0..10).map(|r| r < 6).collect::<Vec<_>>(),
(0..10).map(|r| r < 6).collect::<Vec<_>>(),
(0..10).map(|r| r >= 6).collect::<Vec<_>>(),
];
let rows: Vec<usize> = (0..10).collect();
let pairs = cooccurrence_pairs(&active, &rows, 3);
assert_eq!(pairs.first().map(|p| (p.0, p.1, p.2)), Some((0, 1, 6)));
assert_eq!(pairs.len(), 1);
}
#[test]
fn cooldown_blocks_then_expires() {
let mut led = CurlCooldownLedger::new();
assert!(!led.blocked(&[3, 1, 2]));
led.record(&[1, 2, 3], 2);
assert!(led.blocked(&[3, 2, 1]));
led.tick();
assert!(led.blocked(&[1, 2, 3]));
led.tick();
assert!(
!led.blocked(&[1, 2, 3]),
"cooldown must expire after c ticks"
);
}
}