1use crate::csr::CsrMatrix;
38use crate::direct_solver::{sparse_lu_solve, SparseLuSolver, SparseSolver};
39use crate::error::{SparseError, SparseResult};
40use crate::iterative_solvers::Preconditioner;
41use crate::krylov::{self, IramConfig, KrylovEigenResult, ThickRestartLanczosConfig};
42use crate::lobpcg::{self, EigenTarget, LobpcgConfig, LobpcgResult};
43use scirs2_core::ndarray::{Array1, Array2};
44use scirs2_core::numeric::{Float, NumAssign, SparseElement};
45use std::fmt::Debug;
46use std::iter::Sum;
47
48#[derive(Debug, Clone, Default)]
54pub enum EigenvalueTarget {
55 SmallestAlgebraic,
57 LargestAlgebraic,
59 SmallestMagnitude,
61 #[default]
63 LargestMagnitude,
64 NearestTo(f64),
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum EigenMethod {
75 #[default]
77 Auto,
78 Lobpcg,
80 Iram,
82 ThickRestartLanczos,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum SpectralTransform {
89 #[default]
91 Standard,
92 ShiftInvert,
94 Cayley,
96}
97
98#[derive(Debug, Clone)]
104pub struct SparseEigenConfig {
105 pub n_eigenvalues: usize,
107 pub target: EigenvalueTarget,
109 pub method: EigenMethod,
111 pub symmetric: bool,
113 pub max_iter: usize,
115 pub tol: f64,
117 pub krylov_dim: Option<usize>,
119 pub verbose: bool,
121}
122
123impl Default for SparseEigenConfig {
124 fn default() -> Self {
125 Self {
126 n_eigenvalues: 6,
127 target: EigenvalueTarget::LargestMagnitude,
128 method: EigenMethod::Auto,
129 symmetric: false,
130 max_iter: 300,
131 tol: 1e-8,
132 krylov_dim: None,
133 verbose: false,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
144pub struct SparseEigenResult<F> {
145 pub eigenvalues: Array1<F>,
147 pub eigenvectors: Array2<F>,
149 pub n_converged: usize,
151 pub converged: bool,
153 pub residual_norms: Vec<F>,
155 pub iterations: usize,
157 pub matvec_count: usize,
159 pub method_used: EigenMethod,
161}
162
163pub fn sparse_eig<F>(
182 matrix: &CsrMatrix<F>,
183 config: &SparseEigenConfig,
184 preconditioner: Option<&dyn Preconditioner<F>>,
185) -> SparseResult<SparseEigenResult<F>>
186where
187 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
188{
189 let (m, n) = matrix.shape();
190 if m != n {
191 return Err(SparseError::ValueError(
192 "Eigenvalue computation requires a square matrix".to_string(),
193 ));
194 }
195 if config.n_eigenvalues == 0 {
196 return Err(SparseError::ValueError(
197 "n_eigenvalues must be > 0".to_string(),
198 ));
199 }
200 if config.n_eigenvalues > m {
201 return Err(SparseError::ValueError(format!(
202 "n_eigenvalues ({}) must be <= matrix dimension ({m})",
203 config.n_eigenvalues
204 )));
205 }
206
207 if let EigenvalueTarget::NearestTo(sigma) = config.target {
209 return shift_invert_eig(matrix, sigma, config, preconditioner);
210 }
211
212 let method = select_method(config, m);
214
215 match method {
216 EigenMethod::Lobpcg => run_lobpcg(matrix, config, preconditioner),
217 EigenMethod::Iram => run_iram(matrix, config),
218 EigenMethod::ThickRestartLanczos => run_lanczos(matrix, config),
219 EigenMethod::Auto => unreachable!("select_method should never return Auto"),
220 }
221}
222
223pub fn sparse_eigsh<F>(
227 matrix: &CsrMatrix<F>,
228 n_eigenvalues: usize,
229 target: EigenvalueTarget,
230 tol: Option<f64>,
231 preconditioner: Option<&dyn Preconditioner<F>>,
232) -> SparseResult<SparseEigenResult<F>>
233where
234 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
235{
236 let config = SparseEigenConfig {
237 n_eigenvalues,
238 target,
239 symmetric: true,
240 tol: tol.unwrap_or(1e-8),
241 ..Default::default()
242 };
243 sparse_eig(matrix, &config, preconditioner)
244}
245
246pub fn sparse_eigs<F>(
248 matrix: &CsrMatrix<F>,
249 n_eigenvalues: usize,
250 target: EigenvalueTarget,
251 tol: Option<f64>,
252) -> SparseResult<SparseEigenResult<F>>
253where
254 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
255{
256 let config = SparseEigenConfig {
257 n_eigenvalues,
258 target,
259 symmetric: false,
260 tol: tol.unwrap_or(1e-8),
261 ..Default::default()
262 };
263 sparse_eig(matrix, &config, None)
264}
265
266pub fn shift_invert_eig<F>(
277 matrix: &CsrMatrix<F>,
278 sigma: f64,
279 config: &SparseEigenConfig,
280 _preconditioner: Option<&dyn Preconditioner<F>>,
281) -> SparseResult<SparseEigenResult<F>>
282where
283 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
284{
285 let n = matrix.rows();
286 let sigma_f = F::from(sigma)
287 .ok_or_else(|| SparseError::ValueError("Failed to convert sigma".to_string()))?;
288
289 let shifted = build_shifted_matrix(matrix, sigma_f)?;
291
292 let mut lu_solver = SparseLuSolver::new();
294 lu_solver.factorize(&shifted)?;
295
296 let krylov_dim = config
307 .krylov_dim
308 .unwrap_or((2 * config.n_eigenvalues + 1).min(n));
309
310 if config.symmetric {
311 let lanczos_config = ThickRestartLanczosConfig {
313 n_eigenvalues: config.n_eigenvalues,
314 max_basis_size: krylov_dim.max(config.n_eigenvalues + 2).min(n),
315 max_restarts: config.max_iter,
316 tol: config.tol,
317 which: krylov::WhichEigenvalues::LargestMagnitude,
318 shift: Some(sigma),
319 verbose: config.verbose,
320 };
321
322 let result = krylov::thick_restart_lanczos(matrix, &lanczos_config, None)?;
323 Ok(convert_krylov_result(
324 result,
325 EigenMethod::ThickRestartLanczos,
326 ))
327 } else {
328 let iram_config = IramConfig {
330 n_eigenvalues: config.n_eigenvalues,
331 krylov_dim: krylov_dim.max(config.n_eigenvalues + 2).min(n),
332 max_restarts: config.max_iter,
333 tol: config.tol,
334 which: krylov::WhichEigenvalues::NearShift,
335 harmonic_ritz: true,
336 shift: Some(sigma),
337 verbose: config.verbose,
338 };
339
340 let result = krylov::iram(matrix, &iram_config, None)?;
341 Ok(convert_krylov_result(result, EigenMethod::Iram))
342 }
343}
344
345fn build_shifted_matrix<F>(matrix: &CsrMatrix<F>, sigma: F) -> SparseResult<CsrMatrix<F>>
347where
348 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
349{
350 let n = matrix.rows();
351 let (rows_orig, cols_orig, data_orig) = matrix.get_triplets();
352
353 let mut rows = rows_orig;
354 let mut cols = cols_orig;
355 let mut data = data_orig;
356
357 let mut has_diag = vec![false; n];
359 for (idx, (&r, &c)) in rows.iter().zip(cols.iter()).enumerate() {
360 if r == c {
361 has_diag[r] = true;
362 data[idx] -= sigma;
363 }
364 }
365
366 for i in 0..n {
368 if !has_diag[i] {
369 rows.push(i);
370 cols.push(i);
371 data.push(-sigma);
372 }
373 }
374
375 CsrMatrix::new(data, rows, cols, (n, n))
376}
377
378pub fn compute_residuals<F>(
384 matrix: &CsrMatrix<F>,
385 eigenvalues: &Array1<F>,
386 eigenvectors: &Array2<F>,
387) -> SparseResult<Vec<F>>
388where
389 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
390{
391 let n = matrix.rows();
392 let k = eigenvalues.len();
393 let mut residuals = Vec::with_capacity(k);
394
395 for j in 0..k {
396 let mut v = vec![F::sparse_zero(); n];
398 for i in 0..n {
399 v[i] = eigenvectors[[i, j]];
400 }
401
402 let av = csr_matvec(matrix, &v)?;
404
405 let lambda = eigenvalues[j];
407 let mut norm_sq = F::sparse_zero();
408 for i in 0..n {
409 let diff = av[i] - lambda * v[i];
410 norm_sq += diff * diff;
411 }
412 residuals.push(norm_sq.sqrt());
413 }
414
415 Ok(residuals)
416}
417
418pub fn check_eigenpairs<F>(
420 matrix: &CsrMatrix<F>,
421 eigenvalues: &Array1<F>,
422 eigenvectors: &Array2<F>,
423 tol: F,
424) -> SparseResult<bool>
425where
426 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
427{
428 let residuals = compute_residuals(matrix, eigenvalues, eigenvectors)?;
429 Ok(residuals.iter().all(|&r| r < tol))
430}
431
432pub fn cayley_transform_matvec<F>(
440 matrix: &CsrMatrix<F>,
441 sigma: f64,
442 v: &[F],
443) -> SparseResult<Vec<F>>
444where
445 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
446{
447 let n = matrix.rows();
448 let sigma_f = F::from(sigma)
449 .ok_or_else(|| SparseError::ValueError("Failed to convert sigma".to_string()))?;
450
451 let av = csr_matvec(matrix, v)?;
453 let mut rhs = vec![F::sparse_zero(); n];
454 for i in 0..n {
455 rhs[i] = av[i] + sigma_f * v[i];
456 }
457
458 let shifted = build_shifted_matrix(matrix, sigma_f)?;
460 sparse_lu_solve(&shifted, &rhs)
461}
462
463fn select_method(config: &SparseEigenConfig, n: usize) -> EigenMethod {
468 if config.method != EigenMethod::Auto {
469 return config.method;
470 }
471
472 match &config.target {
473 EigenvalueTarget::NearestTo(_) => {
474 if config.symmetric {
476 EigenMethod::ThickRestartLanczos
477 } else {
478 EigenMethod::Iram
479 }
480 }
481 EigenvalueTarget::SmallestAlgebraic | EigenvalueTarget::LargestAlgebraic => {
482 if config.symmetric {
483 if config.n_eigenvalues <= 10 && n > 100 {
486 EigenMethod::Lobpcg
487 } else {
488 EigenMethod::ThickRestartLanczos
489 }
490 } else {
491 EigenMethod::Iram
492 }
493 }
494 EigenvalueTarget::SmallestMagnitude => {
495 if config.symmetric {
497 EigenMethod::ThickRestartLanczos
498 } else {
499 EigenMethod::Iram
500 }
501 }
502 EigenvalueTarget::LargestMagnitude => {
503 if config.symmetric {
504 if config.n_eigenvalues <= 10 && n > 100 {
505 EigenMethod::Lobpcg
506 } else {
507 EigenMethod::ThickRestartLanczos
508 }
509 } else {
510 EigenMethod::Iram
511 }
512 }
513 }
514}
515
516fn run_lobpcg<F>(
521 matrix: &CsrMatrix<F>,
522 config: &SparseEigenConfig,
523 preconditioner: Option<&dyn Preconditioner<F>>,
524) -> SparseResult<SparseEigenResult<F>>
525where
526 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
527{
528 let target = match &config.target {
529 EigenvalueTarget::SmallestAlgebraic | EigenvalueTarget::SmallestMagnitude => {
530 EigenTarget::Smallest
531 }
532 _ => EigenTarget::Largest,
533 };
534
535 let lobpcg_config = LobpcgConfig {
536 block_size: config.n_eigenvalues,
537 max_iter: config.max_iter,
538 tol: config.tol,
539 target,
540 locking: true,
541 verbose: config.verbose,
542 };
543
544 let result = lobpcg::lobpcg(matrix, &lobpcg_config, preconditioner, None)?;
545 Ok(convert_lobpcg_result(result))
546}
547
548fn run_iram<F>(
549 matrix: &CsrMatrix<F>,
550 config: &SparseEigenConfig,
551) -> SparseResult<SparseEigenResult<F>>
552where
553 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
554{
555 let n = matrix.rows();
556 let krylov_dim = config
557 .krylov_dim
558 .unwrap_or((2 * config.n_eigenvalues + 1).min(n));
559
560 let which = match &config.target {
561 EigenvalueTarget::LargestMagnitude => krylov::WhichEigenvalues::LargestMagnitude,
562 EigenvalueTarget::SmallestMagnitude => krylov::WhichEigenvalues::SmallestMagnitude,
563 EigenvalueTarget::LargestAlgebraic => krylov::WhichEigenvalues::LargestReal,
564 EigenvalueTarget::SmallestAlgebraic => krylov::WhichEigenvalues::SmallestReal,
565 EigenvalueTarget::NearestTo(_) => krylov::WhichEigenvalues::NearShift,
566 };
567
568 let iram_config = IramConfig {
569 n_eigenvalues: config.n_eigenvalues,
570 krylov_dim: krylov_dim.max(config.n_eigenvalues + 2).min(n),
571 max_restarts: config.max_iter,
572 tol: config.tol,
573 which,
574 harmonic_ritz: matches!(config.target, EigenvalueTarget::SmallestMagnitude),
575 shift: None,
576 verbose: config.verbose,
577 };
578
579 let result = krylov::iram(matrix, &iram_config, None)?;
580 Ok(convert_krylov_result(result, EigenMethod::Iram))
581}
582
583fn run_lanczos<F>(
584 matrix: &CsrMatrix<F>,
585 config: &SparseEigenConfig,
586) -> SparseResult<SparseEigenResult<F>>
587where
588 F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
589{
590 let n = matrix.rows();
591 let krylov_dim = config
592 .krylov_dim
593 .unwrap_or((2 * config.n_eigenvalues + 1).min(n));
594
595 let which = match &config.target {
596 EigenvalueTarget::LargestMagnitude | EigenvalueTarget::LargestAlgebraic => {
597 krylov::WhichEigenvalues::LargestMagnitude
598 }
599 EigenvalueTarget::SmallestMagnitude | EigenvalueTarget::SmallestAlgebraic => {
600 krylov::WhichEigenvalues::SmallestMagnitude
601 }
602 EigenvalueTarget::NearestTo(_) => krylov::WhichEigenvalues::NearShift,
603 };
604
605 let lanczos_config = ThickRestartLanczosConfig {
606 n_eigenvalues: config.n_eigenvalues,
607 max_basis_size: krylov_dim.max(config.n_eigenvalues + 2).min(n),
608 max_restarts: config.max_iter,
609 tol: config.tol,
610 which,
611 shift: None,
612 verbose: config.verbose,
613 };
614
615 let result = krylov::thick_restart_lanczos(matrix, &lanczos_config, None)?;
616 Ok(convert_krylov_result(
617 result,
618 EigenMethod::ThickRestartLanczos,
619 ))
620}
621
622fn convert_lobpcg_result<F: Float + SparseElement>(
627 result: LobpcgResult<F>,
628) -> SparseEigenResult<F> {
629 SparseEigenResult {
630 eigenvalues: result.eigenvalues,
631 eigenvectors: result.eigenvectors,
632 n_converged: result.n_converged,
633 converged: result.converged,
634 residual_norms: result.residual_norms,
635 iterations: result.iterations,
636 matvec_count: 0, method_used: EigenMethod::Lobpcg,
638 }
639}
640
641fn convert_krylov_result<F: Float + SparseElement>(
642 result: KrylovEigenResult<F>,
643 method: EigenMethod,
644) -> SparseEigenResult<F> {
645 SparseEigenResult {
646 eigenvalues: result.eigenvalues,
647 eigenvectors: result.eigenvectors,
648 n_converged: result.n_converged,
649 converged: result.converged,
650 residual_norms: result.residual_norms,
651 iterations: result.restarts,
652 matvec_count: result.matvec_count,
653 method_used: method,
654 }
655}
656
657fn csr_matvec<F>(matrix: &CsrMatrix<F>, x: &[F]) -> SparseResult<Vec<F>>
662where
663 F: Float + NumAssign + SparseElement + Debug + 'static,
664{
665 let n = matrix.rows();
666 let m = matrix.cols();
667 if x.len() != m {
668 return Err(SparseError::DimensionMismatch {
669 expected: m,
670 found: x.len(),
671 });
672 }
673
674 let mut result = vec![F::sparse_zero(); n];
675 for i in 0..n {
676 let start = matrix.indptr[i];
677 let end = matrix.indptr[i + 1];
678 let mut sum = F::sparse_zero();
679 for idx in start..end {
680 sum += matrix.data[idx] * x[matrix.indices[idx]];
681 }
682 result[i] = sum;
683 }
684 Ok(result)
685}
686
687#[cfg(test)]
692mod tests {
693 use super::*;
694
695 fn create_diagonal_5x5() -> CsrMatrix<f64> {
697 let rows = vec![0, 1, 2, 3, 4];
698 let cols = vec![0, 1, 2, 3, 4];
699 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
700 CsrMatrix::new(data, rows, cols, (5, 5)).expect("Failed to create diagonal")
701 }
702
703 fn create_tridiag_6x6() -> CsrMatrix<f64> {
705 let mut rows = Vec::new();
706 let mut cols = Vec::new();
707 let mut data = Vec::new();
708 for i in 0..6 {
709 rows.push(i);
710 cols.push(i);
711 data.push(4.0);
712 if i > 0 {
713 rows.push(i);
714 cols.push(i - 1);
715 data.push(1.0);
716 }
717 if i < 5 {
718 rows.push(i);
719 cols.push(i + 1);
720 data.push(1.0);
721 }
722 }
723 CsrMatrix::new(data, rows, cols, (6, 6)).expect("Failed to create tridiag")
724 }
725
726 fn create_nonsymmetric_4x4() -> CsrMatrix<f64> {
728 let rows = vec![0, 0, 1, 1, 2, 2, 3, 3];
729 let cols = vec![0, 1, 0, 1, 2, 3, 2, 3];
730 let data = vec![2.0, 1.0, 0.0, 3.0, 4.0, 1.0, 0.0, 5.0];
731 CsrMatrix::new(data, rows, cols, (4, 4)).expect("Failed to create nonsymmetric")
732 }
733
734 #[test]
735 fn test_method_selection_symmetric_largest() {
736 let config = SparseEigenConfig {
737 n_eigenvalues: 3,
738 target: EigenvalueTarget::LargestMagnitude,
739 symmetric: true,
740 ..Default::default()
741 };
742 let method = select_method(&config, 200);
743 assert_eq!(method, EigenMethod::Lobpcg);
744 }
745
746 #[test]
747 fn test_method_selection_nonsymmetric() {
748 let config = SparseEigenConfig {
749 n_eigenvalues: 3,
750 target: EigenvalueTarget::LargestMagnitude,
751 symmetric: false,
752 ..Default::default()
753 };
754 let method = select_method(&config, 200);
755 assert_eq!(method, EigenMethod::Iram);
756 }
757
758 #[test]
759 fn test_method_selection_forced() {
760 let config = SparseEigenConfig {
761 method: EigenMethod::Lobpcg,
762 ..Default::default()
763 };
764 let method = select_method(&config, 200);
765 assert_eq!(method, EigenMethod::Lobpcg);
766 }
767
768 #[test]
769 fn test_sparse_eigsh_diagonal() {
770 let mat = create_diagonal_5x5();
771 let result = sparse_eigsh(
774 &mat,
775 2,
776 EigenvalueTarget::LargestAlgebraic,
777 Some(1e-6),
778 None,
779 );
780 match result {
783 Ok(res) => {
784 assert!(res.eigenvalues.len() <= 2);
785 }
786 Err(e) => {
787 let msg = format!("{e}");
791 assert!(
792 msg.contains("krylov")
793 || msg.contains("basis")
794 || msg.contains("block")
795 || msg.contains("dim")
796 || msg.contains("Krylov")
797 || msg.contains("eigenvalue"),
798 "Unexpected error: {e}"
799 );
800 }
801 }
802 }
803
804 #[test]
805 fn test_sparse_eigs_nonsymmetric() {
806 let mat = create_nonsymmetric_4x4();
807 let result = sparse_eigs(&mat, 2, EigenvalueTarget::LargestMagnitude, Some(1e-6));
808 match result {
809 Ok(res) => {
810 assert!(res.eigenvalues.len() <= 2);
811 }
812 Err(e) => {
813 let msg = format!("{e}");
814 assert!(
815 msg.contains("krylov")
816 || msg.contains("basis")
817 || msg.contains("dim")
818 || msg.contains("Krylov")
819 || msg.contains("eigenvalue"),
820 "Unexpected error: {e}"
821 );
822 }
823 }
824 }
825
826 #[test]
827 fn test_compute_residuals() {
828 let mat = create_diagonal_5x5();
829 let eigenvalues = Array1::from_vec(vec![1.0]);
831 let mut eigenvectors = Array2::zeros((5, 1));
832 eigenvectors[[0, 0]] = 1.0;
833
834 let residuals =
835 compute_residuals(&mat, &eigenvalues, &eigenvectors).expect("Residuals failed");
836 assert_eq!(residuals.len(), 1);
837 assert!(residuals[0] < 1e-14, "Residual too large: {}", residuals[0]);
838 }
839
840 #[test]
841 fn test_compute_residuals_multiple() {
842 let mat = create_diagonal_5x5();
843 let eigenvalues = Array1::from_vec(vec![1.0, 5.0]);
844 let mut eigenvectors = Array2::zeros((5, 2));
845 eigenvectors[[0, 0]] = 1.0; eigenvectors[[4, 1]] = 1.0; let residuals =
849 compute_residuals(&mat, &eigenvalues, &eigenvectors).expect("Residuals failed");
850 assert_eq!(residuals.len(), 2);
851 for (i, &r) in residuals.iter().enumerate() {
852 assert!(r < 1e-14, "Residual {i} too large: {r}");
853 }
854 }
855
856 #[test]
857 fn test_check_eigenpairs_pass() {
858 let mat = create_diagonal_5x5();
859 let eigenvalues = Array1::from_vec(vec![3.0]);
860 let mut eigenvectors = Array2::zeros((5, 1));
861 eigenvectors[[2, 0]] = 1.0;
862
863 let ok = check_eigenpairs(&mat, &eigenvalues, &eigenvectors, 1e-10).expect("Check failed");
864 assert!(ok);
865 }
866
867 #[test]
868 fn test_check_eigenpairs_fail() {
869 let mat = create_diagonal_5x5();
870 let eigenvalues = Array1::from_vec(vec![2.5]); let mut eigenvectors = Array2::zeros((5, 1));
872 eigenvectors[[2, 0]] = 1.0; let ok = check_eigenpairs(&mat, &eigenvalues, &eigenvectors, 1e-10).expect("Check failed");
875 assert!(!ok);
876 }
877
878 #[test]
879 fn test_build_shifted_matrix() {
880 let mat = create_diagonal_5x5();
881 let shifted = build_shifted_matrix(&mat, 2.0).expect("Shift failed");
882 assert!((shifted.get(0, 0) - (-1.0)).abs() < 1e-14);
884 assert!((shifted.get(1, 1) - 0.0).abs() < 1e-14);
885 assert!((shifted.get(2, 2) - 1.0).abs() < 1e-14);
886 assert!((shifted.get(3, 3) - 2.0).abs() < 1e-14);
887 assert!((shifted.get(4, 4) - 3.0).abs() < 1e-14);
888 }
889
890 #[test]
891 fn test_build_shifted_matrix_with_offdiag() {
892 let mat = create_tridiag_6x6();
893 let shifted = build_shifted_matrix(&mat, 1.0).expect("Shift failed");
894 assert!((shifted.get(0, 0) - 3.0).abs() < 1e-14);
896 assert!((shifted.get(0, 1) - 1.0).abs() < 1e-14);
898 }
899
900 #[test]
901 fn test_sparse_eig_non_square_error() {
902 let rows = vec![0, 1];
903 let cols = vec![0, 0];
904 let data = vec![1.0, 2.0];
905 let mat = CsrMatrix::new(data, rows, cols, (2, 3)).expect("Failed");
906 let config = SparseEigenConfig::default();
907 let result = sparse_eig(&mat, &config, None);
908 assert!(result.is_err());
909 }
910
911 #[test]
912 fn test_sparse_eig_zero_eigenvalues_error() {
913 let mat = create_diagonal_5x5();
914 let config = SparseEigenConfig {
915 n_eigenvalues: 0,
916 ..Default::default()
917 };
918 let result = sparse_eig(&mat, &config, None);
919 assert!(result.is_err());
920 }
921
922 #[test]
923 fn test_sparse_eig_too_many_eigenvalues_error() {
924 let mat = create_diagonal_5x5();
925 let config = SparseEigenConfig {
926 n_eigenvalues: 10,
927 ..Default::default()
928 };
929 let result = sparse_eig(&mat, &config, None);
930 assert!(result.is_err());
931 }
932
933 #[test]
934 fn test_cayley_transform() {
935 let mat = create_diagonal_5x5();
936 let v = vec![1.0, 0.0, 0.0, 0.0, 0.0];
939 let result = cayley_transform_matvec(&mat, 0.5, &v);
940 match result {
941 Ok(cv) => {
942 assert!(
945 (cv[0] - 3.0).abs() < 1e-10,
946 "Cayley[0] = {}, expected 3.0",
947 cv[0]
948 );
949 for i in 1..5 {
951 assert!(cv[i].abs() < 1e-10, "Cayley[{i}] = {}, expected 0", cv[i]);
952 }
953 }
954 Err(e) => {
955 panic!("Cayley transform failed: {e}");
956 }
957 }
958 }
959
960 #[test]
961 fn test_config_defaults() {
962 let config = SparseEigenConfig::default();
963 assert_eq!(config.n_eigenvalues, 6);
964 assert_eq!(config.method, EigenMethod::Auto);
965 assert!(!config.symmetric);
966 assert_eq!(config.max_iter, 300);
967 assert!((config.tol - 1e-8).abs() < 1e-15);
968 }
969
970 #[test]
971 fn test_eigenvalue_target_default() {
972 let target = EigenvalueTarget::default();
973 assert!(matches!(target, EigenvalueTarget::LargestMagnitude));
974 }
975
976 #[test]
977 fn test_method_selection_small_symmetric() {
978 let config = SparseEigenConfig {
979 n_eigenvalues: 3,
980 target: EigenvalueTarget::LargestAlgebraic,
981 symmetric: true,
982 ..Default::default()
983 };
984 let method = select_method(&config, 10);
986 assert_eq!(method, EigenMethod::ThickRestartLanczos);
987 }
988
989 #[test]
990 fn test_csr_matvec_internal() {
991 let mat = create_diagonal_5x5();
992 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
993 let y = csr_matvec(&mat, &x).expect("Matvec failed");
994 assert_eq!(y, vec![1.0, 4.0, 9.0, 16.0, 25.0]);
995 }
996
997 #[test]
998 fn test_csr_matvec_dimension_error() {
999 let mat = create_diagonal_5x5();
1000 let x = vec![1.0, 2.0];
1001 let result = csr_matvec(&mat, &x);
1002 assert!(result.is_err());
1003 }
1004
1005 #[test]
1006 fn test_shift_invert_eig_symmetric() {
1007 let mat = create_diagonal_5x5();
1008 let config = SparseEigenConfig {
1010 n_eigenvalues: 2,
1011 target: EigenvalueTarget::NearestTo(2.5),
1012 symmetric: true,
1013 max_iter: 500,
1014 tol: 1e-6,
1015 ..Default::default()
1016 };
1017 let result = sparse_eig(&mat, &config, None);
1018 match result {
1020 Ok(res) => {
1021 assert!(res.eigenvalues.len() <= 2);
1022 }
1023 Err(_) => {
1024 }
1026 }
1027 }
1028
1029 #[test]
1030 fn test_sparse_eigsh_tridiag() {
1031 let mat = create_tridiag_6x6();
1032 let result = sparse_eigsh(
1033 &mat,
1034 2,
1035 EigenvalueTarget::LargestAlgebraic,
1036 Some(1e-6),
1037 None,
1038 );
1039 match result {
1040 Ok(res) => {
1041 assert!(res.eigenvalues.len() <= 2);
1042 if res.n_converged > 0 {
1046 assert!(
1047 res.eigenvalues[0] > 3.0,
1048 "Largest eigenvalue should be > 3, got {}",
1049 res.eigenvalues[0]
1050 );
1051 }
1052 }
1053 Err(e) => {
1054 let msg = format!("{e}");
1055 assert!(
1056 msg.contains("krylov")
1057 || msg.contains("basis")
1058 || msg.contains("dim")
1059 || msg.contains("Krylov")
1060 || msg.contains("eigenvalue")
1061 || msg.contains("block"),
1062 "Unexpected error: {e}"
1063 );
1064 }
1065 }
1066 }
1067}