use crate::linalg::LinalgError;
use crate::traits::{LinalgScalar, MatrixMut, MatrixRef};
use crate::Matrix;
use num_traits::{Float, One, Zero};
pub fn tridiagonalize<T: LinalgScalar>(
a: &impl MatrixRef<T>,
diag: &mut [T::Real],
off_diag: &mut [T::Real],
q: &mut impl MatrixMut<T>,
) {
let n = a.nrows();
assert_eq!(n, a.ncols(), "tridiagonalize requires a square matrix");
assert!(diag.len() >= n);
assert!(off_diag.len() + 1 >= n);
let mut w = alloc_work::<T>(n);
for i in 0..n {
for j in 0..n {
w[i * n + j] = *a.get(i, j);
}
}
for i in 0..n {
for j in 0..n {
*q.get_mut(i, j) = if i == j { T::one() } else { T::zero() };
}
}
for k in 0..n.saturating_sub(2) {
let mut norm_sq = <T::Real as Zero>::zero();
for i in (k + 1)..n {
let v = w[i * n + k];
norm_sq = norm_sq + (v * v.conj()).re();
}
if norm_sq <= T::lepsilon() * T::lepsilon() {
off_diag[k] = <T::Real as Zero>::zero();
continue;
}
let norm = norm_sq.lsqrt();
let wk1k = w[(k + 1) * n + k];
let alpha = wk1k.modulus();
let sigma = if alpha < T::lepsilon() {
T::from_real(norm)
} else {
T::from_real(norm) * (wk1k / T::from_real(alpha))
};
let v0 = wk1k + sigma;
let v0_sq = (v0 * v0.conj()).re();
let mut v_norm_sq = v0_sq;
for i in (k + 2)..n {
let vi = w[i * n + k];
v_norm_sq = v_norm_sq + (vi * vi.conj()).re();
}
let two = <T::Real as One>::one() + <T::Real as One>::one();
let tau_real = two / v_norm_sq;
let tau = T::from_real(tau_real);
let sub_n = n - k - 1;
let mut p = alloc_work_vec::<T>(sub_n);
for i in 0..sub_n {
let row = k + 1 + i;
let mut dot = T::zero();
for jj in 0..sub_n {
let col = k + 1 + jj;
let vj = if jj == 0 { v0 } else { w[(k + 1 + jj) * n + k] };
dot = dot + w[row * n + col] * vj;
}
p[i] = tau * dot;
}
let mut vhp = T::zero();
vhp = vhp + v0.conj() * p[0];
for i in 1..sub_n {
let vi = w[(k + 1 + i) * n + k];
vhp = vhp + vi.conj() * p[i];
}
let half_tau_vhp = T::from_real(tau_real / two) * vhp;
let mut q_vec = alloc_work_vec::<T>(sub_n);
q_vec[0] = p[0] - half_tau_vhp * v0;
for i in 1..sub_n {
let vi = w[(k + 1 + i) * n + k];
q_vec[i] = p[i] - half_tau_vhp * vi;
}
for i in 0..sub_n {
let vi = if i == 0 { v0 } else { w[(k + 1 + i) * n + k] };
for j in 0..sub_n {
let row = k + 1 + i;
let col = k + 1 + j;
let vj = if j == 0 { v0 } else { w[(k + 1 + j) * n + k] };
w[row * n + col] =
w[row * n + col] - vi * q_vec[j].conj() - q_vec[i] * vj.conj();
}
}
let neg_sigma_re = (T::zero() - sigma).re();
off_diag[k] = neg_sigma_re;
let mut s_vec = alloc_work_vec::<T>(n);
crate::simd::scale_slices_dispatch(
q.col_as_slice(k + 1, 0),
v0,
&mut s_vec[..n],
);
for m in 1..sub_n {
let neg_vm = T::zero() - w[(k + 1 + m) * n + k];
crate::simd::axpy_neg_dispatch(
&mut s_vec[..n],
neg_vm,
q.col_as_slice(k + 1 + m, 0),
);
}
for r in 0..n {
s_vec[r] = tau * s_vec[r];
}
{
let alpha = v0.conj();
let col_slice = q.col_as_mut_slice(k + 1, 0);
crate::simd::axpy_neg_dispatch(col_slice, alpha, &s_vec[..n]);
}
for j in 1..sub_n {
let vj_conj = w[(k + 1 + j) * n + k].conj();
let col_slice = q.col_as_mut_slice(k + 1 + j, 0);
crate::simd::axpy_neg_dispatch(col_slice, vj_conj, &s_vec[..n]);
}
}
for i in 0..n {
diag[i] = w[i * n + i].re();
}
if n >= 2 {
off_diag[n - 2] = w[(n - 1) * n + (n - 2)].re();
}
}
pub fn tridiagonal_qr_with_vecs<T: LinalgScalar>(
diag: &mut [T::Real],
off_diag: &mut [T::Real],
q: &mut impl MatrixMut<T>,
max_iter: usize,
) -> Result<(), LinalgError> {
let n = diag.len();
if n <= 1 {
return Ok(());
}
let eps = T::lepsilon();
let mut iter = 0usize;
let mut hi = n - 1;
while hi > 0 {
let mut lo = hi;
while lo > 0 {
let threshold = eps * (diag[lo - 1].abs() + diag[lo].abs());
if off_diag[lo - 1].abs() <= threshold {
off_diag[lo - 1] = <T::Real as Zero>::zero();
break;
}
lo -= 1;
}
if lo == hi {
hi -= 1;
continue;
}
iter += 1;
if iter > max_iter {
return Err(LinalgError::ConvergenceFailure);
}
let two = <T::Real as One>::one() + <T::Real as One>::one();
let d = (diag[hi - 1] - diag[hi]) / two;
let e = off_diag[hi - 1];
let r = d.hypot(e);
let shift = diag[hi]
- e * e
/ (d + if d >= <T::Real as Zero>::zero() {
r
} else {
<T::Real as Zero>::zero() - r
});
let mut x = diag[lo] - shift;
let mut z = off_diag[lo];
for k in lo..hi {
let (c, s, _r) = givens(x, z);
if k > lo {
off_diag[k - 1] = c * x + s * z;
}
let d_k = diag[k];
let d_k1 = diag[k + 1];
let e_k = off_diag[k];
diag[k] = c * c * d_k + two * c * s * e_k + s * s * d_k1;
diag[k + 1] = s * s * d_k - two * c * s * e_k + c * c * d_k1;
off_diag[k] = c * s * (d_k1 - d_k) + (c * c - s * s) * e_k;
if k + 1 < hi {
let e_next = off_diag[k + 1];
x = off_diag[k];
z = s * e_next;
off_diag[k + 1] = c * e_next;
}
let n_rows = q.nrows();
for i in 0..n_rows {
let qik = *q.get(i, k);
let qik1 = *q.get(i, k + 1);
*q.get_mut(i, k) = T::from_real(c) * qik + T::from_real(s) * qik1;
*q.get_mut(i, k + 1) = T::from_real(c) * qik1 - T::from_real(s) * qik;
}
}
}
sort_eigen_with_vecs::<T>(diag, q);
Ok(())
}
pub fn tridiagonal_qr_no_vecs<T: LinalgScalar>(
diag: &mut [T::Real],
off_diag: &mut [T::Real],
max_iter: usize,
) -> Result<(), LinalgError> {
let n = diag.len();
if n <= 1 {
return Ok(());
}
let eps = T::lepsilon();
let mut iter = 0usize;
let mut hi = n - 1;
while hi > 0 {
let mut lo = hi;
while lo > 0 {
let threshold = eps * (diag[lo - 1].abs() + diag[lo].abs());
if off_diag[lo - 1].abs() <= threshold {
off_diag[lo - 1] = <T::Real as Zero>::zero();
break;
}
lo -= 1;
}
if lo == hi {
hi -= 1;
continue;
}
iter += 1;
if iter > max_iter {
return Err(LinalgError::ConvergenceFailure);
}
let two = <T::Real as One>::one() + <T::Real as One>::one();
let d = (diag[hi - 1] - diag[hi]) / two;
let e = off_diag[hi - 1];
let r = d.hypot(e);
let shift = diag[hi]
- e * e
/ (d + if d >= <T::Real as Zero>::zero() {
r
} else {
<T::Real as Zero>::zero() - r
});
let mut x = diag[lo] - shift;
let mut z = off_diag[lo];
for k in lo..hi {
let (c, s, _r) = givens(x, z);
if k > lo {
off_diag[k - 1] = c * x + s * z;
}
let d_k = diag[k];
let d_k1 = diag[k + 1];
let e_k = off_diag[k];
diag[k] = c * c * d_k + two * c * s * e_k + s * s * d_k1;
diag[k + 1] = s * s * d_k - two * c * s * e_k + c * c * d_k1;
off_diag[k] = c * s * (d_k1 - d_k) + (c * c - s * s) * e_k;
if k + 1 < hi {
let e_next = off_diag[k + 1];
x = off_diag[k];
z = s * e_next;
off_diag[k + 1] = c * e_next;
}
}
}
sort_eigenvalues(diag);
Ok(())
}
#[inline]
pub(crate) fn givens<R: Float + Zero>(a: R, b: R) -> (R, R, R) {
if b == R::zero() {
(R::one(), R::zero(), a)
} else if a == R::zero() {
let s = if b >= R::zero() { R::one() } else { R::zero() - R::one() };
(R::zero(), s, b.abs())
} else {
let r = a.hypot(b);
(a / r, b / r, r)
}
}
fn sort_eigen_with_vecs<T: LinalgScalar>(
diag: &mut [T::Real],
q: &mut impl MatrixMut<T>,
) {
let n = diag.len();
for i in 0..n {
let mut min_idx = i;
for j in (i + 1)..n {
if diag[j] < diag[min_idx] {
min_idx = j;
}
}
if min_idx != i {
diag.swap(i, min_idx);
let n_rows = q.nrows();
for row in 0..n_rows {
let tmp = *q.get(row, i);
*q.get_mut(row, i) = *q.get(row, min_idx);
*q.get_mut(row, min_idx) = tmp;
}
}
}
}
fn sort_eigenvalues<R: Float>(diag: &mut [R]) {
let n = diag.len();
for i in 0..n {
let mut min_idx = i;
for j in (i + 1)..n {
if diag[j] < diag[min_idx] {
min_idx = j;
}
}
if min_idx != i {
diag.swap(i, min_idx);
}
}
}
#[inline]
fn cross3<R: Float>(a: [R; 3], b: [R; 3]) -> [R; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
fn norm_sq3<R: Float>(v: [R; 3]) -> R {
v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
}
#[cfg(feature = "alloc")]
fn alloc_work<T: LinalgScalar>(n: usize) -> alloc::vec::Vec<T> {
alloc::vec![T::zero(); n * n]
}
#[cfg(feature = "alloc")]
fn alloc_work_vec<T: LinalgScalar>(n: usize) -> alloc::vec::Vec<T> {
alloc::vec![T::zero(); n]
}
#[cfg(not(feature = "alloc"))]
fn alloc_work<T: LinalgScalar>(n: usize) -> [T; 36] {
assert!(n <= 6, "tridiagonalize without alloc supports at most 6×6");
[T::zero(); 36]
}
#[cfg(not(feature = "alloc"))]
fn alloc_work_vec<T: LinalgScalar>(n: usize) -> [T; 6] {
assert!(n <= 6, "tridiagonalize without alloc supports at most 6×6");
[T::zero(); 6]
}
#[derive(Debug, Clone)]
pub struct SymmetricEigen<T: LinalgScalar, const N: usize> {
eigenvalues: [T::Real; N],
eigenvectors: Matrix<T, N, N>,
}
impl<T: LinalgScalar, const N: usize> SymmetricEigen<T, N> {
pub fn new(a: &Matrix<T, N, N>) -> Result<Self, LinalgError> {
let mut diag = [<T::Real as Zero>::zero(); N];
let mut off_diag = [<T::Real as Zero>::zero(); N];
let mut q = Matrix::<T, N, N>::zeros();
if N == 0 {
return Ok(Self {
eigenvalues: diag,
eigenvectors: q,
});
}
if N == 1 {
diag[0] = a[(0, 0)].re();
q[(0, 0)] = T::one();
return Ok(Self {
eigenvalues: diag,
eigenvectors: q,
});
}
if N == 2 && core::any::TypeId::of::<T>() == core::any::TypeId::of::<T::Real>() {
let aa = a[(0, 0)].re();
let bb = a[(0, 1)].re(); let dd = a[(1, 1)].re();
let half = <T::Real as One>::one()
/ (<T::Real as One>::one() + <T::Real as One>::one());
let p = (aa + dd) * half;
let diff_half = (aa - dd) * half;
let disc = diff_half.hypot(bb);
diag[0] = p - disc;
diag[1] = p + disc;
let two = <T::Real as One>::one() + <T::Real as One>::one();
let theta = (two * bb).atan2(aa - dd) * half;
let c = theta.cos();
let s = theta.sin();
let mu0 = c * c * aa + two * c * s * bb + s * s * dd;
if (mu0 - diag[0]).abs() <= (mu0 - diag[1]).abs() {
q[(0, 0)] = T::from_real(c);
q[(1, 0)] = T::from_real(s);
q[(0, 1)] = T::from_real(<T::Real as Zero>::zero() - s);
q[(1, 1)] = T::from_real(c);
} else {
q[(0, 0)] = T::from_real(<T::Real as Zero>::zero() - s);
q[(1, 0)] = T::from_real(c);
q[(0, 1)] = T::from_real(c);
q[(1, 1)] = T::from_real(s);
}
return Ok(Self {
eigenvalues: diag,
eigenvectors: q,
});
}
if N == 3 && core::any::TypeId::of::<T>() == core::any::TypeId::of::<T::Real>() {
let one = <T::Real as One>::one();
let two = one + one;
let three = two + one;
let six = three + three;
let a00 = a[(0, 0)].re();
let a01 = a[(0, 1)].re();
let a02 = a[(0, 2)].re();
let a11 = a[(1, 1)].re();
let a12 = a[(1, 2)].re();
let a22 = a[(2, 2)].re();
let trace = a00 + a11 + a22;
let qq = trace / three;
let b00 = a00 - qq;
let b11 = a11 - qq;
let b22 = a22 - qq;
let frob_sq = b00 * b00 + b11 * b11 + b22 * b22
+ two * (a01 * a01 + a02 * a02 + a12 * a12);
let p = (frob_sq / six).sqrt();
if p <= T::lepsilon() * trace.abs() + T::lepsilon() {
diag[0] = qq;
diag[1] = qq;
diag[2] = qq;
for i in 0..3 {
q[(i, i)] = T::one();
}
return Ok(Self {
eigenvalues: diag,
eigenvectors: q,
});
}
let det_b = b00 * (b11 * b22 - a12 * a12)
- a01 * (a01 * b22 - a12 * a02)
+ a02 * (a01 * a12 - b11 * a02);
let r = det_b / (two * p * p * p);
let r_clamped = if r > one {
one
} else if r < <T::Real as Zero>::zero() - one {
<T::Real as Zero>::zero() - one
} else {
r
};
let phi = r_clamped.acos() / three;
let pi = <T::Real as Float>::acos(<T::Real as Zero>::zero() - one);
let two_pi_over_3 = two * pi / three;
let eig2 = qq + two * p * phi.cos();
let eig0 = qq + two * p * (phi + two_pi_over_3).cos();
let eig1 = trace - eig0 - eig2;
let (mut e0, mut e1, mut e2) = (eig0, eig1, eig2);
if e0 > e1 {
core::mem::swap(&mut e0, &mut e1);
}
if e1 > e2 {
core::mem::swap(&mut e1, &mut e2);
}
if e0 > e1 {
core::mem::swap(&mut e0, &mut e1);
}
diag[0] = e0;
diag[1] = e1;
diag[2] = e2;
for col in 0..3 {
let lambda = diag[col];
let r0 = [a00 - lambda, a01, a02];
let r1 = [a01, a11 - lambda, a12];
let r2 = [a02, a12, a22 - lambda];
let c01 = cross3(r0, r1);
let c02 = cross3(r0, r2);
let c12 = cross3(r1, r2);
let n01 = norm_sq3(c01);
let n02 = norm_sq3(c02);
let n12 = norm_sq3(c12);
let (vx, vy, vz, nn) = if n01 >= n02 && n01 >= n12 {
(c01[0], c01[1], c01[2], n01)
} else if n02 >= n12 {
(c02[0], c02[1], c02[2], n02)
} else {
(c12[0], c12[1], c12[2], n12)
};
if nn <= T::lepsilon() * T::lepsilon() {
q[(col, col)] = T::one();
} else {
let inv_n = one / nn.sqrt();
q[(0, col)] = T::from_real(vx * inv_n);
q[(1, col)] = T::from_real(vy * inv_n);
q[(2, col)] = T::from_real(vz * inv_n);
}
}
{
let dot01 = q[(0, 0)].re() * q[(0, 1)].re()
+ q[(1, 0)].re() * q[(1, 1)].re()
+ q[(2, 0)].re() * q[(2, 1)].re();
for i in 0..3 {
let v = q[(i, 1)].re() - dot01 * q[(i, 0)].re();
q[(i, 1)] = T::from_real(v);
}
let n1 = (q[(0, 1)].re() * q[(0, 1)].re()
+ q[(1, 1)].re() * q[(1, 1)].re()
+ q[(2, 1)].re() * q[(2, 1)].re())
.sqrt();
if n1 > T::lepsilon() {
for i in 0..3 {
q[(i, 1)] = T::from_real(q[(i, 1)].re() / n1);
}
}
}
{
let v0 = [q[(0, 0)].re(), q[(1, 0)].re(), q[(2, 0)].re()];
let v1 = [q[(0, 1)].re(), q[(1, 1)].re(), q[(2, 1)].re()];
let c = cross3(v0, v1);
q[(0, 2)] = T::from_real(c[0]);
q[(1, 2)] = T::from_real(c[1]);
q[(2, 2)] = T::from_real(c[2]);
}
return Ok(Self {
eigenvalues: diag,
eigenvectors: q,
});
}
tridiagonalize(a, &mut diag, &mut off_diag, &mut q);
tridiagonal_qr_with_vecs::<T>(
&mut diag,
&mut off_diag[..N.saturating_sub(1)],
&mut q,
30 * N,
)?;
Ok(Self {
eigenvalues: diag,
eigenvectors: q,
})
}
pub fn eigenvalues_only(a: &Matrix<T, N, N>) -> Result<[T::Real; N], LinalgError> {
let mut diag = [<T::Real as Zero>::zero(); N];
if N == 0 {
return Ok(diag);
}
if N == 1 {
diag[0] = a[(0, 0)].re();
return Ok(diag);
}
if N == 2 && core::any::TypeId::of::<T>() == core::any::TypeId::of::<T::Real>() {
let aa = a[(0, 0)].re();
let bb = a[(0, 1)].re();
let dd = a[(1, 1)].re();
let half = <T::Real as One>::one()
/ (<T::Real as One>::one() + <T::Real as One>::one());
let p = (aa + dd) * half;
let diff_half = (aa - dd) * half;
let disc = diff_half.hypot(bb);
diag[0] = p - disc;
diag[1] = p + disc;
return Ok(diag);
}
if N == 3 && core::any::TypeId::of::<T>() == core::any::TypeId::of::<T::Real>() {
let one = <T::Real as One>::one();
let two = one + one;
let three = two + one;
let six = three + three;
let a00 = a[(0, 0)].re();
let a01 = a[(0, 1)].re();
let a02 = a[(0, 2)].re();
let a11 = a[(1, 1)].re();
let a12 = a[(1, 2)].re();
let a22 = a[(2, 2)].re();
let trace = a00 + a11 + a22;
let qq = trace / three;
let b00 = a00 - qq;
let b11 = a11 - qq;
let b22 = a22 - qq;
let frob_sq = b00 * b00 + b11 * b11 + b22 * b22
+ two * (a01 * a01 + a02 * a02 + a12 * a12);
let p = (frob_sq / six).sqrt();
if p <= T::lepsilon() * trace.abs() + T::lepsilon() {
diag[0] = qq;
diag[1] = qq;
diag[2] = qq;
return Ok(diag);
}
let det_b = b00 * (b11 * b22 - a12 * a12)
- a01 * (a01 * b22 - a12 * a02)
+ a02 * (a01 * a12 - b11 * a02);
let r = det_b / (two * p * p * p);
let r_clamped = if r > one {
one
} else if r < <T::Real as Zero>::zero() - one {
<T::Real as Zero>::zero() - one
} else {
r
};
let phi = r_clamped.acos() / three;
let pi = <T::Real as Float>::acos(<T::Real as Zero>::zero() - one);
let two_pi_over_3 = two * pi / three;
let eig2 = qq + two * p * phi.cos();
let eig0 = qq + two * p * (phi + two_pi_over_3).cos();
let eig1 = trace - eig0 - eig2;
let (mut e0, mut e1, mut e2) = (eig0, eig1, eig2);
if e0 > e1 {
core::mem::swap(&mut e0, &mut e1);
}
if e1 > e2 {
core::mem::swap(&mut e1, &mut e2);
}
if e0 > e1 {
core::mem::swap(&mut e0, &mut e1);
}
diag[0] = e0;
diag[1] = e1;
diag[2] = e2;
return Ok(diag);
}
let mut off_diag = [<T::Real as Zero>::zero(); N];
let mut q = Matrix::<T, N, N>::zeros();
tridiagonalize(a, &mut diag, &mut off_diag, &mut q);
tridiagonal_qr_no_vecs::<T>(&mut diag, &mut off_diag[..N.saturating_sub(1)], 30 * N)?;
Ok(diag)
}
#[inline]
pub fn eigenvalues(&self) -> &[T::Real; N] {
&self.eigenvalues
}
#[inline]
pub fn eigenvectors(&self) -> &Matrix<T, N, N> {
&self.eigenvectors
}
}
impl<T: LinalgScalar, const N: usize> Matrix<T, N, N> {
pub fn eig_symmetric(&self) -> Result<SymmetricEigen<T, N>, LinalgError> {
SymmetricEigen::new(self)
}
pub fn eigenvalues_symmetric(&self) -> Result<[T::Real; N], LinalgError> {
SymmetricEigen::eigenvalues_only(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-10;
fn assert_near(a: f64, b: f64, tol: f64, msg: &str) {
assert!(
(a - b).abs() < tol,
"{}: {} vs {} (diff {})",
msg,
a,
b,
(a - b).abs()
);
}
#[test]
fn identity_eigenvalues() {
let id: Matrix<f64, 3, 3> = Matrix::eye();
let eig = id.eig_symmetric().unwrap();
for i in 0..3 {
assert_near(eig.eigenvalues()[i], 1.0, TOL, &format!("λ[{}]", i));
}
let q = eig.eigenvectors();
let qtq = q.transpose() * *q;
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert_near(qtq[(i, j)], expected, TOL, &format!("QtQ[({},{})]", i, j));
}
}
}
#[test]
fn diagonal_matrix() {
let a = Matrix::new([
[3.0_f64, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 2.0],
]);
let eig = a.eig_symmetric().unwrap();
assert_near(eig.eigenvalues()[0], 1.0, TOL, "λ[0]");
assert_near(eig.eigenvalues()[1], 2.0, TOL, "λ[1]");
assert_near(eig.eigenvalues()[2], 3.0, TOL, "λ[2]");
}
#[test]
fn known_2x2() {
let a = Matrix::new([[2.0_f64, -1.0], [-1.0, 2.0]]);
let eig = a.eig_symmetric().unwrap();
assert_near(eig.eigenvalues()[0], 1.0, TOL, "λ[0]");
assert_near(eig.eigenvalues()[1], 3.0, TOL, "λ[1]");
}
#[test]
fn known_3x3_eigenvectors() {
let a = Matrix::new([
[2.0_f64, 1.0, 0.0],
[1.0, 3.0, 1.0],
[0.0, 1.0, 2.0],
]);
let eig = a.eig_symmetric().unwrap();
let q = eig.eigenvectors();
for col in 0..3 {
let lambda = eig.eigenvalues()[col];
for row in 0..3 {
let mut av = 0.0;
for k in 0..3 {
av += a[(row, k)] * q[(k, col)];
}
assert_near(
av,
lambda * q[(row, col)],
TOL,
&format!("Av=λv [({},{})]", row, col),
);
}
}
}
#[test]
fn reconstruction() {
let a = Matrix::new([
[4.0_f64, 1.0, -1.0],
[1.0, 3.0, 2.0],
[-1.0, 2.0, 5.0],
]);
let eig = a.eig_symmetric().unwrap();
let q = eig.eigenvectors();
let vals = eig.eigenvalues();
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..3 {
sum += q[(i, k)] * vals[k] * q[(j, k)];
}
assert_near(sum, a[(i, j)], TOL, &format!("A[({},{})]", i, j));
}
}
}
#[test]
fn orthogonality() {
let a = Matrix::new([
[4.0_f64, 1.0, -1.0],
[1.0, 3.0, 2.0],
[-1.0, 2.0, 5.0],
]);
let eig = a.eig_symmetric().unwrap();
let q = eig.eigenvectors();
let qtq = q.transpose() * *q;
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert_near(qtq[(i, j)], expected, TOL, &format!("QtQ[({},{})]", i, j));
}
}
}
#[test]
fn sorted_ascending() {
let a = Matrix::new([
[10.0_f64, 3.0, 0.0, 0.0],
[3.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 7.0, 2.0],
[0.0, 0.0, 2.0, 4.0],
]);
let eig = a.eig_symmetric().unwrap();
let vals = eig.eigenvalues();
for i in 0..3 {
assert!(
vals[i] <= vals[i + 1] + TOL,
"not ascending: λ[{}]={} > λ[{}]={}",
i,
vals[i],
i + 1,
vals[i + 1]
);
}
}
#[test]
fn repeated_eigenvalues() {
let a = Matrix::new([
[2.0_f64, 0.0, 0.0],
[0.0, 2.0, 0.0],
[0.0, 0.0, 2.0],
]);
let eig = a.eig_symmetric().unwrap();
for i in 0..3 {
assert_near(eig.eigenvalues()[i], 2.0, TOL, &format!("λ[{}]", i));
}
}
#[test]
fn negative_eigenvalues() {
let a = Matrix::new([[1.0_f64, 3.0], [3.0, 1.0]]);
let eig = a.eig_symmetric().unwrap();
assert_near(eig.eigenvalues()[0], -2.0, TOL, "λ[0]");
assert_near(eig.eigenvalues()[1], 4.0, TOL, "λ[1]");
}
#[test]
fn eigenvalues_only() {
let a = Matrix::new([[5.0_f64, 2.0], [2.0, 2.0]]);
let vals = a.eigenvalues_symmetric().unwrap();
assert_near(vals[0], 1.0, TOL, "λ[0]");
assert_near(vals[1], 6.0, TOL, "λ[1]");
}
#[test]
fn f32_support() {
let a = Matrix::new([[2.0_f32, -1.0], [-1.0, 2.0]]);
let eig = a.eig_symmetric().unwrap();
assert!((eig.eigenvalues()[0] - 1.0).abs() < 1e-5);
assert!((eig.eigenvalues()[1] - 3.0).abs() < 1e-5);
}
#[test]
fn size_1x1() {
let a = Matrix::new([[7.0_f64]]);
let eig = a.eig_symmetric().unwrap();
assert_near(eig.eigenvalues()[0], 7.0, TOL, "λ[0]");
}
#[test]
fn size_5x5() {
let a = Matrix::new([
[5.0_f64, 1.0, 0.5, 0.25, 0.125],
[1.0, 4.0, 1.0, 0.5, 0.25],
[0.5, 1.0, 3.0, 1.0, 0.5],
[0.25, 0.5, 1.0, 2.0, 1.0],
[0.125, 0.25, 0.5, 1.0, 1.0],
]);
let eig = a.eig_symmetric().unwrap();
let q = eig.eigenvectors();
for col in 0..5 {
let lambda = eig.eigenvalues()[col];
for row in 0..5 {
let mut av = 0.0;
for k in 0..5 {
av += a[(row, k)] * q[(k, col)];
}
assert_near(
av,
lambda * q[(row, col)],
1e-9,
&format!("Av=λv [({},{})]", row, col),
);
}
}
let qtq = q.transpose() * *q;
for i in 0..5 {
for j in 0..5 {
let expected = if i == j { 1.0 } else { 0.0 };
assert_near(qtq[(i, j)], expected, 1e-9, &format!("QtQ[({},{})]", i, j));
}
}
let trace = a[(0, 0)] + a[(1, 1)] + a[(2, 2)] + a[(3, 3)] + a[(4, 4)];
let eig_sum: f64 = eig.eigenvalues().iter().sum();
assert_near(eig_sum, trace, TOL, "trace");
}
#[cfg(feature = "complex")]
mod complex_tests {
use super::*;
use num_complex::Complex;
#[test]
fn hermitian_real_eigenvalues() {
let a = Matrix::new([
[Complex::new(3.0_f64, 0.0), Complex::new(1.0, -1.0)],
[Complex::new(1.0, 1.0), Complex::new(2.0, 0.0)],
]);
let eig = a.eig_symmetric().unwrap();
let expected_0 = (5.0 - 5.0_f64.sqrt()) / 2.0;
let expected_1 = (5.0 + 5.0_f64.sqrt()) / 2.0;
assert_near(eig.eigenvalues()[0], expected_0, TOL, "λ[0]");
assert_near(eig.eigenvalues()[1], expected_1, TOL, "λ[1]");
let q = eig.eigenvectors();
for i in 0..2 {
for j in 0..2 {
let mut dot = Complex::new(0.0, 0.0);
for k in 0..2 {
dot = dot + q[(k, i)].conj() * q[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert_near(dot.re, expected, TOL, &format!("QHQ[({},{})] re", i, j));
assert_near(dot.im, 0.0, TOL, &format!("QHQ[({},{})] im", i, j));
}
}
}
}
}