#[allow(clippy::needless_range_loop)] pub fn cholesky_lower(omega: &[Vec<f64>]) -> Option<Vec<Vec<f64>>> {
let n = omega.len();
let mut l = vec![vec![0.0f64; n]; n];
for i in 0..n {
for j in 0..=i {
let mut sum = omega[i][j];
for k in 0..j {
sum -= l[i][k] * l[j][k];
}
if i == j {
if sum <= 0.0 {
return None; }
l[i][j] = sum.sqrt();
} else {
l[i][j] = sum / l[j][j];
}
}
}
Some(l)
}
pub fn whiten(omega: &[Vec<f64>], residuals: &[f64]) -> Option<Vec<f64>> {
if omega.len() != residuals.len() {
return None;
}
let l = cholesky_lower(omega)?;
let n = residuals.len();
let mut z = vec![0.0f64; n];
for i in 0..n {
let mut sum = residuals[i];
for k in 0..i {
sum -= l[i][k] * z[k];
}
z[i] = sum / l[i][i];
}
Some(z)
}
pub fn mahalanobis_sq(omega: &[Vec<f64>], residuals: &[f64]) -> Option<f64> {
let z = whiten(omega, residuals)?;
Some(z.iter().map(|zi| zi * zi).sum())
}
#[derive(Clone, Copy, Debug)]
pub struct CommonModeStatistic {
pub value: f64,
pub dof: usize,
}
pub fn common_mode_consistency(
omega: &[Vec<f64>],
residuals: &[f64],
) -> Option<CommonModeStatistic> {
let n = residuals.len();
if n == 0 {
return None;
}
let ones = vec![1.0f64; n];
let w1 = whiten(omega, &ones)?;
let wr = whiten(omega, residuals)?;
let num: f64 = w1.iter().zip(&wr).map(|(a, b)| a * b).sum(); let den: f64 = w1.iter().map(|a| a * a).sum(); if den <= 0.0 {
return None;
}
Some(CommonModeStatistic {
value: num * num / den,
dof: 1,
})
}
pub fn residual_outside_omega_bound(
omega_modelled: &[Vec<f64>],
true_common_mode_dir: &[f64],
ss_threshold: f64,
cm_threshold: f64,
) -> f64 {
let n = true_common_mode_dir.len();
if n == 0 {
return f64::INFINITY;
}
let d = true_common_mode_dir;
let ones = vec![1.0f64; n];
let (w1, wd) = match (whiten(omega_modelled, &ones), whiten(omega_modelled, d)) {
(Some(a), Some(b)) => (a, b),
_ => return f64::INFINITY,
};
let s11: f64 = w1.iter().map(|a| a * a).sum(); let s1d: f64 = w1.iter().zip(&wd).map(|(a, b)| a * b).sum(); let mu = if s11 > 0.0 { s1d / s11 } else { 0.0 };
let d_perp: Vec<f64> = d.iter().map(|di| di - mu).collect();
let wperp = match whiten(omega_modelled, &d_perp) {
Some(v) => v,
None => return f64::INFINITY,
};
let contrast_norm: f64 = wperp.iter().map(|a| a * a).sum::<f64>().sqrt();
let alpha_ss = if contrast_norm > 1e-30 {
ss_threshold / contrast_norm
} else {
f64::INFINITY
};
let alpha_cm = if s1d.abs() > 1e-30 && s11 > 0.0 {
(cm_threshold * s11).sqrt() / s1d.abs()
} else {
f64::INFINITY
};
alpha_ss.min(alpha_cm)
}
#[cfg(test)]
mod tests {
use super::*;
fn matvec(m: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
m.iter()
.map(|row| row.iter().zip(v).map(|(a, b)| a * b).sum())
.collect()
}
#[test]
#[allow(clippy::needless_range_loop)] fn cholesky_reconstructs_omega() {
let omega = vec![
vec![4.0, 1.0, 0.5],
vec![1.0, 3.0, 0.2],
vec![0.5, 0.2, 2.0],
];
let l = cholesky_lower(&omega).expect("PD");
let n = 3;
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += l[i][k] * l[j][k];
}
assert!((s - omega[i][j]).abs() < 1e-12, "L Lᵀ != Ω at {i},{j}");
}
}
}
#[test]
fn cholesky_rejects_non_pd() {
let bad = vec![vec![1.0, 2.0], vec![2.0, 1.0]];
assert!(cholesky_lower(&bad).is_none());
}
#[test]
fn whiten_gives_mahalanobis_identity() {
let omega = vec![vec![4.0, 1.0], vec![1.0, 3.0]];
let r = [2.0, -1.0];
let m = mahalanobis_sq(&omega, &r).unwrap();
let det = 4.0 * 3.0 - 1.0 * 1.0;
let inv = [[3.0 / det, -1.0 / det], [-1.0 / det, 4.0 / det]];
let iv = matvec(&inv.iter().map(|row| row.to_vec()).collect::<Vec<_>>(), &r);
let quad = r[0] * iv[0] + r[1] * iv[1];
assert!(
(m - quad).abs() < 1e-12,
"Mahalanobis identity zᵀz = rᵀΩ⁻¹r"
);
}
#[test]
fn common_mode_shift_inflates_statistic_but_not_contrasts() {
let omega = vec![
vec![4.0, 1.5, 1.5],
vec![1.5, 4.0, 1.5],
vec![1.5, 1.5, 4.0],
];
let null = [0.05, -0.03, 0.02];
let shift = [1.0, 1.0, 1.0];
let s_null = common_mode_consistency(&omega, &null).unwrap().value;
let s_shift = common_mode_consistency(&omega, &shift).unwrap().value;
assert!(
s_shift > s_null * 100.0,
"a common-mode shift must inflate the statistic"
);
let b = residual_outside_omega_bound(&omega, &shift, 3.0, 3.841);
assert!(
b.is_finite(),
"a pure common-mode fault is caught by the cm statistic (finite ceiling)"
);
}
#[test]
fn separation_alone_is_blind_to_common_mode() {
let omega = vec![vec![2.0, 0.5], vec![0.5, 2.0]];
let d = [1.0, 1.0];
let d_blind = [1.0, -1.0];
let b_common = residual_outside_omega_bound(&omega, &d, 3.0, 3.841);
let b_blind = residual_outside_omega_bound(&omega, &d_blind, 3.0, 3.841);
assert!(
b_common.is_finite(),
"cm statistic catches the modelled common axis"
);
assert!(
b_blind.is_finite(),
"a pure contrast is caught by separation"
);
assert!(
b_blind < b_common * 100.0,
"sanity: both bounds are real magnitudes"
);
}
#[test]
fn degenerate_direction_or_non_pd_gives_no_finite_ceiling() {
let omega = vec![vec![1.0]];
let d_zero = [0.0];
let b = residual_outside_omega_bound(&omega, &d_zero, 3.0, 3.841);
assert!(
b.is_infinite(),
"a zero-signal direction is undetectable by construction"
);
}
#[test]
fn every_nonzero_direction_under_pd_omega_has_a_finite_ceiling() {
let omega = vec![
vec![4.0, 1.5, 1.5],
vec![1.5, 4.0, 1.5],
vec![1.5, 1.5, 4.0],
];
for d in [
[1.0, 1.0, 1.0], [1.0, -1.0, 0.0], [2.0, 0.3, -1.1], [1.0, 1.0, 0.9], ] {
let b = residual_outside_omega_bound(&omega, &d, 3.0, 3.841);
assert!(
b.is_finite() && b > 0.0,
"nonzero direction {d:?} must have a finite positive ceiling, got {b}"
);
}
}
#[test]
fn post_fit_common_mode_statistic_is_zero() {
let omega = vec![
vec![4.0, 1.5, 0.8],
vec![1.5, 3.0, 0.5],
vec![0.8, 0.5, 2.5],
];
let y = [1.2, -0.7, 0.4];
let ones = [1.0, 1.0, 1.0];
let w1 = whiten(&omega, &ones).unwrap();
let wy = whiten(&omega, &y).unwrap();
let s1y: f64 = w1.iter().zip(&wy).map(|(a, b)| a * b).sum();
let s11: f64 = w1.iter().map(|a| a * a).sum();
let xhat = s1y / s11; let r_postfit: Vec<f64> = y.iter().map(|yi| yi - xhat).collect();
let s = common_mode_consistency(&omega, &r_postfit).unwrap();
assert!(
s.value.abs() < 1e-12,
"post-fit common-mode statistic must be ~0 (I2), got {}",
s.value
);
}
}