use ndarray::{Array1, Array2};
pub fn equilibrate_gram(g: &Array2<f64>) -> (Array2<f64>, Array1<f64>) {
let p = g.nrows();
let scale: Array1<f64> = Array1::from_shape_fn(p, |j| {
let d = g[[j, j]];
if d > 0.0 { d.sqrt() } else { 1.0 }
});
let mut c = Array2::<f64>::zeros((p, p));
for i in 0..p {
for j in 0..p {
c[[i, j]] = g[[i, j]] / (scale[i] * scale[j]);
}
}
(c, scale)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RankDecision {
Certified {
rank: usize,
sigma_r: f64,
sigma_next: f64,
margin_low: f64,
margin_high: f64,
},
Ambiguous {
rank_floor: usize,
rank_ceil: usize,
sigma_in_band: f64,
tol: f64,
gap: f64,
},
}
pub fn certified_rank(singular_values: &[f64], tol: f64, gap: f64) -> RankDecision {
let mut sv: Vec<f64> = singular_values.to_vec();
sv.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let n = sv.len();
let high = tol * (1.0 + gap);
let low = tol / (1.0 + gap);
if let Some(&sigma_in_band) = sv.iter().find(|&&s| s > low && s < high) {
let rank_floor = sv.iter().filter(|&&s| s >= high).count();
let rank_ceil = sv.iter().filter(|&&s| s > low).count();
return RankDecision::Ambiguous {
rank_floor,
rank_ceil,
sigma_in_band,
tol,
gap,
};
}
let rank = sv.iter().filter(|&&s| s >= high).count();
let sigma_r = if rank == 0 { f64::INFINITY } else { sv[rank - 1] };
let sigma_next = if rank < n { sv[rank] } else { 0.0 };
let margin_high = if rank == 0 { f64::INFINITY } else { sigma_r / high };
let margin_low = if sigma_next == 0.0 {
f64::INFINITY
} else {
low / sigma_next
};
RankDecision::Certified {
rank,
sigma_r,
sigma_next,
margin_low,
margin_high,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DecrementEnclosure {
pub lower: f64,
pub upper: f64,
}
pub fn newton_decrement_enclosure(
g_dot_z: f64,
r_dot_z: f64,
r_norm_sq: f64,
lambda_min_lower: f64,
) -> Option<DecrementEnclosure> {
if lambda_min_lower <= 0.0 {
return None;
}
let lower = g_dot_z + r_dot_z;
let upper = lower + r_norm_sq / lambda_min_lower;
Some(DecrementEnclosure { lower, upper })
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ShadowSum {
pub sum: f64,
pub abs_sum: f64,
pub count: usize,
}
impl ShadowSum {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, x: f64) {
self.sum += x;
self.abs_sum += x.abs();
self.count += 1;
}
pub fn merge(&mut self, other: &ShadowSum) {
self.sum += other.sum;
self.abs_sum += other.abs_sum;
self.count += other.count;
}
pub fn rounding_floor(&self, unit_roundoff: f64) -> f64 {
let depth = self.count.saturating_sub(1);
gamma(depth, unit_roundoff) * self.abs_sum
}
pub fn rounding_floor_with_depth(&self, unit_roundoff: f64, depth: usize) -> f64 {
gamma(depth, unit_roundoff) * self.abs_sum
}
}
fn gamma(k: usize, unit_roundoff: f64) -> f64 {
let ku = (k as f64) * unit_roundoff;
if ku >= 1.0 {
f64::INFINITY
} else {
ku / (1.0 - ku)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::faer_ndarray::{FaerEigh, fast_ata};
use faer::Side;
use ndarray::{Array1, Array2};
const U: f64 = f64::EPSILON / 2.0;
fn eigenvalues(m: &Array2<f64>) -> Vec<f64> {
let (evals, _) = m.eigh(Side::Lower).expect("eigh");
evals.to_vec()
}
#[test]
fn equilibration_certifies_full_rank_where_raw_gram_would_kill_eleven_columns() {
let n = 50usize;
let p = 12usize;
let stiff = 2.4e6_f64;
let mut x = Array2::<f64>::zeros((n, p));
for j in 0..p {
x[[j, j]] = if j == 0 { stiff } else { 1.0 };
}
let g = fast_ata(&x);
let raw_evals = eigenvalues(&g);
let raw_lambda_max = raw_evals.iter().cloned().fold(0.0_f64, f64::max);
let raw_tol = raw_lambda_max * 64.0 * (n as f64) * f64::EPSILON;
let raw_rank = raw_evals.iter().filter(|&&e| e > raw_tol).count();
assert_eq!(raw_rank, 1, "raw size-scaled cutoff must kill 11 columns");
let (g_eq, _) = equilibrate_gram(&g);
let eq_evals = eigenvalues(&g_eq);
for &e in &eq_evals {
assert!((e - 1.0).abs() < 1e-9, "equilibrated spectrum must be ~1");
}
let eq_lambda_max = eq_evals.iter().cloned().fold(0.0_f64, f64::max);
let nk = (n.max(p)) as f64;
let eq_tol = eq_lambda_max * 64.0 * nk * f64::EPSILON;
match certified_rank(&eq_evals, eq_tol, 1.0) {
RankDecision::Certified {
rank, margin_high, ..
} => {
assert_eq!(rank, 12, "equilibrated Gram is full rank");
assert!(
margin_high > 1e10,
"kept side must clear the band by a huge factor, got {margin_high}"
);
}
other => panic!("expected Certified full rank, got {other:?}"),
}
}
#[test]
fn spectrum_inside_two_sided_band_is_ambiguous() {
let sv = [10.0_f64, 3.0, 1.0, 0.2];
match certified_rank(&sv, 1.0, 1.0) {
RankDecision::Ambiguous {
rank_floor,
rank_ceil,
sigma_in_band,
..
} => {
assert_eq!(rank_floor, 2, "#{{σ ≥ 2}} = 2");
assert_eq!(rank_ceil, 3, "#{{σ > 0.5}} = 3");
assert_eq!(sigma_in_band, 1.0);
}
other => panic!("expected Ambiguous, got {other:?}"),
}
}
#[test]
fn decrement_enclosure_is_exact_when_residual_zero_and_contains_truth_when_perturbed() {
let h = Array2::from_shape_vec(
(3, 3),
vec![4.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0],
)
.unwrap();
let g = Array1::from_vec(vec![1.0, -2.0, 0.5]);
let (evals, evecs) = h.eigh(Side::Lower).expect("eigh");
let lambda_min = evals.iter().cloned().fold(f64::INFINITY, f64::min);
assert!(lambda_min > 0.0, "H must be SPD");
let vt_g = evecs.t().dot(&g);
let scaled: Array1<f64> =
Array1::from_shape_fn(3, |i| vt_g[i] / evals[i]);
let z = evecs.dot(&scaled);
let true_lambda_n_sq = g.dot(&z);
let hz = h.dot(&z);
let r = &g - &hz;
let g_dot_z = g.dot(&z);
let r_dot_z = r.dot(&z);
let r_norm_sq = r.dot(&r);
let ell = lambda_min * 0.999; let exact = newton_decrement_enclosure(g_dot_z, r_dot_z, r_norm_sq, ell)
.expect("positive definite");
assert!(
(exact.upper - exact.lower).abs() < 1e-10,
"width must be ~0 when r=0"
);
assert!((exact.lower - true_lambda_n_sq).abs() < 1e-9);
let z_bad = &z + &Array1::from_vec(vec![0.05, -0.03, 0.02]);
let hz_bad = h.dot(&z_bad);
let r_bad = &g - &hz_bad;
let encl = newton_decrement_enclosure(
g.dot(&z_bad),
r_bad.dot(&z_bad),
r_bad.dot(&r_bad),
ell,
)
.expect("positive definite");
assert!(
encl.lower <= true_lambda_n_sq + 1e-9 && true_lambda_n_sq <= encl.upper + 1e-9,
"enclosure [{}, {}] must contain λ_N² = {true_lambda_n_sq}",
encl.lower,
encl.upper
);
assert!(encl.upper - encl.lower > 0.0, "inexact solve widens the band");
assert!(newton_decrement_enclosure(g_dot_z, r_dot_z, r_norm_sq, 0.0).is_none());
}
#[test]
fn shadow_sum_error_stays_within_certified_rounding_floor() {
let mut acc = ShadowSum::new();
for _ in 0..1_000_000 {
acc.push(0.1);
}
assert_eq!(acc.count, 1_000_000);
let exact = 100_000.0_f64;
let error = (acc.sum - exact).abs();
let floor = acc.rounding_floor(U);
assert!(
error <= floor,
"summation error {error} must not exceed rounding floor {floor}"
);
assert!(acc.rounding_floor_with_depth(U, 20) < floor);
}
#[test]
fn shadow_sum_merge_is_additive() {
let mut a = ShadowSum::new();
let mut b = ShadowSum::new();
a.push(1.0);
a.push(-2.0);
b.push(3.0);
a.merge(&b);
assert_eq!(a.count, 3);
assert_eq!(a.sum, 2.0);
assert_eq!(a.abs_sum, 6.0);
}
#[test]
fn gamma_saturates_when_ku_exceeds_one() {
assert!(gamma(usize::MAX, U).is_infinite());
assert_eq!(gamma(0, U), 0.0);
}
}