#![allow(clippy::needless_range_loop)]
use ndarray::{Array1, array};
use crate::types::{F, F3, F3x3};
const MAX_SWEEPS: usize = 50;
const OFF_DIAG_TOL: F = 1e-14;
pub fn eigvals_sym_3x3(a: &F3x3) -> F3 {
let (vals, _) = eigh_sym_3x3(a);
vals
}
pub fn eigh_largest_sym_4x4(a: &[[F; 4]; 4]) -> (F, [F; 4]) {
let mut m: [[F; 4]; 4] = *a;
let mut v: [[F; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let pairs = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
for _ in 0..MAX_SWEEPS {
let off: F = pairs.iter().map(|&(p, q)| m[p][q].abs()).sum();
if off < OFF_DIAG_TOL {
break;
}
for (p, q) in pairs {
let apq = m[p][q];
if apq.abs() < OFF_DIAG_TOL {
continue;
}
let app = m[p][p];
let aqq = m[q][q];
let theta = if (app - aqq).abs() < OFF_DIAG_TOL {
std::f64::consts::FRAC_PI_4 * apq.signum()
} else {
0.5 * (2.0 * apq).atan2(app - aqq)
};
let c = theta.cos();
let s = theta.sin();
let new_app = c * c * app + 2.0 * s * c * apq + s * s * aqq;
let new_aqq = s * s * app - 2.0 * s * c * apq + c * c * aqq;
m[p][p] = new_app;
m[q][q] = new_aqq;
m[p][q] = 0.0;
m[q][p] = 0.0;
for r in 0..4 {
if r != p && r != q {
let arp = m[r][p];
let arq = m[r][q];
let nrp = c * arp + s * arq;
let nrq = -s * arp + c * arq;
m[r][p] = nrp;
m[p][r] = nrp;
m[r][q] = nrq;
m[q][r] = nrq;
}
}
for r in 0..4 {
let vrp = v[r][p];
let vrq = v[r][q];
v[r][p] = c * vrp + s * vrq;
v[r][q] = -s * vrp + c * vrq;
}
}
}
let mut imax = 0usize;
let mut lmax = m[0][0];
for i in 1..4 {
if m[i][i] > lmax {
imax = i;
lmax = m[i][i];
}
}
(lmax, [v[0][imax], v[1][imax], v[2][imax], v[3][imax]])
}
pub fn eigh_sym_3x3(a: &F3x3) -> (F3, F3x3) {
debug_assert_eq!(a.dim(), (3, 3), "eigh_sym_3x3 expects a 3×3 matrix");
let mut m: [[F; 3]; 3] = [
[a[[0, 0]], a[[0, 1]], a[[0, 2]]],
[a[[0, 1]], a[[1, 1]], a[[1, 2]]],
[a[[0, 2]], a[[1, 2]], a[[2, 2]]],
];
let mut v: [[F; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
for _ in 0..MAX_SWEEPS {
let off = m[0][1].abs() + m[0][2].abs() + m[1][2].abs();
if off < OFF_DIAG_TOL {
break;
}
for (p, q) in [(0, 1), (0, 2), (1, 2)] {
let apq = m[p][q];
if apq.abs() < OFF_DIAG_TOL {
continue;
}
let app = m[p][p];
let aqq = m[q][q];
let theta = if (app - aqq).abs() < OFF_DIAG_TOL {
std::f64::consts::FRAC_PI_4 * apq.signum()
} else {
0.5 * (2.0 * apq).atan2(app - aqq)
};
let c = theta.cos();
let s = theta.sin();
let new_app = c * c * app + 2.0 * s * c * apq + s * s * aqq;
let new_aqq = s * s * app - 2.0 * s * c * apq + c * c * aqq;
m[p][p] = new_app;
m[q][q] = new_aqq;
m[p][q] = 0.0;
m[q][p] = 0.0;
for r in 0..3 {
if r != p && r != q {
let arp = m[r][p];
let arq = m[r][q];
let new_arp = c * arp + s * arq;
let new_arq = -s * arp + c * arq;
m[r][p] = new_arp;
m[p][r] = new_arp;
m[r][q] = new_arq;
m[q][r] = new_arq;
}
}
for r in 0..3 {
let vrp = v[r][p];
let vrq = v[r][q];
v[r][p] = c * vrp + s * vrq;
v[r][q] = -s * vrp + c * vrq;
}
}
}
let mut vals = [m[0][0], m[1][1], m[2][2]];
let mut order = [0usize, 1, 2];
order.sort_by(|&i, &j| vals[j].partial_cmp(&vals[i]).unwrap());
let sorted_vals = [vals[order[0]], vals[order[1]], vals[order[2]]];
let sorted_vecs: F3x3 = array![
[v[0][order[0]], v[0][order[1]], v[0][order[2]]],
[v[1][order[0]], v[1][order[1]], v[1][order[2]]],
[v[2][order[0]], v[2][order[1]], v[2][order[2]]],
];
let _ = &mut vals;
(Array1::from_vec(sorted_vals.to_vec()), sorted_vecs)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
const TOL: F = 1e-10;
fn approx_eq(a: F, b: F, tol: F) {
assert!((a - b).abs() < tol, "expected {b}, got {a} (Δ={})", a - b);
}
#[test]
fn diagonal_matrix_unchanged() {
let a: F3x3 = array![[3.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 2.0]];
let vals = eigvals_sym_3x3(&a);
approx_eq(vals[0], 3.0, TOL);
approx_eq(vals[1], 2.0, TOL);
approx_eq(vals[2], 1.0, TOL);
}
#[test]
fn identity_returns_ones() {
let a: F3x3 = array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let (vals, vecs) = eigh_sym_3x3(&a);
for v in vals.iter() {
approx_eq(*v, 1.0, TOL);
}
for c in 0..3 {
let mut norm: F = 0.0;
for r in 0..3 {
norm += vecs[[r, c]].powi(2);
}
approx_eq(norm, 1.0, TOL);
}
}
#[test]
fn known_two_by_two_embedded() {
let a: F3x3 = array![[2.0, 1.0, 0.0], [1.0, 2.0, 0.0], [0.0, 0.0, 5.0]];
let vals = eigvals_sym_3x3(&a);
approx_eq(vals[0], 5.0, TOL);
approx_eq(vals[1], 3.0, TOL);
approx_eq(vals[2], 1.0, TOL);
}
#[test]
fn eigenvector_reconstruction() {
let a: F3x3 = array![[4.0, 1.0, 2.0], [1.0, 3.0, -1.0], [2.0, -1.0, 5.0]];
let (vals, vecs) = eigh_sym_3x3(&a);
for i in 0..3 {
let v = vecs.column(i);
for r in 0..3 {
let av_r: F = (0..3).map(|c| a[[r, c]] * v[c]).sum();
approx_eq(av_r, vals[i] * v[r], 1e-8);
}
}
let tr_a: F = (0..3).map(|i| a[[i, i]]).sum();
let tr_eig: F = vals.sum();
approx_eq(tr_a, tr_eig, TOL);
}
#[test]
fn eigenvectors_are_orthonormal() {
let a: F3x3 = array![[7.0, 2.0, -1.0], [2.0, 5.0, 3.0], [-1.0, 3.0, 6.0]];
let (_, vecs) = eigh_sym_3x3(&a);
for i in 0..3 {
for j in 0..3 {
let mut dot: F = 0.0;
for r in 0..3 {
dot += vecs[[r, i]] * vecs[[r, j]];
}
let expected = if i == j { 1.0 } else { 0.0 };
approx_eq(dot, expected, 1e-10);
}
}
}
#[test]
fn degenerate_eigenvalues_handled() {
let a: F3x3 = array![[3.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]];
let vals = eigvals_sym_3x3(&a);
approx_eq(vals[0], 3.0, TOL);
approx_eq(vals[1], 2.0, TOL);
approx_eq(vals[2], 2.0, TOL);
}
#[test]
fn largest_eigenvalue_sym_4x4_diagonal() {
let m = [
[4.0_f64, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 7.0, 0.0],
[0.0, 0.0, 0.0, 2.0],
];
let (lambda, v) = eigh_largest_sym_4x4(&m);
approx_eq(lambda, 7.0, TOL);
approx_eq(v[0].abs(), 0.0, TOL);
approx_eq(v[1].abs(), 0.0, TOL);
approx_eq(v[2].abs(), 1.0, TOL);
approx_eq(v[3].abs(), 0.0, TOL);
}
#[test]
fn largest_eigenvalue_sym_4x4_general() {
let u = [1.0_f64, 2.0, 3.0, 1.0];
let mut m = [[0.0_f64; 4]; 4];
for i in 0..4 {
for j in 0..4 {
m[i][j] = u[i] * u[j];
}
}
let (lambda, v) = eigh_largest_sym_4x4(&m);
approx_eq(lambda, 15.0, 1e-9);
let dot: F = (0..4).map(|i| v[i] * u[i]).sum::<F>().abs();
let nu = 15.0_f64.sqrt();
approx_eq(dot, nu, 1e-9);
}
}