rust_igraph/algorithms/eigen/
symmetric.rs1use crate::algorithms::community::lanczos;
24use crate::core::error::{IgraphError, IgraphResult};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum EigenWhich {
29 LargestAlgebraic,
31 SmallestAlgebraic,
33 LargestMagnitude,
35}
36
37#[derive(Debug, Clone)]
39pub struct EigenDecomposition {
40 pub eigenvalues: Vec<f64>,
42 pub eigenvectors: Vec<Vec<f64>>,
45}
46
47pub 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 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 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 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}