//! Integration tests for the Neumann series solver. //! //! The Neumann solver solves Ax = b by iterating x_{k+1} = b + (I - A) x_k. //! Convergence requires spectral radius rho(I - A) < 1, which is guaranteed //! for diagonally dominant systems. mod helpers; use approx::assert_relative_eq; use ruvector_solver::error::SolverError; use ruvector_solver::neumann::NeumannSolver; use ruvector_solver::traits::SolverEngine; use ruvector_solver::types::{Algorithm, ComputeBudget, CsrMatrix}; use helpers::{ compute_residual, dense_solve, f32_to_f64, l2_norm, random_diag_dominant_csr, random_vector, relative_error, }; // --------------------------------------------------------------------------- // Helper: call solver via the SolverEngine trait (f64 interface) // --------------------------------------------------------------------------- fn solve_via_trait( solver: &NeumannSolver, matrix: &CsrMatrix, rhs: &[f64], budget: &ComputeBudget, ) -> Result { SolverEngine::solve(solver, matrix, rhs, budget) } // --------------------------------------------------------------------------- // Solve a diagonally dominant system, verify ||Ax - b|| < eps // --------------------------------------------------------------------------- #[test] fn test_neumann_diagonal_dominant() { let n = 20; let matrix = random_diag_dominant_csr(n, 0.3, 42); let rhs = random_vector(n, 43); let budget = ComputeBudget::default(); let solver = NeumannSolver::new(1e-8, 500); let result = solve_via_trait(&solver, &matrix, &rhs, &budget).unwrap(); assert!( result.residual_norm < 1e-6, "residual too large: {}", result.residual_norm ); // Double-check by computing residual independently. let x = f32_to_f64(&result.solution); let residual = compute_residual(&matrix, &x, &rhs); let resid_norm = l2_norm(&residual); assert!( resid_norm < 1e-4, "independent residual check failed: {}", resid_norm ); // Compare with dense solve. let exact = dense_solve(&matrix, &rhs); let rel_err = relative_error(&x, &exact); assert!(rel_err < 1e-3, "relative error vs dense solve: {}", rel_err); } // --------------------------------------------------------------------------- // Verify geometric convergence rate // --------------------------------------------------------------------------- #[test] fn test_neumann_convergence_rate() { let n = 15; let matrix = random_diag_dominant_csr(n, 0.2, 44); let rhs = random_vector(n, 45); let budget = ComputeBudget::default(); let solver = NeumannSolver::new(1e-12, 500); let result = solve_via_trait(&solver, &matrix, &rhs, &budget).unwrap(); // The convergence history should show monotonic decrease (geometric). let history = &result.convergence_history; assert!( history.len() >= 3, "need at least 3 iterations for rate check" ); // Check that residual decreases monotonically for at least the first // several iterations (allowing a small tolerance for floating point). let mut decreasing_count = 0; for w in history.windows(2) { if w[1].residual_norm < w[0].residual_norm * 1.01 { decreasing_count += 1; } } let decrease_ratio = decreasing_count as f64 / (history.len() - 1) as f64; assert!( decrease_ratio > 0.8, "expected mostly decreasing residuals, got {:.0}% decreasing", decrease_ratio * 100.0 ); // Estimate the convergence factor from the last few iterations. if history.len() >= 4 { let n_hist = history.len(); let r_late = history[n_hist - 1].residual_norm; let r_early = history[n_hist - 4].residual_norm; if r_early > 1e-15 { let avg_factor = (r_late / r_early).powf(1.0 / 3.0); assert!( avg_factor < 1.0, "convergence factor should be < 1, got {}", avg_factor ); } } } // --------------------------------------------------------------------------- // Verify rejection when spectral radius >= 1 // --------------------------------------------------------------------------- #[test] fn test_neumann_spectral_radius_check() { // Build a matrix where off-diagonal entries dominate the diagonal, // giving spectral radius >= 1. The matrix: // [1 2] // [2 1] // has off-diag sum > diag for each row. let matrix = CsrMatrix::::from_coo( 2, 2, vec![(0, 0, 1.0), (0, 1, 2.0), (1, 0, 2.0), (1, 1, 1.0)], ); let rhs = vec![1.0, 1.0]; let budget = ComputeBudget::default(); let solver = NeumannSolver::new(1e-8, 100); let result = solve_via_trait(&solver, &matrix, &rhs, &budget); match result { Err(SolverError::SpectralRadiusExceeded { spectral_radius, limit, algorithm, }) => { assert!(spectral_radius >= 1.0); assert_relative_eq!(limit, 1.0, epsilon = 1e-12); assert_eq!(algorithm, Algorithm::Neumann); } Err(SolverError::NumericalInstability { .. }) => { // Also acceptable: the solver might detect NaN divergence // before the spectral radius check catches it. } other => panic!( "expected SpectralRadiusExceeded or NumericalInstability, got {:?}", other ), } } // --------------------------------------------------------------------------- // Identity system: Ix = b should converge in 1 iteration // --------------------------------------------------------------------------- #[test] fn test_neumann_identity_system() { for n in [1, 5, 20] { let matrix = CsrMatrix::::identity(n); let rhs: Vec = (0..n).map(|i| (i as f64 + 1.0) * 0.3).collect(); let budget = ComputeBudget::default(); let solver = NeumannSolver::new(1e-10, 100); let result = solve_via_trait(&solver, &matrix, &rhs, &budget).unwrap(); // Solution should equal rhs for identity matrix. let x = f32_to_f64(&result.solution); for i in 0..n { assert_relative_eq!(x[i], rhs[i], epsilon = 1e-5); } // Should converge in exactly 1 iteration for identity. assert_eq!( result.iterations, 1, "identity system should converge in 1 iteration, got {}", result.iterations ); assert_eq!(result.algorithm, Algorithm::Neumann); } } // --------------------------------------------------------------------------- // Manually constructed system with known solution // --------------------------------------------------------------------------- #[test] fn test_neumann_known_solution() { // System: // [3 -1 0] [x0] [2] // [-1 3 -1] [x1] = [0] // [0 -1 3] [x2] [2] // // By symmetry, x0 = x2. From row 0: 3*x0 - x1 = 2. // From row 1: -x0 + 3*x1 - x2 = 0 => -2*x0 + 3*x1 = 0 => x1 = 2*x0/3. // Substituting: 3*x0 - 2*x0/3 = 2 => (9 - 2)/3 * x0 = 2 => x0 = 6/7. // x1 = 4/7, x2 = 6/7. let matrix = CsrMatrix::::from_coo( 3, 3, vec![ (0, 0, 3.0), (0, 1, -1.0), (1, 0, -1.0), (1, 1, 3.0), (1, 2, -1.0), (2, 1, -1.0), (2, 2, 3.0), ], ); let rhs = vec![2.0, 0.0, 2.0]; let budget = ComputeBudget::default(); let solver = NeumannSolver::new(1e-10, 500); let result = solve_via_trait(&solver, &matrix, &rhs, &budget).unwrap(); let x = f32_to_f64(&result.solution); let expected = [6.0 / 7.0, 4.0 / 7.0, 6.0 / 7.0]; for i in 0..3 { assert_relative_eq!(x[i], expected[i], epsilon = 1e-4); } }