use crate::manifold::{
AssignmentMode, PeriodicHarmonicEvaluator, SaeAssignment, SaeAtomBasisKind, SaeBasisEvaluator,
SaeManifoldAtom, SaeManifoldRho, SaeManifoldTerm,
};
use gam_terms::latent::LatentManifold;
use ndarray::{Array1, Array2};
use std::sync::{Arc, Mutex};
static K3_SERIAL: Mutex<()> = Mutex::new(());
fn k3_guard() -> std::sync::MutexGuard<'static, ()> {
K3_SERIAL.lock().unwrap_or_else(|e| e.into_inner())
}
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() * (std::f64::consts::TAU * u2).cos()
}
fn fit_circle_subset(
x: &Array2<f64>,
theta: &[Vec<f64>],
circles: &[usize],
) -> (SaeManifoldTerm, SaeManifoldRho) {
let n = x.nrows();
let p = x.ncols();
let evaluator = Arc::new(
PeriodicHarmonicEvaluator::new(3)
.expect("the fixture's harmonic order is a valid periodic basis order"),
);
let mut atoms = Vec::new();
let mut coord_blocks = Vec::new();
let mut manifolds = Vec::new();
for &c in circles {
let coords =
Array2::<f64>::from_shape_fn((n, 1), |(r, _)| theta[r][c] / std::f64::consts::TAU);
let (phi, jet) = evaluator
.evaluate(coords.view())
.expect("the fixture's coordinate block is a valid input for this evaluator");
let mut decoder = Array2::<f64>::zeros((3, p));
decoder[[1, 2 * c]] = 1.0;
decoder[[2, 2 * c + 1]] = 1.0;
let atom = SaeManifoldAtom::new_with_provided_function_gram(
format!("circle{c}"),
SaeAtomBasisKind::Periodic,
1,
phi,
jet,
decoder,
Array2::<f64>::eye(3),
)
.expect("the fixture's basis, decoder and Gram blocks agree in dimension")
.with_basis_second_jet(evaluator.clone());
atoms.push(atom);
coord_blocks.push(coords);
manifolds.push(LatentManifold::Circle { period: 1.0 });
}
let logits = Array2::<f64>::from_elem((n, circles.len()), 3.0);
let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
logits,
coord_blocks,
manifolds,
AssignmentMode::ordered_beta_bernoulli(0.7, 1.0, false),
)
.expect("the fixture's logits, coordinate blocks and manifolds agree in length");
let mut term = SaeManifoldTerm::new(atoms, assignment)
.expect("the fixture's atoms and assignment describe the same latent blocks");
term.set_guards_enabled(false);
let mut rho = SaeManifoldRho::new(0.0, 0.0, vec![Array1::<f64>::zeros(1); circles.len()]);
term.run_joint_fit_arrow_schur(x.view(), &mut rho, None, 60, 1.0, 1e-6, 1e-6)
.expect("subset fit");
(term, rho)
}
#[test]
fn rank_charge_k3_accepts_clean_atoms() {
let serial = k3_guard();
let n = 96usize;
let p = 18usize;
let ncirc = 3usize;
let mut s = 0x2101_DEC_0000_0011u64;
let theta: Vec<Vec<f64>> = (0..n)
.map(|_| {
(0..ncirc)
.map(|_| std::f64::consts::TAU * lcg(&mut s))
.collect()
})
.collect();
let mut x = Array2::<f64>::zeros((n, p));
for i in 0..n {
for c in 0..ncirc {
x[[i, 2 * c]] += theta[i][c].cos();
x[[i, 2 * c + 1]] += theta[i][c].sin();
}
for j in 0..p {
x[[i, j]] += 0.05 * lcg_normal(&mut s);
}
}
let margins = || -> Vec<f64> {
let (mut t3, r3) = fit_circle_subset(&x, &theta, &[0, 1, 2]);
let (v3, _, _) = t3
.penalized_quasi_laplace_criterion_with_cache(x.view(), &r3, None, 0, 1.0, 1e-6, 1e-6)
.unwrap();
(0..ncirc)
.map(|drop| {
let keep: Vec<usize> = (0..ncirc).filter(|&c| c != drop).collect();
let (mut t2, r2) = fit_circle_subset(&x, &theta, &keep);
let (v2, _, _) = t2
.penalized_quasi_laplace_criterion_with_cache(
x.view(),
&r2,
None,
0,
1.0,
1e-6,
1e-6,
)
.unwrap();
v3 - v2 })
.collect()
};
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(1)
.build()
.expect("1-thread rayon pool for deterministic K=3 fits");
let margins = pool.install(margins);
eprintln!("[rank-charge K=3 decisions] leave-one-out margins={margins:?}");
for (k, margin) in margins.iter().enumerate() {
assert!(
*margin < 0.0,
"circle {k}: rank-charge must ACCEPT the real atom (margin<0); got {:.3}",
margin
);
}
drop(serial); }
#[test]
fn rank_charge_prices_zero_dof_without_re_adjudicating_disappearance() {
let zero_charge =
super::construction::rank_adjusted_quasi_laplace_complexity(1.0, 0.5, &[0.0], &[10.0])
.expect("the upstream same-state signal proof owns decoder disappearance");
assert_eq!(zero_charge, 0.25);
let error = super::construction::rank_adjusted_quasi_laplace_complexity(
1.0,
0.5,
&[0.0, f64::NAN],
&[10.0, 10.0],
)
.unwrap_err();
assert!(
matches!(error, super::SaeCriterionError::Numerical(_)),
"a simultaneous invalid DOF must remain a numerical error, not {error}"
);
}