fn frobenius_norm(m: &[f64]) -> f64 {
m.iter().map(|&x| x * x).sum::<f64>().sqrt()
}
fn off_diagonal_sum_sq(m: &[f64], n: usize) -> f64 {
let mut s = 0.0;
for p in 0..n {
for q in (p + 1)..n {
let v = m[p * n + q];
s += v * v;
}
}
s
}
pub(super) fn jacobi_eigen(a: &[f64], n: usize) -> Option<(Vec<f64>, Vec<f64>)> {
debug_assert_eq!(a.len(), n * n, "jacobi_eigen: expected an n×n buffer");
let mut m = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..=i {
let val = a[i * n + j];
m[i * n + j] = val;
m[j * n + i] = val;
}
}
let mut v = vec![0.0_f64; n * n];
for i in 0..n {
v[i * n + i] = 1.0;
}
if n <= 1 {
let eigvals = (0..n).map(|i| m[i * n + i]).collect();
return Some((eigvals, v));
}
const MAX_SWEEPS: usize = 100;
let tol = 1e-15 * frobenius_norm(&m).max(f64::MIN_POSITIVE);
for _ in 0..MAX_SWEEPS {
if off_diagonal_sum_sq(&m, n).sqrt() <= tol {
let eigvals = (0..n).map(|i| m[i * n + i]).collect();
return Some((eigvals, v));
}
for p in 0..n {
for q in (p + 1)..n {
let apq = m[p * n + q];
if apq == 0.0 {
continue;
}
let app = m[p * n + p];
let aqq = m[q * n + q];
let theta = (aqq - app) / (2.0 * apq);
let t = if theta == 0.0 {
1.0
} else {
let sign = if theta > 0.0 { 1.0 } else { -1.0 };
sign / (theta.abs() + (theta * theta + 1.0).sqrt())
};
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
let tau = s / (1.0 + c);
m[p * n + p] = app - t * apq;
m[q * n + q] = aqq + t * apq;
m[p * n + q] = 0.0;
m[q * n + p] = 0.0;
for i in 0..n {
if i != p && i != q {
let aip = m[i * n + p];
let aiq = m[i * n + q];
let new_ip = aip - s * (aiq + tau * aip);
let new_iq = aiq + s * (aip - tau * aiq);
m[i * n + p] = new_ip;
m[p * n + i] = new_ip;
m[i * n + q] = new_iq;
m[q * n + i] = new_iq;
}
}
for i in 0..n {
let vip = v[i * n + p];
let viq = v[i * n + q];
v[i * n + p] = vip - s * (viq + tau * vip);
v[i * n + q] = viq + s * (vip - tau * viq);
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn reconstruct(eigvals: &[f64], eigvecs: &[f64], n: usize) -> Vec<f64> {
let mut out = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += eigvecs[i * n + k] * eigvals[k] * eigvecs[j * n + k];
}
out[i * n + j] = s;
}
}
out
}
fn assert_close(a: &[f64], b: &[f64], tol: f64) {
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b.iter()) {
assert!((x - y).abs() < tol, "‖{x} − {y}‖ ≥ {tol}");
}
}
fn assert_orthonormal(eigvecs: &[f64], n: usize) {
for p in 0..n {
for q in 0..n {
let mut dot = 0.0;
for i in 0..n {
dot += eigvecs[i * n + p] * eigvecs[i * n + q];
}
let expected = if p == q { 1.0 } else { 0.0 };
assert!(
(dot - expected).abs() < 1e-10,
"BᵀB[{p}][{q}] = {dot}, expected {expected}"
);
}
}
}
#[test]
fn known_2x2() {
let a = vec![2.0, 1.0, 1.0, 2.0];
let (eigs, vecs) = jacobi_eigen(&a, 2).unwrap();
assert_orthonormal(&vecs, 2);
assert_close(&reconstruct(&eigs, &vecs, 2), &a, 1e-12);
let mut sorted = eigs.clone();
sorted.sort_by(|x, y| x.partial_cmp(y).unwrap());
assert!((sorted[0] - 1.0).abs() < 1e-12, "λ₀ = {}", sorted[0]);
assert!((sorted[1] - 3.0).abs() < 1e-12, "λ₁ = {}", sorted[1]);
}
#[test]
fn reconstructs_symmetric_3x3() {
let a = vec![4.0, 1.0, -2.0, 1.0, 2.0, 0.0, -2.0, 0.0, 3.0];
let (eigs, vecs) = jacobi_eigen(&a, 3).unwrap();
assert_orthonormal(&vecs, 3);
assert_close(&reconstruct(&eigs, &vecs, 3), &a, 1e-10);
}
#[test]
fn diagonal_input_passthrough() {
let a = vec![3.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 7.0];
let (mut eigs, vecs) = jacobi_eigen(&a, 3).unwrap();
assert_orthonormal(&vecs, 3);
eigs.sort_by(|x, y| x.partial_cmp(y).unwrap());
assert_close(&eigs, &[3.0, 5.0, 7.0], 1e-12);
}
#[test]
fn lower_triangle_is_authoritative() {
let clean = vec![2.0, 1.0, 1.0, 2.0];
let dirty = vec![2.0, 99.0, 1.0, 2.0]; let (mut e_clean, _) = jacobi_eigen(&clean, 2).unwrap();
let (mut e_dirty, _) = jacobi_eigen(&dirty, 2).unwrap();
e_clean.sort_by(|x, y| x.partial_cmp(y).unwrap());
e_dirty.sort_by(|x, y| x.partial_cmp(y).unwrap());
assert_close(&e_clean, &e_dirty, 1e-14);
}
#[test]
fn reconstructs_spd_5x5() {
let n = 5;
let mraw = [
1.0, 0.3, -0.2, 0.5, 0.1, 0.0, 1.2, 0.4, -0.1, 0.2, 0.3, 0.0, 0.9, 0.6, -0.3, -0.4,
0.1, 0.2, 1.1, 0.0, 0.2, -0.5, 0.3, 0.1, 1.3,
];
let mut a = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += mraw[k * n + i] * mraw[k * n + j];
}
a[i * n + j] = s + if i == j { 5.0 } else { 0.0 };
}
}
let (eigs, vecs) = jacobi_eigen(&a, n).unwrap();
assert_orthonormal(&vecs, n);
for &lam in &eigs {
assert!(
lam > 0.0,
"SPD matrix must have positive eigenvalues, got {lam}"
);
}
assert_close(&reconstruct(&eigs, &vecs, n), &a, 1e-9);
}
#[test]
fn single_element() {
let (eigs, vecs) = jacobi_eigen(&[42.0], 1).unwrap();
assert_eq!(eigs, vec![42.0]);
assert_eq!(vecs, vec![1.0]);
}
}