use crate::Float;
use crate::error::{CoreError, Result};
use crate::tensor::Tensor;
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Debug, Clone)]
pub struct EigDecomposition<T: Float> {
eigenvalues: Vec<T>,
eigenvectors: Vec<T>,
n: usize,
}
const MAX_SWEEPS: usize = 100;
#[allow(clippy::many_single_char_names)]
impl<T: Float> EigDecomposition<T> {
pub fn decompose_symmetric(a: &Tensor<T>) -> Result<Self> {
if a.ndim() != 2 {
return Err(CoreError::InvalidArgument {
reason: "Eigendecomposition requires a 2-D tensor (matrix)",
});
}
let n = a.shape()[0];
if a.shape()[1] != n {
return Err(CoreError::InvalidArgument {
reason: "Eigendecomposition requires a square matrix",
});
}
let mut s: Vec<T> = a.as_slice().to_vec();
let mut v = vec![T::zero(); n * n];
for i in 0..n {
v[i * n + i] = T::one();
}
let tol = T::epsilon() * T::from_f64(100.0);
for _sweep in 0..MAX_SWEEPS {
let mut off_norm = T::zero();
for i in 0..n {
for j in (i + 1)..n {
off_norm += s[i * n + j] * s[i * n + j];
}
}
if off_norm.sqrt() < tol {
break;
}
for p in 0..n {
for q in (p + 1)..n {
let apq = s[p * n + q];
if apq.abs() < tol {
continue;
}
let app = s[p * n + p];
let aqq = s[q * n + q];
let theta = (aqq - app) / (apq + apq);
let t = if theta >= T::zero() {
T::one() / (theta + (T::one() + theta * theta).sqrt())
} else {
T::zero()
- T::one() / (T::zero() - theta + (T::one() + theta * theta).sqrt())
};
let cs = T::one() / (T::one() + t * t).sqrt();
let sn = t * cs;
s[p * n + p] = app - t * apq;
s[q * n + q] = aqq + t * apq;
s[p * n + q] = T::zero();
s[q * n + p] = T::zero();
for r in 0..n {
if r == p || r == q {
continue;
}
let srp = s[r * n + p];
let srq = s[r * n + q];
s[r * n + p] = cs * srp - sn * srq;
s[p * n + r] = cs * srp - sn * srq;
s[r * n + q] = sn * srp + cs * srq;
s[q * n + r] = sn * srp + cs * srq;
}
for i in 0..n {
let vp = v[i * n + p];
let vq = v[i * n + q];
v[i * n + p] = cs * vp - sn * vq;
v[i * n + q] = sn * vp + cs * vq;
}
}
}
}
let eigenvalues: Vec<T> = (0..n).map(|i| s[i * n + i]).collect();
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| {
eigenvalues[b]
.abs()
.partial_cmp(&eigenvalues[a].abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
let eigenvalues_sorted: Vec<T> = indices.iter().map(|&i| eigenvalues[i]).collect();
let mut v_sorted = vec![T::zero(); n * n];
for (new_j, &old_j) in indices.iter().enumerate() {
for i in 0..n {
v_sorted[i * n + new_j] = v[i * n + old_j];
}
}
Ok(Self {
eigenvalues: eigenvalues_sorted,
eigenvectors: v_sorted,
n,
})
}
pub fn eigenvalues(&self) -> &[T] {
&self.eigenvalues
}
pub fn eigenvalues_tensor(&self) -> Tensor<T> {
Tensor::from_vec(self.eigenvalues.clone(), vec![self.n])
.expect("eigenvalues length equals n by construction")
}
pub fn eigenvectors(&self) -> Tensor<T> {
Tensor::from_vec(self.eigenvectors.clone(), vec![self.n, self.n])
.expect("eigenvectors length equals n*n by construction")
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
fn approx_eq(a: &[f64], b: &[f64], tol: f64) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(&x, &y)| (x - y).abs() < tol)
}
#[test]
fn test_eig_diagonal() {
let a = Tensor::from_vec(vec![3.0_f64, 0.0, 0.0, 5.0], vec![2, 2]).unwrap();
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let vals = eig.eigenvalues();
assert!((vals[0] - 5.0).abs() < 1e-10);
assert!((vals[1] - 3.0).abs() < 1e-10);
}
#[test]
fn test_eig_identity() {
let eye = Tensor::<f64>::eye(3);
let eig = EigDecomposition::decompose_symmetric(&eye).unwrap();
for &v in eig.eigenvalues() {
assert!((v - 1.0).abs() < 1e-10);
}
}
#[test]
fn test_eig_2x2() {
let a = Tensor::from_vec(vec![2.0_f64, 1.0, 1.0, 3.0], vec![2, 2]).unwrap();
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let vals = eig.eigenvalues();
let sqrt5 = 5.0_f64.sqrt();
let expected_1 = 2.5 + sqrt5 * 0.5;
let expected_2 = 2.5 - sqrt5 * 0.5;
assert!((vals[0] - expected_1).abs() < 1e-10);
assert!((vals[1] - expected_2).abs() < 1e-10);
}
#[test]
fn test_eig_reconstruction() {
let a = Tensor::from_vec(
vec![4.0_f64, 2.0, 1.0, 2.0, 5.0, 3.0, 1.0, 3.0, 6.0],
vec![3, 3],
)
.unwrap();
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let v = eig.eigenvectors();
let vt = v.transpose().unwrap();
let d_vals = eig.eigenvalues();
let n = 3;
let mut d_data = vec![0.0_f64; n * n];
for i in 0..n {
d_data[i * n + i] = d_vals[i];
}
let d = Tensor::from_vec(d_data, vec![n, n]).unwrap();
let vd = v.matmul(&d).unwrap();
let reconstructed = vd.matmul(&vt).unwrap();
assert!(approx_eq(reconstructed.as_slice(), a.as_slice(), 1e-8));
}
#[test]
fn test_eig_orthogonal_eigenvectors() {
let a = Tensor::from_vec(
vec![4.0_f64, 2.0, 1.0, 2.0, 5.0, 3.0, 1.0, 3.0, 6.0],
vec![3, 3],
)
.unwrap();
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let v = eig.eigenvectors();
let vt = v.transpose().unwrap();
let vtv = vt.matmul(&v).unwrap();
let eye = Tensor::<f64>::eye(3);
assert!(approx_eq(vtv.as_slice(), eye.as_slice(), 1e-8));
}
#[test]
fn test_eig_negative_eigenvalue() {
let a = Tensor::from_vec(vec![1.0_f64, 2.0, 2.0, 1.0], vec![2, 2]).unwrap();
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let vals = eig.eigenvalues();
assert!((vals[0] - 3.0).abs() < 1e-10);
assert!((vals[1] - (-1.0)).abs() < 1e-10);
}
#[test]
fn test_eig_4x4() {
let a = Tensor::from_vec(
vec![
5.0_f64, 1.0, 2.0, 0.0, 1.0, 4.0, 1.0, 1.0, 2.0, 1.0, 3.0, 0.0, 0.0, 1.0, 0.0, 2.0,
],
vec![4, 4],
)
.unwrap();
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let v = eig.eigenvectors();
let vt = v.transpose().unwrap();
let d_vals = eig.eigenvalues();
let mut d_data = vec![0.0_f64; 16];
for i in 0..4 {
d_data[i * 4 + i] = d_vals[i];
}
let d = Tensor::from_vec(d_data, vec![4, 4]).unwrap();
let vd = v.matmul(&d).unwrap();
let reconstructed = vd.matmul(&vt).unwrap();
assert!(approx_eq(reconstructed.as_slice(), a.as_slice(), 1e-8));
}
#[test]
fn test_eig_not_square() {
let a = Tensor::from_vec(vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]).unwrap();
assert!(EigDecomposition::decompose_symmetric(&a).is_err());
}
#[test]
fn test_eig_not_2d() {
let a = Tensor::from_vec(vec![1.0_f64, 2.0, 3.0], vec![3]).unwrap();
assert!(EigDecomposition::decompose_symmetric(&a).is_err());
}
}