const MAX_SWEEPS: usize = 100;
#[derive(Debug, Clone)]
pub struct SymEigen {
pub values: Vec<f32>,
pub vectors: Vec<f32>,
}
#[allow(clippy::many_single_char_names)]
#[must_use]
pub fn jacobi_eigen(a: &[f32], n: usize) -> SymEigen {
assert_eq!(a.len(), n * n, "matrix buffer must be n*n");
let mut work: Vec<f32> = a.to_vec();
let mut vecs: Vec<f32> = vec![0.0; n * n];
for i in 0..n {
vecs[i * n + i] = 1.0;
}
if n <= 1 {
return SymEigen {
values: work,
vectors: vecs,
};
}
let tol: f32 = 1e-14;
for _ in 0..MAX_SWEEPS {
let mut off: f32 = 0.0;
for p in 0..n {
for q in (p + 1)..n {
off += work[p * n + q] * work[p * n + q];
}
}
if off <= tol {
break;
}
for p in 0..n {
for q in (p + 1)..n {
let apq: f32 = work[p * n + q];
if apq.abs() <= f32::EPSILON {
continue;
}
let app: f32 = work[p * n + p];
let aqq: f32 = work[q * n + q];
let theta: f32 = (aqq - app) / (2.0 * apq);
let t: f32 = if theta >= 0.0 {
1.0 / (theta + (1.0 + theta * theta).sqrt())
} else {
-1.0 / (-theta + (1.0 + theta * theta).sqrt())
};
let c: f32 = 1.0 / (1.0 + t * t).sqrt();
let s: f32 = t * c;
for r in 0..n {
let arp: f32 = work[r * n + p];
let arq: f32 = work[r * n + q];
work[r * n + p] = c * arp - s * arq;
work[r * n + q] = s * arp + c * arq;
}
for r in 0..n {
let apr: f32 = work[p * n + r];
let aqr: f32 = work[q * n + r];
work[p * n + r] = c * apr - s * aqr;
work[q * n + r] = s * apr + c * aqr;
}
work[p * n + q] = 0.0;
work[q * n + p] = 0.0;
for r in 0..n {
let vrp: f32 = vecs[r * n + p];
let vrq: f32 = vecs[r * n + q];
vecs[r * n + p] = c * vrp - s * vrq;
vecs[r * n + q] = s * vrp + c * vrq;
}
}
}
}
let eigvals: Vec<f32> = (0..n).map(|i| work[i * n + i]).collect();
SymEigen {
values: eigvals,
vectors: vecs,
}
}
#[must_use]
pub fn cholesky(a: &[f32], n: usize) -> Option<Vec<f32>> {
assert_eq!(a.len(), n * n, "matrix buffer must be n*n");
let mut l: Vec<f32> = vec![0.0; n * n];
for i in 0..n {
for j in 0..=i {
let mut sum: f32 = a[i * n + j];
for k in 0..j {
sum -= l[i * n + k] * l[j * n + k];
}
if i == j {
if !sum.is_finite() || sum <= 0.0 {
return None;
}
l[i * n + i] = sum.sqrt();
} else {
l[i * n + j] = sum / l[j * n + j];
}
}
}
Some(l)
}
#[must_use]
pub fn matvec(m: &[f32], x: &[f32], n: usize) -> Vec<f32> {
assert_eq!(m.len(), n * n, "matrix buffer must be n*n");
assert_eq!(x.len(), n, "vector length must be n");
let mut y: Vec<f32> = vec![0.0; n];
for i in 0..n {
let mut acc: f32 = 0.0;
for j in 0..n {
acc += m[i * n + j] * x[j];
}
y[i] = acc;
}
y
}
pub fn symmetrize(m: &mut [f32], n: usize) {
assert_eq!(m.len(), n * n, "matrix buffer must be n*n");
for i in 0..n {
for j in 0..i {
let avg: f32 = 0.5 * (m[i * n + j] + m[j * n + i]);
m[i * n + j] = avg;
m[j * n + i] = avg;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reconstruct(eigvals: &[f32], eigvecs: &[f32], n: usize) -> Vec<f32> {
let mut out: Vec<f32> = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
let mut acc: f32 = 0.0;
for k in 0..n {
acc += eigvecs[i * n + k] * eigvals[k] * eigvecs[j * n + k];
}
out[i * n + j] = acc;
}
}
out
}
fn assert_matrix_close(a: &[f32], b: &[f32], eps: f32) {
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b.iter()) {
approx::assert_relative_eq!(x, y, epsilon = eps);
}
}
#[test]
fn eigen_diagonal_matrix() {
let a: Vec<f32> = vec![3.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 7.0];
let SymEigen { values, vectors } = jacobi_eigen(&a, 3);
let recon = reconstruct(&values, &vectors, 3);
assert_matrix_close(&a, &recon, 1e-5);
}
#[test]
fn eigen_known_2x2() {
let a: Vec<f32> = vec![2.0, 1.0, 1.0, 2.0];
let SymEigen { values, vectors } = jacobi_eigen(&a, 2);
let mut sorted: Vec<f32> = values.clone();
sorted.sort_by(f32::total_cmp);
approx::assert_relative_eq!(sorted[0], 1.0, epsilon = 1e-5);
approx::assert_relative_eq!(sorted[1], 3.0, epsilon = 1e-5);
let recon = reconstruct(&values, &vectors, 2);
assert_matrix_close(&a, &recon, 1e-5);
}
#[test]
fn eigen_3x3_reconstructs_and_is_orthonormal() {
let a: Vec<f32> = vec![4.0, 1.0, 2.0, 1.0, 5.0, 3.0, 2.0, 3.0, 6.0];
let SymEigen { values, vectors } = jacobi_eigen(&a, 3);
let recon = reconstruct(&values, &vectors, 3);
assert_matrix_close(&a, &recon, 1e-4);
for p in 0..3 {
for q in 0..3 {
let mut dot: f32 = 0.0;
for i in 0..3 {
dot += vectors[i * 3 + p] * vectors[i * 3 + q];
}
let expected: f32 = if p == q { 1.0 } else { 0.0 };
approx::assert_relative_eq!(dot, expected, epsilon = 1e-4);
}
}
}
#[test]
fn eigen_identity_is_fixed_point() {
let a: Vec<f32> = vec![1.0, 0.0, 0.0, 1.0];
let SymEigen { values, vectors } = jacobi_eigen(&a, 2);
for v in &values {
approx::assert_relative_eq!(v, &1.0, epsilon = 1e-6);
}
assert_matrix_close(&vectors, &[1.0, 0.0, 0.0, 1.0], 1e-6);
}
#[test]
fn cholesky_known_2x2() {
let a: Vec<f32> = vec![4.0, 2.0, 2.0, 3.0];
let l = cholesky(&a, 2).expect("matrix is positive-definite");
approx::assert_relative_eq!(l[0], 2.0, epsilon = 1e-6);
approx::assert_relative_eq!(l[1], 0.0, epsilon = 1e-6);
approx::assert_relative_eq!(l[2], 1.0, epsilon = 1e-6);
approx::assert_relative_eq!(l[3], 2.0_f32.sqrt(), epsilon = 1e-6);
let mut recon: Vec<f32> = vec![0.0; 4];
for i in 0..2 {
for j in 0..2 {
let mut acc: f32 = 0.0;
for k in 0..2 {
acc += l[i * 2 + k] * l[j * 2 + k];
}
recon[i * 2 + j] = acc;
}
}
assert_matrix_close(&a, &recon, 1e-6);
}
#[test]
fn cholesky_rejects_non_positive_definite() {
let a: Vec<f32> = vec![1.0, 2.0, 2.0, 1.0];
assert!(cholesky(&a, 2).is_none());
}
#[test]
fn cholesky_rejects_nan_on_diagonal() {
let a: Vec<f32> = vec![f32::NAN, 0.0, 0.0, 1.0];
assert!(cholesky(&a, 2).is_none());
}
#[test]
fn cholesky_rejects_off_diagonal_only_nan() {
let a: Vec<f32> = vec![1.0, f32::NAN, f32::NAN, 1.0];
assert!(cholesky(&a, 2).is_none());
}
#[test]
fn cholesky_rejects_infinity() {
let a: Vec<f32> = vec![f32::INFINITY, 0.0, 0.0, 1.0];
assert!(cholesky(&a, 2).is_none());
}
#[test]
fn symmetrize_averages_asymmetric_and_is_idempotent() {
let mut m: Vec<f32> = vec![1.0, 2.0, 4.0, 5.0];
symmetrize(&mut m, 2);
assert_matrix_close(&m, &[1.0, 3.0, 3.0, 5.0], 1e-6);
let once: Vec<f32> = m.clone();
symmetrize(&mut m, 2);
assert_matrix_close(&m, &once, 1e-6);
}
#[test]
fn symmetrize_leaves_symmetric_unchanged() {
let mut m: Vec<f32> = vec![4.0, 1.0, 2.0, 1.0, 5.0, 3.0, 2.0, 3.0, 6.0];
let before: Vec<f32> = m.clone();
symmetrize(&mut m, 3);
assert_matrix_close(&m, &before, 1e-6);
}
#[test]
fn symmetrize_handles_scalar_and_identity() {
let mut one: Vec<f32> = vec![7.0];
symmetrize(&mut one, 1);
assert_eq!(one, vec![7.0]);
let mut id: Vec<f32> = vec![1.0, 0.0, 0.0, 1.0];
symmetrize(&mut id, 2);
assert_matrix_close(&id, &[1.0, 0.0, 0.0, 1.0], 1e-6);
}
#[test]
fn matvec_identity_and_general() {
let id: Vec<f32> = vec![1.0, 0.0, 0.0, 1.0];
let x: Vec<f32> = vec![3.0, -2.0];
assert_eq!(matvec(&id, &x, 2), vec![3.0, -2.0]);
let m: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
assert_eq!(matvec(&m, &[1.0, 1.0], 2), vec![3.0, 7.0]);
}
}