1use std::time::Instant;
36
37use tracing::{debug, info, instrument, warn};
38
39use crate::error::{SolverError, ValidationError};
40use crate::traits::SolverEngine;
41use crate::types::{
42 Algorithm, ComplexityClass, ComplexityEstimate, ComputeBudget, ConvergenceInfo, CsrMatrix,
43 SolverResult, SparsityProfile,
44};
45
46const POWER_ITERATION_STEPS: usize = 10;
48
49const INSTABILITY_GROWTH_FACTOR: f64 = 2.0;
52
53#[derive(Debug, Clone)]
80pub struct NeumannSolver {
81 pub tolerance: f64,
83 pub max_iterations: usize,
85}
86
87impl NeumannSolver {
88 pub fn new(tolerance: f64, max_iterations: usize) -> Self {
95 Self {
96 tolerance,
97 max_iterations,
98 }
99 }
100
101 #[instrument(skip(matrix), fields(n = matrix.rows))]
117 pub fn estimate_spectral_radius(matrix: &CsrMatrix<f32>) -> f64 {
118 let n = matrix.rows;
119 if n == 0 {
120 return 0.0;
121 }
122
123 let d_inv = extract_diag_inv_f32(matrix);
124 Self::estimate_spectral_radius_with_diag(matrix, &d_inv)
125 }
126
127 fn estimate_spectral_radius_with_diag(
133 matrix: &CsrMatrix<f32>,
134 d_inv: &[f32],
135 ) -> f64 {
136 let n = matrix.rows;
137 if n == 0 {
138 return 0.0;
139 }
140
141 let mut v: Vec<f32> = (0..n)
143 .map(|i| ((i.wrapping_mul(7).wrapping_add(13)) % 100) as f32 / 100.0)
144 .collect();
145 let norm = l2_norm_f32(&v);
146 if norm > 1e-12 {
147 scale_vec_f32(&mut v, 1.0 / norm);
148 }
149
150 let mut av = vec![0.0f32; n]; let mut w = vec![0.0f32; n]; let mut eigenvalue_estimate = 0.0f64;
153
154 for _ in 0..POWER_ITERATION_STEPS {
155 matrix.spmv(&v, &mut av);
157 for j in 0..n {
158 w[j] = v[j] - d_inv[j] * av[j];
159 }
160
161 let dot: f64 = v
163 .iter()
164 .zip(w.iter())
165 .map(|(&a, &b)| a as f64 * b as f64)
166 .sum();
167 eigenvalue_estimate = dot;
168
169 let w_norm = l2_norm_f32(&w);
171 if w_norm < 1e-12 {
172 break;
173 }
174 for j in 0..n {
175 v[j] = w[j] / w_norm as f32;
176 }
177 }
178
179 let rho = eigenvalue_estimate.abs();
180 debug!(rho, "estimated spectral radius of (I - D^-1 A)");
181 rho
182 }
183
184 #[instrument(skip(self, matrix, rhs), fields(n = matrix.rows, nnz = matrix.nnz()))]
198 pub fn solve(
199 &self,
200 matrix: &CsrMatrix<f32>,
201 rhs: &[f32],
202 ) -> Result<SolverResult, SolverError> {
203 let start = Instant::now();
204 let n = matrix.rows;
205
206 if matrix.rows != matrix.cols {
210 return Err(SolverError::InvalidInput(
211 ValidationError::DimensionMismatch(format!(
212 "matrix must be square: got {}x{}",
213 matrix.rows, matrix.cols,
214 )),
215 ));
216 }
217
218 if rhs.len() != n {
219 return Err(SolverError::InvalidInput(
220 ValidationError::DimensionMismatch(format!(
221 "rhs length {} does not match matrix dimension {}",
222 rhs.len(),
223 n,
224 )),
225 ));
226 }
227
228 if n == 0 {
230 return Ok(SolverResult {
231 solution: Vec::new(),
232 iterations: 0,
233 residual_norm: 0.0,
234 wall_time: start.elapsed(),
235 convergence_history: Vec::new(),
236 algorithm: Algorithm::Neumann,
237 });
238 }
239
240 let d_inv = extract_diag_inv_f32(matrix);
243
244 let rho = Self::estimate_spectral_radius_with_diag(matrix, &d_inv);
248 if rho >= 1.0 {
249 warn!(rho, "spectral radius >= 1.0, Neumann series will diverge");
250 return Err(SolverError::SpectralRadiusExceeded {
251 spectral_radius: rho,
252 limit: 1.0,
253 algorithm: Algorithm::Neumann,
254 });
255 }
256 info!(rho, "spectral radius check passed");
257
258 let mut x: Vec<f32> = (0..n).map(|i| d_inv[i] * rhs[i]).collect();
272 let mut r = vec![0.0f32; n]; let mut convergence_history = Vec::with_capacity(self.max_iterations.min(256));
275 let mut prev_residual_norm = f64::MAX;
276 let final_residual_norm: f64;
277 let mut iterations_done: usize = 0;
278
279 for k in 0..self.max_iterations {
280 let residual_norm_sq = matrix.fused_residual_norm_sq(&x, rhs, &mut r);
282 let residual_norm = residual_norm_sq.sqrt();
283 iterations_done = k + 1;
284
285 convergence_history.push(ConvergenceInfo {
286 iteration: k,
287 residual_norm,
288 });
289
290 debug!(iteration = k, residual_norm, "neumann iteration");
291
292 if residual_norm < self.tolerance {
294 final_residual_norm = residual_norm;
295 info!(iterations = iterations_done, residual_norm, "converged");
296 return Ok(SolverResult {
297 solution: x,
298 iterations: iterations_done,
299 residual_norm: final_residual_norm,
300 wall_time: start.elapsed(),
301 convergence_history,
302 algorithm: Algorithm::Neumann,
303 });
304 }
305
306 if residual_norm.is_nan() || residual_norm.is_infinite() {
308 return Err(SolverError::NumericalInstability {
309 iteration: k,
310 detail: format!("residual became {residual_norm}"),
311 });
312 }
313
314 if k > 0
316 && prev_residual_norm < f64::MAX
317 && prev_residual_norm > 0.0
318 && residual_norm > INSTABILITY_GROWTH_FACTOR * prev_residual_norm
319 {
320 warn!(
321 iteration = k,
322 prev = prev_residual_norm,
323 current = residual_norm,
324 "residual diverging",
325 );
326 return Err(SolverError::NumericalInstability {
327 iteration: k,
328 detail: format!(
329 "residual grew from {prev_residual_norm:.6e} to \
330 {residual_norm:.6e} (>{INSTABILITY_GROWTH_FACTOR:.0}x)",
331 ),
332 });
333 }
334
335 let chunks = n / 4;
338 for c in 0..chunks {
339 let j = c * 4;
340 x[j] += d_inv[j] * r[j];
341 x[j + 1] += d_inv[j + 1] * r[j + 1];
342 x[j + 2] += d_inv[j + 2] * r[j + 2];
343 x[j + 3] += d_inv[j + 3] * r[j + 3];
344 }
345 for j in (chunks * 4)..n {
346 x[j] += d_inv[j] * r[j];
347 }
348
349 prev_residual_norm = residual_norm;
350 }
351
352 final_residual_norm = prev_residual_norm;
354 Err(SolverError::NonConvergence {
355 iterations: iterations_done,
356 residual: final_residual_norm,
357 tolerance: self.tolerance,
358 })
359 }
360}
361
362impl SolverEngine for NeumannSolver {
367 fn solve(
373 &self,
374 matrix: &CsrMatrix<f64>,
375 rhs: &[f64],
376 budget: &ComputeBudget,
377 ) -> Result<SolverResult, SolverError> {
378 let start = Instant::now();
379
380 for (i, &v) in matrix.values.iter().enumerate() {
382 if v.is_finite() && v.abs() > f32::MAX as f64 {
383 return Err(SolverError::InvalidInput(
384 ValidationError::NonFiniteValue(format!(
385 "matrix value at index {i} ({v:.6e}) overflows f32"
386 )),
387 ));
388 }
389 }
390 for (i, &v) in rhs.iter().enumerate() {
391 if v.is_finite() && v.abs() > f32::MAX as f64 {
392 return Err(SolverError::InvalidInput(
393 ValidationError::NonFiniteValue(format!(
394 "rhs value at index {i} ({v:.6e}) overflows f32"
395 )),
396 ));
397 }
398 }
399
400 let f32_matrix = CsrMatrix {
402 row_ptr: matrix.row_ptr.clone(),
403 col_indices: matrix.col_indices.clone(),
404 values: matrix.values.iter().map(|&v| v as f32).collect(),
405 rows: matrix.rows,
406 cols: matrix.cols,
407 };
408 let f32_rhs: Vec<f32> = rhs.iter().map(|&v| v as f32).collect();
409
410 let max_iters = self.max_iterations.min(budget.max_iterations);
414 let tol = self
415 .tolerance
416 .min(budget.tolerance)
417 .max(f32::EPSILON as f64 * 4.0);
418
419 let inner_solver = NeumannSolver::new(tol, max_iters);
420
421 let mut result = inner_solver.solve(&f32_matrix, &f32_rhs)?;
422
423 if start.elapsed() > budget.max_time {
425 return Err(SolverError::BudgetExhausted {
426 reason: "wall-clock time limit exceeded".to_string(),
427 elapsed: start.elapsed(),
428 });
429 }
430
431 result.wall_time = start.elapsed();
433 Ok(result)
434 }
435
436 fn estimate_complexity(
437 &self,
438 profile: &SparsityProfile,
439 n: usize,
440 ) -> ComplexityEstimate {
441 let rho = profile.estimated_spectral_radius.max(0.01).min(0.999);
443 let est_iters = ((1.0 / self.tolerance).ln() / (1.0 - rho).ln().abs())
444 .ceil() as usize;
445 let est_iters = est_iters.min(self.max_iterations).max(1);
446
447 ComplexityEstimate {
448 algorithm: Algorithm::Neumann,
449 estimated_flops: (est_iters as u64) * (profile.nnz as u64) * 2,
451 estimated_iterations: est_iters,
452 estimated_memory_bytes: n * 4 * 3,
454 complexity_class: ComplexityClass::SublinearNnz,
455 }
456 }
457
458 fn algorithm(&self) -> Algorithm {
459 Algorithm::Neumann
460 }
461}
462
463fn extract_diag_inv_f32(matrix: &CsrMatrix<f32>) -> Vec<f32> {
472 let n = matrix.rows;
473 let mut d_inv = vec![1.0f32; n];
474 for i in 0..n {
475 let start = matrix.row_ptr[i];
476 let end = matrix.row_ptr[i + 1];
477 for idx in start..end {
478 if matrix.col_indices[idx] == i {
479 let diag = matrix.values[idx];
480 if diag.abs() > 1e-15 {
481 d_inv[i] = 1.0 / diag;
482 } else {
483 warn!(
484 row = i,
485 diag_value = %diag,
486 "zero or near-zero diagonal entry; substituting 1.0 — matrix may be singular"
487 );
488 }
489 break;
490 }
491 }
492 }
493 d_inv
494}
495
496#[inline]
501fn l2_norm_f32(v: &[f32]) -> f32 {
502 let sum: f64 = v.iter().map(|&x| (x as f64) * (x as f64)).sum();
503 sum.sqrt() as f32
504}
505
506#[inline]
508fn scale_vec_f32(v: &mut [f32], s: f32) {
509 for x in v.iter_mut() {
510 *x *= s;
511 }
512}
513
514#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::types::CsrMatrix;
522
523 fn tridiag_f32(n: usize, diag_val: f32, off_val: f32) -> CsrMatrix<f32> {
525 let mut entries = Vec::new();
526 for i in 0..n {
527 entries.push((i, i, diag_val));
528 if i > 0 {
529 entries.push((i, i - 1, off_val));
530 }
531 if i + 1 < n {
532 entries.push((i, i + 1, off_val));
533 }
534 }
535 CsrMatrix::<f32>::from_coo(n, n, entries)
536 }
537
538 fn test_matrix_f64() -> CsrMatrix<f64> {
541 CsrMatrix::<f64>::from_coo(
542 3,
543 3,
544 vec![
545 (0, 0, 1.0),
546 (0, 1, -0.1),
547 (1, 0, -0.1),
548 (1, 1, 1.0),
549 (1, 2, -0.1),
550 (2, 1, -0.1),
551 (2, 2, 1.0),
552 ],
553 )
554 }
555
556 #[test]
557 fn test_new() {
558 let solver = NeumannSolver::new(1e-8, 100);
559 assert_eq!(solver.tolerance, 1e-8);
560 assert_eq!(solver.max_iterations, 100);
561 }
562
563 #[test]
564 fn test_spectral_radius_identity() {
565 let identity = CsrMatrix::<f32>::identity(4);
566 let rho = NeumannSolver::estimate_spectral_radius(&identity);
567 assert!(rho < 0.1, "expected rho ~ 0 for identity, got {rho}");
568 }
569
570 #[test]
571 fn test_spectral_radius_pure_diagonal() {
572 let a = CsrMatrix::<f32>::from_coo(
575 3, 3,
576 vec![(0, 0, 0.5_f32), (1, 1, 0.5), (2, 2, 0.5)],
577 );
578 let rho = NeumannSolver::estimate_spectral_radius(&a);
579 assert!(rho < 0.1, "expected rho ~ 0 for diagonal matrix, got {rho}");
580 }
581
582 #[test]
583 fn test_spectral_radius_empty() {
584 let empty = CsrMatrix::<f32> {
585 row_ptr: vec![0], col_indices: vec![], values: vec![],
586 rows: 0, cols: 0,
587 };
588 assert_eq!(NeumannSolver::estimate_spectral_radius(&empty), 0.0);
589 }
590
591 #[test]
592 fn test_spectral_radius_non_diag_dominant() {
593 let a = CsrMatrix::<f32>::from_coo(
599 2, 2,
600 vec![(0, 0, 1.0_f32), (0, 1, 2.0), (1, 0, 2.0), (1, 1, 1.0)],
601 );
602 let rho = NeumannSolver::estimate_spectral_radius(&a);
603 assert!(rho > 1.0, "expected rho > 1 for non-diag-dominant matrix, got {rho}");
604 }
605
606 #[test]
607 fn test_solve_identity() {
608 let identity = CsrMatrix::<f32>::identity(3);
609 let rhs = vec![1.0_f32, 2.0, 3.0];
610 let solver = NeumannSolver::new(1e-6, 100);
611 let result = solver.solve(&identity, &rhs).unwrap();
612 for (i, (&e, &a)) in rhs.iter().zip(result.solution.iter()).enumerate() {
613 assert!((e - a).abs() < 1e-4, "index {i}: expected {e}, got {a}");
614 }
615 assert!(result.residual_norm < 1e-6);
616 }
617
618 #[test]
619 fn test_solve_diagonal() {
620 let a = CsrMatrix::<f32>::from_coo(
621 3, 3,
622 vec![(0, 0, 0.5_f32), (1, 1, 0.5), (2, 2, 0.5)],
623 );
624 let rhs = vec![1.0_f32, 1.0, 1.0];
625 let solver = NeumannSolver::new(1e-6, 200);
626 let result = solver.solve(&a, &rhs).unwrap();
627 for (i, &val) in result.solution.iter().enumerate() {
628 assert!((val - 2.0).abs() < 0.01, "index {i}: expected ~2.0, got {val}");
629 }
630 }
631
632 #[test]
633 fn test_solve_tridiagonal() {
634 let a = tridiag_f32(5, 1.0, -0.1);
637 let rhs = vec![1.0_f32, 0.0, 1.0, 0.0, 1.0];
638 let solver = NeumannSolver::new(1e-6, 1000);
639 let result = solver.solve(&a, &rhs).unwrap();
640 assert!(result.residual_norm < 1e-4);
641 assert!(result.iterations > 0);
642 assert!(!result.convergence_history.is_empty());
643 }
644
645 #[test]
646 fn test_solve_empty_system() {
647 let a = CsrMatrix::<f32> {
648 row_ptr: vec![0], col_indices: vec![], values: vec![],
649 rows: 0, cols: 0,
650 };
651 let result = NeumannSolver::new(1e-6, 10).solve(&a, &[]).unwrap();
652 assert_eq!(result.iterations, 0);
653 assert!(result.solution.is_empty());
654 }
655
656 #[test]
657 fn test_solve_dimension_mismatch() {
658 let a = CsrMatrix::<f32>::identity(3);
659 let rhs = vec![1.0_f32, 2.0];
660 let err = NeumannSolver::new(1e-6, 100).solve(&a, &rhs).unwrap_err();
661 let msg = err.to_string();
662 assert!(msg.contains("dimension") || msg.contains("mismatch"), "got: {msg}");
663 }
664
665 #[test]
666 fn test_solve_non_square() {
667 let a = CsrMatrix::<f32>::from_coo(2, 3, vec![(0, 0, 1.0_f32), (1, 1, 1.0)]);
668 let rhs = vec![1.0_f32, 1.0];
669 let err = NeumannSolver::new(1e-6, 100).solve(&a, &rhs).unwrap_err();
670 let msg = err.to_string();
671 assert!(msg.contains("square") || msg.contains("dimension"), "got: {msg}");
672 }
673
674 #[test]
675 fn test_solve_divergent_matrix() {
676 let a = CsrMatrix::<f32>::from_coo(
678 2, 2,
679 vec![(0, 0, 1.0_f32), (0, 1, 2.0), (1, 0, 2.0), (1, 1, 1.0)],
680 );
681 let rhs = vec![1.0_f32, 1.0];
682 let err = NeumannSolver::new(1e-6, 100).solve(&a, &rhs).unwrap_err();
683 assert!(err.to_string().contains("spectral radius"), "got: {}", err);
684 }
685
686 #[test]
687 fn test_convergence_history_monotone() {
688 let a = CsrMatrix::<f32>::identity(4);
689 let rhs = vec![1.0_f32; 4];
690 let result = NeumannSolver::new(1e-10, 50).solve(&a, &rhs).unwrap();
691 assert!(!result.convergence_history.is_empty());
692 for window in result.convergence_history.windows(2) {
693 assert!(
694 window[1].residual_norm <= window[0].residual_norm + 1e-12,
695 "residual not decreasing: {} -> {}",
696 window[0].residual_norm, window[1].residual_norm,
697 );
698 }
699 }
700
701 #[test]
702 fn test_algorithm_tag() {
703 let a = CsrMatrix::<f32>::identity(2);
704 let rhs = vec![1.0_f32; 2];
705 let result = NeumannSolver::new(1e-6, 100).solve(&a, &rhs).unwrap();
706 assert_eq!(result.algorithm, Algorithm::Neumann);
707 }
708
709 #[test]
710 fn test_solver_engine_trait_f64() {
711 let solver = NeumannSolver::new(1e-6, 200);
712 let engine: &dyn SolverEngine = &solver;
713 let a = test_matrix_f64();
714 let rhs = vec![1.0_f64, 0.0, 1.0];
715 let budget = ComputeBudget::default();
716 let result = engine.solve(&a, &rhs, &budget).unwrap();
717 assert!(result.residual_norm < 1e-4);
718 assert_eq!(result.algorithm, Algorithm::Neumann);
719 }
720
721 #[test]
722 fn test_larger_system_accuracy() {
723 let n = 50;
724 let a = tridiag_f32(n, 1.0, -0.1);
727 let rhs: Vec<f32> = (0..n).map(|i| (i as f32 + 1.0) / n as f32).collect();
728 let result = NeumannSolver::new(1e-6, 2000).solve(&a, &rhs).unwrap();
729 assert!(result.residual_norm < 1e-6, "residual too large: {}", result.residual_norm);
730 let mut ax = vec![0.0f32; n];
731 a.spmv(&result.solution, &mut ax);
732 for i in 0..n {
733 assert!((ax[i] - rhs[i]).abs() < 1e-4, "A*x[{i}]={} but b[{i}]={}", ax[i], rhs[i]);
734 }
735 }
736
737 #[test]
738 fn test_scalar_system() {
739 let a = CsrMatrix::<f32>::from_coo(1, 1, vec![(0, 0, 0.5_f32)]);
740 let rhs = vec![4.0_f32];
741 let result = NeumannSolver::new(1e-8, 200).solve(&a, &rhs).unwrap();
742 assert!((result.solution[0] - 8.0).abs() < 0.01, "expected ~8.0, got {}", result.solution[0]);
743 }
744
745 #[test]
746 fn test_estimate_complexity() {
747 let solver = NeumannSolver::new(1e-6, 1000);
748 let profile = SparsityProfile {
749 rows: 100, cols: 100, nnz: 500, density: 0.05,
750 is_diag_dominant: true, estimated_spectral_radius: 0.5,
751 estimated_condition: 3.0, is_symmetric_structure: true,
752 avg_nnz_per_row: 5.0, max_nnz_per_row: 8,
753 };
754 let estimate = solver.estimate_complexity(&profile, 100);
755 assert_eq!(estimate.algorithm, Algorithm::Neumann);
756 assert!(estimate.estimated_flops > 0);
757 assert!(estimate.estimated_iterations > 0);
758 assert!(estimate.estimated_memory_bytes > 0);
759 assert_eq!(estimate.complexity_class, ComplexityClass::SublinearNnz);
760 }
761}