use gam_linalg::faer_ndarray::{FaerCholesky, FaerEigh, FaerSvd};
use ndarray::{Array2, ArrayView2};
use super::Side;
fn wbic_tempered_rank_fraction(mu: f64, edge: f64, n_eff: f64) -> f64 {
if mu == 0.0 {
return 0.0;
}
if edge == 0.0 {
return 1.0;
}
let log_n_eff = n_eff.max(std::f64::consts::E).ln();
let scale = mu.max(edge);
let scaled_mu = mu / scale;
scaled_mu / (scaled_mu + (edge / scale) * log_n_eff)
}
#[derive(Clone, Debug)]
pub struct ReconSpectrum {
mu: Vec<f64>,
edge: f64,
dispersion: f64,
basis_edf: f64,
n_eff: f64,
}
impl ReconSpectrum {
fn rank_classification(&self) -> super::construction::ReconstructionRankClassification {
super::construction::classify_reconstruction_rank(&self.mu, self.edge, self.dispersion)
}
pub fn reconstruction_energies(&self) -> &[f64] {
&self.mu
}
pub fn mp_reconstruction_rank_edge(&self) -> f64 {
self.edge
}
pub fn basis_edf(&self) -> f64 {
self.basis_edf
}
pub(super) fn with_audit_basis_edf(mut self, basis_edf: f64) -> Result<Self, String> {
if !basis_edf.is_finite() || basis_edf < 0.0 {
return Err(format!(
"audit basis EDF must be finite and non-negative; got {basis_edf}"
));
}
self.basis_edf = basis_edf;
Ok(self)
}
pub fn mp_reconstruction_rank(&self) -> usize {
self.rank_classification().mp_reconstruction_rank
}
pub fn production_chargeable_rank(&self) -> usize {
self.rank_classification().production_chargeable_rank
}
pub fn rank_soft(&self) -> f64 {
self.mu
.iter()
.map(|&m| wbic_tempered_rank_fraction(m, self.edge, self.n_eff))
.sum()
}
pub fn mp_reconstruction_rank_charge(&self) -> f64 {
0.5 * self.mp_reconstruction_rank() as f64 * self.basis_edf * self.n_eff.max(1.0).ln()
}
pub fn production_charge(&self) -> f64 {
0.5 * self.production_chargeable_rank() as f64 * self.basis_edf * self.n_eff.max(1.0).ln()
}
pub fn wbic_charge(&self) -> f64 {
0.5 * self.rank_soft() * self.basis_edf * self.n_eff.max(1.0).ln()
}
pub fn learning_coefficient(&self) -> f64 {
0.5 * self.rank_soft() * self.basis_edf
}
}
pub fn recon_spectrum(
gram: &Array2<f64>,
decoder: &Array2<f64>,
n_eff: f64,
p_out: f64,
r_floor: f64,
lam_smooth: f64,
smooth_penalty: Option<&Array2<f64>>,
) -> Result<ReconSpectrum, String> {
let m = gram.nrows();
super::construction::validate_rank_charge_problem(
gram,
decoder,
n_eff,
p_out,
r_floor,
lam_smooth,
smooth_penalty,
)?;
if m == 0 || n_eff == 0.0 {
return Ok(ReconSpectrum {
mu: Vec::new(),
edge: 0.0,
dispersion: r_floor,
basis_edf: 0.0,
n_eff,
});
}
let (evals, u) = gram
.eigh(Side::Lower)
.map_err(|e| format!("recon_spectrum: eigh(G): {e}"))?;
let evals = super::construction::certified_psd_spectrum(evals.view(), "rank-charge Gram")?;
let mut scaled = u.t().dot(decoder);
let cols = scaled.ncols();
for i in 0..m {
let s = evals[i].sqrt();
for j in 0..cols {
scaled[[i, j]] *= s;
}
}
let sv = match scaled.svd(false, false) {
Ok((_, sv, _)) => sv,
Err(e) => return Err(format!("recon_spectrum: recon svd: {e}")),
};
let edge = crate::null_battery::mp_reconstruction_rank_edge(n_eff, p_out, r_floor)
.map_err(|error| format!("recon_spectrum: {error}"))?;
let mu = sv
.iter()
.map(|&singular_value| {
super::construction::normalized_reconstruction_energy(singular_value, n_eff)
})
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("recon_spectrum: {error}"))?;
let mut mmat = gram.clone();
if let Some(pen) = smooth_penalty {
for i in 0..m {
for j in 0..m {
mmat[[i, j]] += lam_smooth * pen[[i, j]];
}
}
}
let factor = mmat.cholesky(Side::Lower).map_err(|error| {
format!("recon_spectrum: G + lambda*S is not positive definite: {error}")
})?;
let x = factor.solve_mat(gram);
let raw_basis_edf = (0..m).map(|i| x[[i, i]]).sum::<f64>();
let basis_edf = super::construction::certified_basis_edf(raw_basis_edf, m, "recon_spectrum")?;
Ok(ReconSpectrum {
mu,
edge,
dispersion: r_floor,
basis_edf,
n_eff,
})
}
#[derive(Clone, Debug)]
pub struct AuditRow {
pub name: String,
pub n: usize,
pub mp_reconstruction_rank: usize,
pub production_chargeable_rank: usize,
pub rank_soft: f64,
pub basis_edf: f64,
pub mp_reconstruction_rank_charge: f64,
pub production_charge: f64,
pub wbic_charge: f64,
pub production_minus_wbic: f64,
pub production_delta_fraction: f64,
}
impl AuditRow {
pub fn from_spectrum(name: impl Into<String>, spec: &ReconSpectrum, n: usize) -> Self {
let mp_reconstruction_rank_charge = spec.mp_reconstruction_rank_charge();
let production_charge = spec.production_charge();
let wbic_charge = spec.wbic_charge();
let production_minus_wbic = production_charge - wbic_charge;
let production_delta_fraction = if production_charge.abs() > 0.0 {
production_minus_wbic / production_charge
} else {
f64::NAN
};
Self {
name: name.into(),
n,
mp_reconstruction_rank: spec.mp_reconstruction_rank(),
production_chargeable_rank: spec.production_chargeable_rank(),
rank_soft: spec.rank_soft(),
basis_edf: spec.basis_edf(),
mp_reconstruction_rank_charge,
production_charge,
wbic_charge,
production_minus_wbic,
production_delta_fraction,
}
}
}
pub fn render_audit_table(rows: &[AuditRow]) -> String {
let mut out = String::new();
out.push_str(
"population n r_mp r_prod r_soft basis_edf C_mp C_prod C_wbic prod-wbic frac\n",
);
out.push_str(
"----------------------- --- ------ ------ ------- -------- ------- ------- ------- ---------- -------\n",
);
for r in rows {
out.push_str(&format!(
"{:<23} {:>3} {:>6} {:>6} {:>7.3} {:>8.3} {:>7.3} {:>7.3} {:>7.3} {:>10.3} {:>7.3}\n",
r.name,
r.n,
r.mp_reconstruction_rank,
r.production_chargeable_rank,
r.rank_soft,
r.basis_edf,
r.mp_reconstruction_rank_charge,
r.production_charge,
r.wbic_charge,
r.production_minus_wbic,
r.production_delta_fraction,
));
}
out
}
#[cfg(test)]
mod learning_coeff_helpers_tests {
use super::*;
pub(super) fn direction_learning_coefficient(mu: f64, edge: f64, n_eff: f64) -> f64 {
0.5 * wbic_tempered_rank_fraction(mu, edge, n_eff)
}
pub(super) fn sampled_direction_learning_coefficient(
mu: f64,
edge: f64,
n_eff: f64,
r_floor: f64,
) -> f64 {
let ln_neff = n_eff.max(std::f64::consts::E).ln();
if !(ln_neff > 0.0) || !(r_floor > 0.0) || !(n_eff > 0.0) {
return 0.0;
}
let beta = 1.0 / ln_neff;
let g = n_eff * mu; let g_edge = n_eff * edge; let h = beta * g / r_floor; let tau = g_edge / r_floor; let prec_post = h + tau;
if !(prec_post > 0.0) {
return 0.0;
}
let alpha_hat2 = ((mu - edge).max(0.0)) / mu.max(f64::MIN_POSITIVE);
let var = 1.0 / prec_post;
let m_post = h * 0.0_f64.max(alpha_hat2.sqrt()) / prec_post; let alpha_hat = alpha_hat2.sqrt();
let shift2 = (m_post - alpha_hat) * (m_post - alpha_hat);
let e_delta = 0.5 * (g / r_floor) * (var + shift2);
e_delta / ln_neff
}
}
pub fn spectrum_from_fit(
data: ArrayView2<'_, f64>,
w: &[f64],
phi: &Array2<f64>,
r_floor: f64,
lam_smooth: f64,
smooth_penalty: Option<&Array2<f64>>,
) -> Result<ReconSpectrum, String> {
let (n, p) = data.dim();
let m = phi.ncols();
if phi.nrows() != n || w.len() != n {
return Err("spectrum_from_fit: shape mismatch".into());
}
if data.iter().any(|value| !value.is_finite()) {
return Err("spectrum_from_fit: data must be finite".into());
}
if phi.iter().any(|value| !value.is_finite()) {
return Err("spectrum_from_fit: basis must be finite".into());
}
if w.iter().any(|weight| !weight.is_finite() || *weight < 0.0) {
return Err("spectrum_from_fit: weights must be finite and non-negative".into());
}
let mut gram = Array2::<f64>::zeros((m, m));
let mut cross = Array2::<f64>::zeros((m, p));
let mut n_eff = 0.0_f64;
for i in 0..n {
let wi = w[i];
n_eff += wi;
for a in 0..m {
let pa = phi[[i, a]] * wi;
for b in a..m {
gram[[a, b]] += pa * phi[[i, b]];
}
for j in 0..p {
cross[[a, j]] += pa * data[[i, j]];
}
}
}
for a in 0..m {
for b in a..m {
let v = gram[[a, b]];
gram[[a, b]] = v;
gram[[b, a]] = v;
}
}
let mut reg = gram.clone();
for a in 0..m {
reg[[a, a]] += 1.0e-9;
}
let decoder = reg
.cholesky(Side::Lower)
.map_err(|e| format!("spectrum_from_fit: chol: {e}"))?
.solve_mat(&cross);
recon_spectrum(
&gram,
&decoder,
n_eff,
p as f64,
r_floor,
lam_smooth,
smooth_penalty,
)
}
#[cfg(test)]
mod tests {
use super::learning_coeff_helpers_tests::{
direction_learning_coefficient, sampled_direction_learning_coefficient,
};
use super::*;
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 harmonic_phi(turns: &[f64], h: usize) -> Array2<f64> {
let n = turns.len();
let m = 1 + 2 * h;
Array2::from_shape_fn((n, m), |(i, c)| {
if c == 0 {
1.0
} else {
let k = (c + 1) / 2;
let ang = std::f64::consts::TAU * k as f64 * turns[i];
if c % 2 == 1 { ang.cos() } else { ang.sin() }
}
})
}
fn poly_phi(t: &[f64], deg: usize) -> Array2<f64> {
let n = t.len();
Array2::from_shape_fn((n, deg + 1), |(i, c)| t[i].powi(c as i32))
}
#[test]
fn spectrum_chargeable_rank_matches_production_deff() {
let mut s = 0x0B1C_0001_u64;
let n = 800usize;
let p = 12usize;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let phi = harmonic_phi(&turns, 3);
let m = phi.ncols();
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let a = std::f64::consts::TAU * turns[i];
data[[i, 0]] += a.cos();
data[[i, 1]] += a.sin();
for j in 0..p {
data[[i, j]] += 0.05 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let r_floor = 0.05_f64 * 0.05;
let spec = spectrum_from_fit(data.view(), &w, &phi, r_floor, 0.0, None).unwrap();
let mut gram = Array2::<f64>::zeros((m, m));
let mut cross = Array2::<f64>::zeros((m, p));
for i in 0..n {
for a in 0..m {
for b in 0..m {
gram[[a, b]] += phi[[i, a]] * phi[[i, b]];
}
for j in 0..p {
cross[[a, j]] += phi[[i, a]] * data[[i, j]];
}
}
}
let mut reg = gram.clone();
for a in 0..m {
reg[[a, a]] += 1.0e-9;
}
let decoder = reg.cholesky(Side::Lower).unwrap().solve_mat(&cross);
let d_prod = super::super::construction::realised_rank_charge_dof(
&gram, &decoder, n as f64, p as f64, r_floor, 0.0, None,
)
.unwrap();
let d_audit = spec.production_chargeable_rank() as f64 * spec.basis_edf();
eprintln!(
"[wbic parity] production d_eff={d_prod:.10} \
audit rank_chargeable·basis_edf={d_audit:.10}"
);
assert!(
(d_prod - d_audit).abs() < 1e-8,
"audit chargeable d_eff must match production: prod={d_prod} audit={d_audit}"
);
}
#[test]
fn weak_signal_reconstruction_rank_zero_is_production_chargeable_one() {
let gram = Array2::<f64>::eye(1);
let decoder = Array2::<f64>::ones((1, 1));
let n_eff = 100.0;
let r_floor = 1.0;
let spec = recon_spectrum(&gram, &decoder, n_eff, 1.0, r_floor, 0.0, None).unwrap();
let d_prod = super::super::construction::realised_rank_charge_dof(
&gram, &decoder, n_eff, 1.0, r_floor, 0.0, None,
)
.unwrap();
assert_eq!(spec.mp_reconstruction_rank(), 0);
assert_eq!(spec.production_chargeable_rank(), 1);
assert_eq!(d_prod, spec.basis_edf());
assert_eq!(spec.production_charge(), 0.5 * d_prod * n_eff.ln());
assert_eq!(spec.mp_reconstruction_rank_charge(), 0.0);
}
#[test]
fn zero_edge_soft_rank_distinguishes_zero_from_positive_energy() {
let gram = Array2::<f64>::eye(2);
let zero = Array2::<f64>::zeros((2, 2));
let zero_spec = recon_spectrum(&gram, &zero, 2.0, 2.0, 0.0, 0.0, None).unwrap();
assert_eq!(zero_spec.mp_reconstruction_rank_edge(), 0.0);
assert_eq!(zero_spec.rank_soft(), 0.0);
assert_eq!(zero_spec.mp_reconstruction_rank(), 0);
assert_eq!(zero_spec.production_chargeable_rank(), 0);
assert_eq!(zero_spec.wbic_charge(), 0.0);
let mut one_direction = Array2::<f64>::zeros((2, 2));
one_direction[[0, 0]] = 1.0;
let positive_spec =
recon_spectrum(&gram, &one_direction, 2.0, 2.0, 0.0, 0.0, None).unwrap();
assert_eq!(positive_spec.mp_reconstruction_rank_edge(), 0.0);
assert_eq!(positive_spec.rank_soft(), 1.0);
assert_eq!(positive_spec.mp_reconstruction_rank(), 1);
assert_eq!(positive_spec.production_chargeable_rank(), 1);
}
#[test]
fn soft_rank_fraction_avoids_finite_input_overflow() {
let scale = 0.5 * f64::MAX;
let spec = ReconSpectrum {
mu: vec![scale],
edge: scale,
dispersion: 1.0,
basis_edf: 1.0,
n_eff: f64::MAX,
};
let expected = 1.0 / (1.0 + f64::MAX.ln());
let actual = spec.rank_soft();
assert!(actual.is_finite() && actual > 0.0);
assert!((actual - expected).abs() <= 8.0 * f64::EPSILON * expected);
}
#[test]
fn extreme_singular_value_has_finite_shared_energy_and_rank() {
let gram = Array2::<f64>::eye(1);
let decoder = Array2::<f64>::from_elem((1, 1), 1.0e200);
let n_eff = 1.0e200;
let spec = recon_spectrum(&gram, &decoder, n_eff, 1.0, 1.0, 0.0, None).unwrap();
let energy = spec.reconstruction_energies()[0];
assert!(energy.is_finite());
assert!((energy / 1.0e200 - 1.0).abs() < 1.0e-12);
assert_eq!(spec.mp_reconstruction_rank(), 1);
assert_eq!(spec.production_chargeable_rank(), 1);
let d_prod = super::super::construction::realised_rank_charge_dof(
&gram, &decoder, n_eff, 1.0, 1.0, 0.0, None,
)
.unwrap();
assert_eq!(d_prod, spec.basis_edf());
}
#[test]
fn rank_charge_value_and_audit_share_strict_numeric_contract() {
let gram = Array2::<f64>::eye(2);
let decoder = Array2::<f64>::zeros((2, 3));
let production = |gram: &Array2<f64>,
decoder: &Array2<f64>,
n_eff: f64,
p_out: f64,
r_floor: f64,
lam_smooth: f64,
penalty: Option<&Array2<f64>>| {
super::super::construction::realised_rank_charge_dof(
gram, decoder, n_eff, p_out, r_floor, lam_smooth, penalty,
)
};
for (n_eff, p_out, r_floor, lam_smooth) in [
(f64::NAN, 3.0, 1.0, 0.0),
(-1.0, 3.0, 1.0, 0.0),
(10.0, f64::INFINITY, 1.0, 0.0),
(10.0, 3.0, -1.0, 0.0),
(10.0, 3.0, 1.0, f64::NAN),
] {
assert!(production(&gram, &decoder, n_eff, p_out, r_floor, lam_smooth, None).is_err());
assert!(
recon_spectrum(&gram, &decoder, n_eff, p_out, r_floor, lam_smooth, None).is_err()
);
}
let wrong_width = Array2::<f64>::zeros((2, 2));
assert!(production(&gram, &wrong_width, 10.0, 3.0, 1.0, 0.0, None).is_err());
assert!(recon_spectrum(&gram, &wrong_width, 10.0, 3.0, 1.0, 0.0, None).is_err());
let indefinite = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, -0.25]).unwrap();
let penalty = Array2::<f64>::eye(2);
assert!(production(&indefinite, &decoder, 10.0, 3.0, 1.0, 1.0, Some(&penalty)).is_err());
assert!(
recon_spectrum(&indefinite, &decoder, 10.0, 3.0, 1.0, 1.0, Some(&penalty)).is_err()
);
let indefinite_penalty =
Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 0.0, -0.25]).unwrap();
assert!(
production(
&gram,
&decoder,
10.0,
3.0,
1.0,
1.0,
Some(&indefinite_penalty),
)
.is_err()
);
assert!(
recon_spectrum(
&gram,
&decoder,
10.0,
3.0,
1.0,
1.0,
Some(&indefinite_penalty),
)
.is_err()
);
assert_eq!(
production(&gram, &decoder, 0.0, 3.0, 1.0, 0.0, None).unwrap(),
0.0
);
assert_eq!(
recon_spectrum(&gram, &decoder, 0.0, 3.0, 1.0, 0.0, None)
.unwrap()
.mp_reconstruction_rank_edge(),
0.0
);
}
#[test]
fn spectrum_fit_rejects_invalid_weights_and_nonfinite_inputs() {
let data = Array2::<f64>::zeros((2, 1));
let phi = Array2::<f64>::eye(2);
assert!(spectrum_from_fit(data.view(), &[-1.0, 2.0], &phi, 1.0, 0.0, None).is_err());
assert!(spectrum_from_fit(data.view(), &[f64::NAN, 1.0], &phi, 1.0, 0.0, None).is_err());
let mut nonfinite_data = data.clone();
nonfinite_data[[0, 0]] = f64::INFINITY;
assert!(
spectrum_from_fit(nonfinite_data.view(), &[1.0, 1.0], &phi, 1.0, 0.0, None,).is_err()
);
let mut nonfinite_phi = phi;
nonfinite_phi[[0, 0]] = f64::NAN;
assert!(
spectrum_from_fit(data.view(), &[1.0, 1.0], &nonfinite_phi, 1.0, 0.0, None,).is_err()
);
}
#[test]
fn sigmoid_matches_tempered_posterior_variance_term() {
let n_eff = 800.0_f64;
let r_floor = 0.0025_f64;
let edge = r_floor * (1.0 + (12.0_f64 / n_eff).sqrt()).powi(2);
let ln_neff = n_eff.max(std::f64::consts::E).ln();
for &ratio in &[8.0_f64, 4.0, 2.0, 1.0, 0.5, 0.25] {
let mu = ratio * edge;
let closed = direction_learning_coefficient(mu, edge, n_eff);
let sampled = sampled_direction_learning_coefficient(mu, edge, n_eff, r_floor);
eprintln!("[wbic sigmoid] μ/e={ratio:.2} closed={closed:.4} sampled≈{sampled:.4}");
let beta = 1.0 / ln_neff;
let g = n_eff * mu;
let g_edge = n_eff * edge;
let h = beta * g / r_floor;
let tau = g_edge / r_floor;
let var_term = 0.5 * (g / r_floor) * (1.0 / (h + tau)) / ln_neff;
assert!(
(closed - var_term).abs() < 1e-9,
"closed sigmoid must equal the tempered variance term: closed={closed} var={var_term}"
);
}
}
#[test]
fn wbic_audit_disagreement_table() {
let mut rows: Vec<AuditRow> = Vec::new();
let n = 1200usize;
let p = 16usize;
{
let mut s = 0x1111_u64;
let t: Vec<f64> = (0..n).map(|_| 2.0 * lcg(&mut s) - 1.0).collect();
let phi = poly_phi(&t, 2);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
data[[i, 0]] += 2.0 * t[i];
data[[i, 1]] += 0.5 * t[i];
for j in 0..p {
data[[i, j]] += 0.05 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let spec = spectrum_from_fit(data.view(), &w, &phi, 0.0025, 0.0, None).unwrap();
rows.push(AuditRow::from_spectrum("line (regular)", &spec, n));
}
{
let mut s = 0x2222_u64;
let phi =
Array2::<f64>::from_shape_fn((n, 3), |(i, c)| if i % 3 == c { 1.0 } else { 0.0 });
let centers = [[3.0, 0.0], [0.0, 3.0], [-3.0, -3.0]];
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let c = i % 3;
data[[i, 0]] += centers[c][0];
data[[i, 1]] += centers[c][1];
for j in 0..p {
data[[i, j]] += 0.1 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let spec = spectrum_from_fit(data.view(), &w, &phi, 0.01, 0.0, None).unwrap();
rows.push(AuditRow::from_spectrum("clusters (regular)", &spec, n));
}
{
let mut s = 0x3333_u64;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let phi = harmonic_phi(&turns, 3);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let a = std::f64::consts::TAU * turns[i];
data[[i, 0]] += a.cos();
data[[i, 1]] += a.sin();
for j in 0..p {
data[[i, j]] += 0.05 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let spec = spectrum_from_fit(data.view(), &w, &phi, 0.0025, 0.0, None).unwrap();
rows.push(AuditRow::from_spectrum("circle clean (curved)", &spec, n));
}
{
let mut s = 0x4444_u64;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let phi = harmonic_phi(&turns, 3);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let a = std::f64::consts::TAU * turns[i];
data[[i, 0]] += 0.22 * a.cos();
data[[i, 1]] += 0.22 * a.sin();
for j in 0..p {
data[[i, j]] += 0.15 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let spec = spectrum_from_fit(data.view(), &w, &phi, 0.15 * 0.15, 0.0, None).unwrap();
rows.push(AuditRow::from_spectrum(
"circle near-edge (singular)",
&spec,
n,
));
}
{
let mut s = 0x5555_u64;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let radii: Vec<f64> = (0..n).map(|_| lcg(&mut s).sqrt()).collect();
let phi = harmonic_phi(&turns, 3);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let a = std::f64::consts::TAU * turns[i];
data[[i, 0]] += 0.35 * radii[i] * a.cos();
data[[i, 1]] += 0.35 * radii[i] * a.sin();
for j in 0..p {
data[[i, j]] += 0.12 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let spec = spectrum_from_fit(data.view(), &w, &phi, 0.12 * 0.12, 0.0, None).unwrap();
rows.push(AuditRow::from_spectrum("disk (curved)", &spec, n));
}
{
let mut s = 0x6666_u64;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let phi = harmonic_phi(&turns, 3);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
for j in 0..p {
data[[i, j]] += 0.1 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let spec = spectrum_from_fit(data.view(), &w, &phi, 0.01, 0.0, None).unwrap();
rows.push(AuditRow::from_spectrum("gaussian blend (noise)", &spec, n));
}
rows.push(AuditRow::from_spectrum(
"decoder vanished",
&ReconSpectrum {
mu: vec![0.0; 7],
edge: 0.01 * (1.0 + (p as f64 / n as f64).sqrt()).powi(2),
dispersion: 0.01,
basis_edf: 7.0,
n_eff: n as f64,
},
n,
));
eprintln!("\n{}", render_audit_table(&rows));
let get = |name: &str| rows.iter().find(|r| r.name == name).unwrap().clone();
let line = get("line (regular)");
let clusters = get("clusters (regular)");
let near = get("circle near-edge (singular)");
let disk = get("disk (curved)");
let blend = get("gaussian blend (noise)");
let vanished = get("decoder vanished");
assert!(
line.production_delta_fraction.abs() < 0.05,
"regular line must show ~no disagreement; frac={:.3}",
line.production_delta_fraction
);
assert!(
clusters.production_delta_fraction.abs() < 0.05,
"regular clusters must show ~no disagreement; frac={:.3}",
clusters.production_delta_fraction
);
assert!(
disk.production_minus_wbic > 0.0 && disk.production_delta_fraction > 0.15,
"curved disk must be OVER-charged (C_prod > C_wbic) by a clear fraction; \
delta={:.3} frac={:.3}",
disk.production_minus_wbic,
disk.production_delta_fraction
);
assert!(
disk.production_delta_fraction > line.production_delta_fraction + 0.1
&& disk.production_delta_fraction > clusters.production_delta_fraction + 0.1,
"singular over-charge fraction ({:.3}) must exceed the regular atoms' \
(line {:.3}, clusters {:.3}) by a clear margin",
disk.production_delta_fraction,
line.production_delta_fraction,
clusters.production_delta_fraction
);
assert!(
near.mp_reconstruction_rank == 0
&& near.production_chargeable_rank == 1
&& near.rank_soft > 0.0
&& near.mp_reconstruction_rank_charge == 0.0
&& near.production_charge > 0.0,
"weak near-edge circle must distinguish MP reconstruction rank from production \
chargeability: mp={} prod={} soft={:.3}",
near.mp_reconstruction_rank,
near.production_chargeable_rank,
near.rank_soft
);
assert!(
blend.mp_reconstruction_rank == 0
&& blend.production_chargeable_rank == 1
&& blend.rank_soft < 0.3,
"noise fit must distinguish MP rank from production rank: \
mp={} prod={} soft={:.3}",
blend.mp_reconstruction_rank,
blend.production_chargeable_rank,
blend.rank_soft
);
assert_eq!(vanished.mp_reconstruction_rank, 0);
assert_eq!(vanished.production_chargeable_rank, 0);
assert_eq!(vanished.production_charge, 0.0);
assert_eq!(vanished.wbic_charge, 0.0);
}
#[test]
fn rank_charge_is_inert_row_invariant() {
let mut s = 0x7777_u64;
let n = 400usize;
let p = 12usize;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let phi = harmonic_phi(&turns, 3);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let a = std::f64::consts::TAU * turns[i];
data[[i, 0]] += a.cos();
data[[i, 1]] += a.sin();
for j in 0..p {
data[[i, j]] += 0.05 * lcg_normal(&mut s);
}
}
let w_on = vec![1.0_f64; n];
let spec_before = spectrum_from_fit(data.view(), &w_on, &phi, 0.0025, 0.0, None).unwrap();
let m_extra = 600usize;
let n_aug = n + m_extra;
let mut turns_aug = turns.clone();
let mut data_aug = Array2::<f64>::zeros((n_aug, p));
data_aug.slice_mut(ndarray::s![0..n, ..]).assign(&data);
for _ in 0..m_extra {
turns_aug.push(lcg(&mut s)); }
for i in n..n_aug {
for j in 0..p {
data_aug[[i, j]] = lcg_normal(&mut s);
}
}
let phi_aug = harmonic_phi(&turns_aug, 3);
let mut w_aug = vec![1.0_f64; n];
w_aug.extend(std::iter::repeat(0.0_f64).take(m_extra));
let spec_after =
spectrum_from_fit(data_aug.view(), &w_aug, &phi_aug, 0.0025, 0.0, None).unwrap();
assert_eq!(
spec_before.production_charge(),
spec_after.production_charge(),
"inert (gate-off) rows must not change the rank charge: before={} after={}",
spec_before.production_charge(),
spec_after.production_charge()
);
assert_eq!(spec_before.n_eff, spec_after.n_eff);
assert!(
spec_after.production_chargeable_rank() > 0,
"fixture must have a real chargeable atom"
);
let old_before = 0.5
* spec_before.production_chargeable_rank() as f64
* spec_before.basis_edf
* (n as f64).ln();
let old_after = 0.5
* spec_after.production_chargeable_rank() as f64
* spec_after.basis_edf
* (n_aug as f64).ln();
assert!(
old_after > old_before + 1e-6,
"the OLD global-n scale WOULD have inflated the charge on inert rows \
(old_before={old_before:.4} old_after={old_after:.4}); the fix removes exactly this"
);
}
#[test]
fn rank_charge_equals_half_deff_ln_neff() {
let spec = ReconSpectrum {
mu: vec![10.0, 0.01],
edge: 1.0,
dispersion: 1.0,
basis_edf: 3.0,
n_eff: 50.0,
};
assert_eq!(spec.mp_reconstruction_rank(), 1);
assert_eq!(spec.production_chargeable_rank(), 1);
let d_eff = spec.production_chargeable_rank() as f64 * spec.basis_edf(); let expected = 0.5 * d_eff * (50.0_f64).ln();
assert!(
(spec.production_charge() - expected).abs() < 1e-12,
"rank charge must be ½·d_eff·ln(N_eff)={expected}, got {}",
spec.production_charge()
);
let global = 0.5 * d_eff * (5000.0_f64).ln();
assert!(
(spec.production_charge() - global).abs() > 1.0,
"charge must use N_eff (50), not a global n (5000)"
);
}
#[test]
fn soft_ledger_reduces_to_hard_away_from_edge_and_undercuts_near_it() {
let n = 1200usize;
let p = 16usize;
let mut s = 0xA1A1_u64;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let phi = harmonic_phi(&turns, 3);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let a = std::f64::consts::TAU * turns[i];
data[[i, 0]] += a.cos();
data[[i, 1]] += a.sin();
for j in 0..p {
data[[i, j]] += 0.05 * lcg_normal(&mut s);
}
}
let w = vec![1.0_f64; n];
let clean = spectrum_from_fit(data.view(), &w, &phi, 0.0025, 0.0, None).unwrap();
assert!(clean.production_charge() > 0.0);
let clean_ratio = clean.wbic_charge() / clean.production_charge();
assert!(
(clean_ratio - 1.0).abs() < 0.05,
"clean circle: soft ledger must reduce to the hard charge away from the edge \
(ratio soft/hard={clean_ratio:.3})"
);
let mut s = 0xB2B2_u64;
let turns: Vec<f64> = (0..n).map(|_| lcg(&mut s)).collect();
let radii: Vec<f64> = (0..n).map(|_| lcg(&mut s).sqrt()).collect();
let phi = harmonic_phi(&turns, 3);
let mut data = Array2::<f64>::zeros((n, p));
for i in 0..n {
let a = std::f64::consts::TAU * turns[i];
data[[i, 0]] += 0.35 * radii[i] * a.cos();
data[[i, 1]] += 0.35 * radii[i] * a.sin();
for j in 0..p {
data[[i, j]] += 0.12 * lcg_normal(&mut s);
}
}
let disk = spectrum_from_fit(data.view(), &w, &phi, 0.12 * 0.12, 0.0, None).unwrap();
assert!(
disk.mp_reconstruction_rank() > 0
&& disk.rank_soft() < disk.mp_reconstruction_rank() as f64,
"disk fixture must have above-edge directions the tempered count discounts: \
hard={} soft={:.3}",
disk.mp_reconstruction_rank(),
disk.rank_soft()
);
assert!(
disk.wbic_charge() < disk.production_charge(),
"disk: soft ledger must undercut the hard charge near the edge \
(soft={:.4} hard={:.4})",
disk.wbic_charge(),
disk.production_charge()
);
let disk_ratio = disk.wbic_charge() / disk.production_charge();
assert!(
disk_ratio < clean_ratio - 0.1,
"near-edge soft/hard ratio ({disk_ratio:.3}) must be clearly below the \
far-from-edge ratio ({clean_ratio:.3})"
);
}
}