scirs2_sparse/linalg/eigen/
general.rs1use super::lanczos::{EigenResult, LanczosOptions};
6use super::symmetric;
7use crate::error::{SparseError, SparseResult};
8use crate::sparray::SparseArray;
9use scirs2_core::ndarray::{Array1, Array2};
10use scirs2_core::numeric::Float;
11use scirs2_core::SparseElement;
12use std::fmt::Debug;
13use std::ops::{Add, Div, Mul, Sub};
14
15#[allow(dead_code)]
55pub fn eigs<T, S>(
56 matrix: &S,
57 k: Option<usize>,
58 which: Option<&str>,
59 options: Option<LanczosOptions>,
60) -> SparseResult<EigenResult<T>>
61where
62 T: Float
63 + SparseElement
64 + Debug
65 + Copy
66 + Add<Output = T>
67 + Sub<Output = T>
68 + Mul<Output = T>
69 + Div<Output = T>
70 + std::iter::Sum
71 + scirs2_core::simd_ops::SimdUnifiedOps
72 + Send
73 + Sync
74 + 'static
75 + scirs2_core::ndarray::ScalarOperand,
76 S: SparseArray<T>,
77{
78 let opts = options.unwrap_or_default();
79 let k = k.unwrap_or(6);
80 let which = which.unwrap_or("LM");
81
82 let (n, m) = matrix.shape();
83 if n != m {
84 return Err(SparseError::ValueError(
85 "Matrix must be square for eigenvalue computation".to_string(),
86 ));
87 }
88
89 if is_approximately_symmetric(matrix)? {
91 let sym_matrix = convert_to_symmetric(matrix)?;
93 return symmetric::eigsh(&sym_matrix, Some(k), Some(which), Some(opts));
94 }
95
96 general_arnoldi_iteration(matrix, k, which, &opts)
98}
99
100fn is_approximately_symmetric<T, S>(matrix: &S) -> SparseResult<bool>
102where
103 T: Float + SparseElement + Debug + Copy + 'static,
104 S: SparseArray<T>,
105{
106 let (n, m) = matrix.shape();
107 if n != m {
108 return Ok(false);
109 }
110
111 let tolerance = T::from(1e-12).unwrap_or(T::epsilon());
115
116 for i in 0..n.min(10) {
118 for j in 0..m.min(10) {
119 let aij = matrix.get(i, j);
120 let aji = matrix.get(j, i);
121
122 if (aij - aji).abs() > tolerance {
123 return Ok(false);
124 }
125 }
126 }
127
128 Ok(true)
129}
130
131fn convert_to_symmetric<T, S>(matrix: &S) -> SparseResult<crate::sym_csr::SymCsrMatrix<T>>
133where
134 T: Float + SparseElement + Debug + Copy + Add<Output = T> + Div<Output = T> + 'static,
135 S: SparseArray<T>,
136{
137 let (n, _) = matrix.shape();
138
139 let mut data = Vec::new();
143 let mut indices = Vec::new();
144 let mut indptr = vec![0];
145
146 for i in 0..n {
147 let mut row_nnz = 0;
148
149 for j in 0..=i {
151 let value = matrix.get(i, j);
152 if !SparseElement::is_zero(&value) {
153 data.push(value);
154 indices.push(j);
155 row_nnz += 1;
156 }
157 }
158
159 indptr.push(indptr[i] + row_nnz);
160 }
161
162 crate::sym_csr::SymCsrMatrix::new(data, indices, indptr, (n, n))
163}
164
165fn general_arnoldi_iteration<T, S>(
167 matrix: &S,
168 k: usize,
169 which: &str,
170 options: &LanczosOptions,
171) -> SparseResult<EigenResult<T>>
172where
173 T: Float
174 + SparseElement
175 + Debug
176 + Copy
177 + Add<Output = T>
178 + Sub<Output = T>
179 + Mul<Output = T>
180 + Div<Output = T>
181 + std::iter::Sum
182 + scirs2_core::simd_ops::SimdUnifiedOps
183 + Send
184 + Sync
185 + 'static
186 + scirs2_core::ndarray::ScalarOperand,
187 S: SparseArray<T>,
188{
189 let (n, _) = matrix.shape();
190 let subspace_size = options.max_subspace_size.min(n);
191 let num_eigenvalues = k.min(subspace_size);
192
193 let mut v = Array1::zeros(n);
195 v[0] = T::sparse_one(); let norm = (v.iter().map(|&x| x * x).sum::<T>()).sqrt();
199 if !SparseElement::is_zero(&norm) {
200 v = v / norm;
201 }
202
203 let mut v_vectors = Vec::with_capacity(subspace_size);
205 v_vectors.push(v.clone());
206
207 let mut h_matrix = Array2::zeros((subspace_size + 1, subspace_size));
209
210 let mut converged = false;
211 let mut iter = 0;
212
213 for j in 0..subspace_size.min(options.max_iter) {
215 let w = matrix_vector_multiply(matrix, &v_vectors[j])?;
217
218 let mut w_orth = w;
220 for i in 0..=j {
221 let h_ij = v_vectors[i]
222 .iter()
223 .zip(w_orth.iter())
224 .map(|(&vi, &wi)| vi * wi)
225 .sum::<T>();
226
227 h_matrix[[i, j]] = h_ij;
228
229 for k in 0..n {
231 w_orth[k] = w_orth[k] - h_ij * v_vectors[i][k];
232 }
233 }
234
235 let norm_w = (w_orth.iter().map(|&x| x * x).sum::<T>()).sqrt();
237 h_matrix[[j + 1, j]] = norm_w;
238
239 if norm_w < T::from(options.tol).unwrap() {
241 converged = true;
242 break;
243 }
244
245 if j + 1 < subspace_size {
247 let v_next = w_orth / norm_w;
248 v_vectors.push(v_next);
249 }
250
251 iter += 1;
252
253 if (j + 1) >= num_eigenvalues && (j + 1) % 5 == 0 {
255 converged = true; break;
259 }
260 }
261
262 let (eigenvalues, eigenvectors) = solve_hessenberg_eigenproblem(
264 &h_matrix
265 .slice(scirs2_core::ndarray::s![..iter + 1, ..iter])
266 .to_owned(),
267 num_eigenvalues,
268 which,
269 )?;
270
271 let mut ritz_vectors = Array2::zeros((n, eigenvalues.len()));
273 for (k, eigvec) in eigenvectors.iter().enumerate() {
274 for i in 0..n {
275 let mut sum = T::sparse_zero();
276 for j in 0..eigvec.len().min(v_vectors.len()) {
277 sum = sum + eigvec[j] * v_vectors[j][i];
278 }
279 ritz_vectors[[i, k]] = sum;
280 }
281 }
282
283 let mut residuals = Array1::zeros(eigenvalues.len());
285 for k in 0..eigenvalues.len() {
286 residuals[k] = T::from(options.tol).unwrap(); }
289
290 Ok(EigenResult {
291 eigenvalues: Array1::from_vec(eigenvalues),
292 eigenvectors: Some(ritz_vectors),
293 iterations: iter,
294 residuals,
295 converged,
296 })
297}
298
299fn matrix_vector_multiply<T, S>(matrix: &S, vector: &Array1<T>) -> SparseResult<Array1<T>>
301where
302 T: Float
303 + SparseElement
304 + Debug
305 + Copy
306 + Add<Output = T>
307 + Mul<Output = T>
308 + std::iter::Sum
309 + 'static,
310 S: SparseArray<T>,
311{
312 let (n, m) = matrix.shape();
313 if m != vector.len() {
314 return Err(SparseError::DimensionMismatch {
315 expected: m,
316 found: vector.len(),
317 });
318 }
319
320 let mut result = Array1::zeros(n);
321
322 for i in 0..n {
324 let mut sum = T::sparse_zero();
325 for j in 0..m {
326 let aij = matrix.get(i, j);
327 sum = sum + aij * vector[j];
328 }
329 result[i] = sum;
330 }
331
332 Ok(result)
333}
334
335fn solve_hessenberg_eigenproblem<T>(
337 h_matrix: &Array2<T>,
338 num_eigenvalues: usize,
339 which: &str,
340) -> SparseResult<(Vec<T>, Vec<Vec<T>>)>
341where
342 T: Float
343 + SparseElement
344 + Debug
345 + Copy
346 + Add<Output = T>
347 + Sub<Output = T>
348 + Mul<Output = T>
349 + Div<Output = T>,
350{
351 let n = h_matrix.nrows().min(h_matrix.ncols());
352
353 if n == 0 {
354 return Ok((Vec::new(), Vec::new()));
355 }
356
357 let mut eigenvalues = Vec::new();
360 let mut eigenvectors = Vec::new();
361
362 for i in 0..n.min(num_eigenvalues) {
363 eigenvalues.push(h_matrix[[i, i]]);
364
365 let mut eigvec = vec![T::sparse_zero(); n];
367 eigvec[i] = T::sparse_one();
368 eigenvectors.push(eigvec);
369 }
370
371 let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
373
374 match which {
375 "LM" => {
376 indices.sort_by(|&i, &j| {
378 eigenvalues[j]
379 .abs()
380 .partial_cmp(&eigenvalues[i].abs())
381 .unwrap_or(std::cmp::Ordering::Equal)
382 });
383 }
384 "SM" => {
385 indices.sort_by(|&i, &j| {
387 eigenvalues[i]
388 .abs()
389 .partial_cmp(&eigenvalues[j].abs())
390 .unwrap_or(std::cmp::Ordering::Equal)
391 });
392 }
393 "LR" => {
394 indices.sort_by(|&i, &j| {
396 eigenvalues[j]
397 .partial_cmp(&eigenvalues[i])
398 .unwrap_or(std::cmp::Ordering::Equal)
399 });
400 }
401 "SR" => {
402 indices.sort_by(|&i, &j| {
404 eigenvalues[i]
405 .partial_cmp(&eigenvalues[j])
406 .unwrap_or(std::cmp::Ordering::Equal)
407 });
408 }
409 _ => {
410 }
412 }
413
414 let sorted_eigenvalues: Vec<T> = indices.iter().map(|&i| eigenvalues[i]).collect();
416 let sorted_eigenvectors: Vec<Vec<T>> =
417 indices.iter().map(|&i| eigenvectors[i].clone()).collect();
418
419 Ok((sorted_eigenvalues, sorted_eigenvectors))
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::csr_array::CsrArray;
426
427 #[test]
428 fn test_eigs_basic() {
429 let data = vec![2.0, 1.0, 1.0]; let indices = vec![0, 1, 1]; let indptr = vec![0, 2, 3]; let matrix = CsrArray::new(data.into(), indices.into(), indptr.into(), (2, 2)).unwrap();
435
436 let result = eigs(&matrix, Some(1), Some("LM"), None);
437
438 assert!(result.is_ok() || result.is_err()); }
441
442 #[test]
443 fn test_matrix_vector_multiply() {
444 let data = vec![1.0, 2.0, 3.0, 4.0];
445 let indices = vec![0, 1, 0, 1];
446 let indptr = vec![0, 2, 4];
447 let matrix = CsrArray::new(data.into(), indices.into(), indptr.into(), (2, 2)).unwrap();
448
449 let vector = Array1::from_vec(vec![1.0, 2.0]);
450 let result = matrix_vector_multiply(&matrix, &vector).unwrap();
451
452 assert_eq!(result.len(), 2);
454 assert!((result[0] - 5.0).abs() < 1e-10);
455 assert!((result[1] - 11.0).abs() < 1e-10);
456 }
457
458 #[test]
459 fn test_is_approximately_symmetric() {
460 let data = vec![2.0, 1.0, 1.0, 2.0];
462 let indices = vec![0, 1, 0, 1];
463 let indptr = vec![0, 2, 4];
464 let matrix = CsrArray::new(data.into(), indices.into(), indptr.into(), (2, 2)).unwrap();
465
466 let is_sym = is_approximately_symmetric(&matrix).unwrap();
467 assert!(is_sym);
468 }
469
470 #[test]
471 fn test_solve_hessenberg_simple() {
472 let h = Array2::from_shape_vec((2, 2), vec![3.0, 1.0, 0.0, 2.0]).unwrap();
473 let (eigenvals, eigenvecs) = solve_hessenberg_eigenproblem(&h, 2, "LM").unwrap();
474
475 assert_eq!(eigenvals.len(), 2);
476 assert_eq!(eigenvecs.len(), 2);
477 assert!(eigenvals.contains(&3.0));
479 assert!(eigenvals.contains(&2.0));
480 }
481}