Skip to main content

rust_igraph/algorithms/eigen/
symmetric.rs

1//! Symmetric eigenvalue solver via Lanczos iteration.
2//!
3//! Computes selected eigenvalues and eigenvectors of a real symmetric
4//! matrix defined implicitly by a matrix-vector product closure.
5//!
6//! ```
7//! use rust_igraph::eigen_matrix_symmetric;
8//! use rust_igraph::EigenWhich;
9//!
10//! // diag(3, 2, 1): top-2 eigenvalues are 3.0 and 2.0
11//! let result = eigen_matrix_symmetric(
12//!     3,
13//!     |x, y| { y[0] = 3.0*x[0]; y[1] = 2.0*x[1]; y[2] = x[2]; },
14//!     2,
15//!     EigenWhich::LargestAlgebraic,
16//! ).unwrap();
17//!
18//! assert_eq!(result.eigenvalues.len(), 2);
19//! assert!((result.eigenvalues[0] - 3.0).abs() < 1e-6);
20//! assert!((result.eigenvalues[1] - 2.0).abs() < 1e-6);
21//! ```
22
23use crate::algorithms::community::lanczos;
24use crate::core::error::{IgraphError, IgraphResult};
25
26/// Which eigenvalues to compute.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum EigenWhich {
29    /// Largest algebraic value (most positive).
30    LargestAlgebraic,
31    /// Smallest algebraic value (most negative).
32    SmallestAlgebraic,
33    /// Largest magnitude (largest |λ|).
34    LargestMagnitude,
35}
36
37/// Result of an eigenvalue decomposition.
38#[derive(Debug, Clone)]
39pub struct EigenDecomposition {
40    /// Computed eigenvalues, ordered according to the requested criterion.
41    pub eigenvalues: Vec<f64>,
42    /// Corresponding eigenvectors (one `Vec<f64>` per eigenvalue, each of
43    /// length `n`).
44    pub eigenvectors: Vec<Vec<f64>>,
45}
46
47/// Compute selected eigenpairs of a real symmetric matrix.
48///
49/// The matrix is defined implicitly: `matvec(x, y)` must compute
50/// `y = A * x` where both slices have length `n`.
51///
52/// # Arguments
53///
54/// * `n` — matrix dimension
55/// * `matvec` — closure computing the matrix-vector product `y = A x`
56/// * `nev` — number of eigenvalues to compute (clamped to `n`)
57/// * `which` — which part of the spectrum to target
58///
59/// # Errors
60///
61/// Returns [`IgraphError::InvalidArgument`] if `n == 0`.
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{eigen_matrix_symmetric, EigenWhich};
67///
68/// // diag(5, 3, 1): top-2 eigenvalues are 5.0 and 3.0.
69/// let result = eigen_matrix_symmetric(
70///     3,
71///     |x, y| { y[0] = 5.0*x[0]; y[1] = 3.0*x[1]; y[2] = x[2]; },
72///     2,
73///     EigenWhich::LargestAlgebraic,
74/// ).unwrap();
75/// assert!((result.eigenvalues[0] - 5.0).abs() < 1e-6);
76/// assert!((result.eigenvalues[1] - 3.0).abs() < 1e-6);
77/// ```
78pub fn eigen_matrix_symmetric<F>(
79    n: usize,
80    matvec: F,
81    nev: usize,
82    which: EigenWhich,
83) -> IgraphResult<EigenDecomposition>
84where
85    F: Fn(&[f64], &mut [f64]),
86{
87    if n == 0 {
88        return Err(IgraphError::InvalidArgument(
89            "eigen_matrix_symmetric: matrix dimension must be > 0".into(),
90        ));
91    }
92
93    let nev = nev.min(n);
94    if nev == 0 {
95        return Ok(EigenDecomposition {
96            eigenvalues: Vec::new(),
97            eigenvectors: Vec::new(),
98        });
99    }
100
101    let internal_which = match which {
102        EigenWhich::LargestAlgebraic => lanczos::EigenWhich::LargestAlgebraic,
103        EigenWhich::SmallestAlgebraic => lanczos::EigenWhich::SmallestAlgebraic,
104        EigenWhich::LargestMagnitude => lanczos::EigenWhich::LargestMagnitude,
105    };
106
107    let max_iter = n.saturating_mul(200).max(2000);
108    let result = lanczos::lanczos_top_k(n, &matvec, nev, internal_which, max_iter);
109
110    Ok(EigenDecomposition {
111        eigenvalues: result.eigenvalues,
112        eigenvectors: result.eigenvectors,
113    })
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn identity_eigenvalues() {
122        // The identity matrix has eigenvalue 1 with multiplicity n.
123        // Lanczos collapses the Krylov subspace for scalar multiples of I,
124        // so we request 1 eigenvalue (the only distinct one).
125        let result = eigen_matrix_symmetric(
126            4,
127            |x, y| {
128                y[..4].copy_from_slice(&x[..4]);
129            },
130            1,
131            EigenWhich::LargestAlgebraic,
132        )
133        .unwrap();
134
135        assert_eq!(result.eigenvalues.len(), 1);
136        assert!(
137            (result.eigenvalues[0] - 1.0).abs() < 1e-6,
138            "identity eigenvalue should be 1.0, got {}",
139            result.eigenvalues[0]
140        );
141    }
142
143    #[test]
144    fn diagonal_matrix_top2() {
145        // diag(4, 3, 2, 1) — top-2 eigenvalues should be 4.0 and 3.0
146        let diag = [4.0, 3.0, 2.0, 1.0];
147        let result = eigen_matrix_symmetric(
148            4,
149            |x, y| {
150                for i in 0..4 {
151                    y[i] = diag[i] * x[i];
152                }
153            },
154            2,
155            EigenWhich::LargestAlgebraic,
156        )
157        .unwrap();
158
159        assert_eq!(result.eigenvalues.len(), 2);
160        assert!(
161            (result.eigenvalues[0] - 4.0).abs() < 1e-6,
162            "top eigenvalue should be 4.0, got {}",
163            result.eigenvalues[0]
164        );
165        assert!(
166            (result.eigenvalues[1] - 3.0).abs() < 1e-6,
167            "second eigenvalue should be 3.0, got {}",
168            result.eigenvalues[1]
169        );
170    }
171
172    #[test]
173    fn smallest_algebraic() {
174        let diag = [4.0, 3.0, 2.0, 1.0];
175        let result = eigen_matrix_symmetric(
176            4,
177            |x, y| {
178                for i in 0..4 {
179                    y[i] = diag[i] * x[i];
180                }
181            },
182            2,
183            EigenWhich::SmallestAlgebraic,
184        )
185        .unwrap();
186
187        assert_eq!(result.eigenvalues.len(), 2);
188        assert!(
189            (result.eigenvalues[0] - 1.0).abs() < 1e-6,
190            "smallest eigenvalue should be 1.0, got {}",
191            result.eigenvalues[0]
192        );
193    }
194
195    #[test]
196    fn nev_zero_returns_empty() {
197        let result =
198            eigen_matrix_symmetric(3, |_x, _y| {}, 0, EigenWhich::LargestAlgebraic).unwrap();
199        assert!(result.eigenvalues.is_empty());
200    }
201
202    #[test]
203    fn n_zero_returns_error() {
204        let result = eigen_matrix_symmetric(0, |_x, _y| {}, 1, EigenWhich::LargestAlgebraic);
205        assert!(result.is_err());
206    }
207
208    #[test]
209    fn eigenvectors_orthogonal() {
210        // Symmetric matrix with distinct eigenvalues: eigenvectors should be orthogonal
211        let diag = [5.0, 3.0, 1.0];
212        let result = eigen_matrix_symmetric(
213            3,
214            |x, y| {
215                for i in 0..3 {
216                    y[i] = diag[i] * x[i];
217                }
218            },
219            3,
220            EigenWhich::LargestAlgebraic,
221        )
222        .unwrap();
223
224        for i in 0..result.eigenvectors.len() {
225            for j in (i + 1)..result.eigenvectors.len() {
226                let dot: f64 = result.eigenvectors[i]
227                    .iter()
228                    .zip(&result.eigenvectors[j])
229                    .map(|(a, b)| a * b)
230                    .sum();
231                assert!(
232                    dot.abs() < 1e-6,
233                    "eigenvectors {i} and {j} should be orthogonal, dot={dot}"
234                );
235            }
236        }
237    }
238}