use crate::algorithms::community::lanczos;
use crate::core::error::{IgraphError, IgraphResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EigenWhich {
LargestAlgebraic,
SmallestAlgebraic,
LargestMagnitude,
}
#[derive(Debug, Clone)]
pub struct EigenDecomposition {
pub eigenvalues: Vec<f64>,
pub eigenvectors: Vec<Vec<f64>>,
}
pub fn eigen_matrix_symmetric<F>(
n: usize,
matvec: F,
nev: usize,
which: EigenWhich,
) -> IgraphResult<EigenDecomposition>
where
F: Fn(&[f64], &mut [f64]),
{
if n == 0 {
return Err(IgraphError::InvalidArgument(
"eigen_matrix_symmetric: matrix dimension must be > 0".into(),
));
}
let nev = nev.min(n);
if nev == 0 {
return Ok(EigenDecomposition {
eigenvalues: Vec::new(),
eigenvectors: Vec::new(),
});
}
let internal_which = match which {
EigenWhich::LargestAlgebraic => lanczos::EigenWhich::LargestAlgebraic,
EigenWhich::SmallestAlgebraic => lanczos::EigenWhich::SmallestAlgebraic,
EigenWhich::LargestMagnitude => lanczos::EigenWhich::LargestMagnitude,
};
let max_iter = n.saturating_mul(200).max(2000);
let result = lanczos::lanczos_top_k(n, &matvec, nev, internal_which, max_iter);
Ok(EigenDecomposition {
eigenvalues: result.eigenvalues,
eigenvectors: result.eigenvectors,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_eigenvalues() {
let result = eigen_matrix_symmetric(
4,
|x, y| {
y[..4].copy_from_slice(&x[..4]);
},
1,
EigenWhich::LargestAlgebraic,
)
.unwrap();
assert_eq!(result.eigenvalues.len(), 1);
assert!(
(result.eigenvalues[0] - 1.0).abs() < 1e-6,
"identity eigenvalue should be 1.0, got {}",
result.eigenvalues[0]
);
}
#[test]
fn diagonal_matrix_top2() {
let diag = [4.0, 3.0, 2.0, 1.0];
let result = eigen_matrix_symmetric(
4,
|x, y| {
for i in 0..4 {
y[i] = diag[i] * x[i];
}
},
2,
EigenWhich::LargestAlgebraic,
)
.unwrap();
assert_eq!(result.eigenvalues.len(), 2);
assert!(
(result.eigenvalues[0] - 4.0).abs() < 1e-6,
"top eigenvalue should be 4.0, got {}",
result.eigenvalues[0]
);
assert!(
(result.eigenvalues[1] - 3.0).abs() < 1e-6,
"second eigenvalue should be 3.0, got {}",
result.eigenvalues[1]
);
}
#[test]
fn smallest_algebraic() {
let diag = [4.0, 3.0, 2.0, 1.0];
let result = eigen_matrix_symmetric(
4,
|x, y| {
for i in 0..4 {
y[i] = diag[i] * x[i];
}
},
2,
EigenWhich::SmallestAlgebraic,
)
.unwrap();
assert_eq!(result.eigenvalues.len(), 2);
assert!(
(result.eigenvalues[0] - 1.0).abs() < 1e-6,
"smallest eigenvalue should be 1.0, got {}",
result.eigenvalues[0]
);
}
#[test]
fn nev_zero_returns_empty() {
let result =
eigen_matrix_symmetric(3, |_x, _y| {}, 0, EigenWhich::LargestAlgebraic).unwrap();
assert!(result.eigenvalues.is_empty());
}
#[test]
fn n_zero_returns_error() {
let result = eigen_matrix_symmetric(0, |_x, _y| {}, 1, EigenWhich::LargestAlgebraic);
assert!(result.is_err());
}
#[test]
fn eigenvectors_orthogonal() {
let diag = [5.0, 3.0, 1.0];
let result = eigen_matrix_symmetric(
3,
|x, y| {
for i in 0..3 {
y[i] = diag[i] * x[i];
}
},
3,
EigenWhich::LargestAlgebraic,
)
.unwrap();
for i in 0..result.eigenvectors.len() {
for j in (i + 1)..result.eigenvectors.len() {
let dot: f64 = result.eigenvectors[i]
.iter()
.zip(&result.eigenvectors[j])
.map(|(a, b)| a * b)
.sum();
assert!(
dot.abs() < 1e-6,
"eigenvectors {i} and {j} should be orthogonal, dot={dot}"
);
}
}
}
}