git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
250 lines
8.0 KiB
Rust
250 lines
8.0 KiB
Rust
//! Integration tests for the Conjugate Gradient (CG) solver.
|
|
//!
|
|
//! Tests cover correctness on SPD systems, Laplacian solves, preconditioning
|
|
//! benefits, known-solution verification, and tolerance scaling.
|
|
|
|
mod helpers;
|
|
|
|
use approx::assert_relative_eq;
|
|
use ruvector_solver::cg::ConjugateGradientSolver;
|
|
use ruvector_solver::traits::SolverEngine;
|
|
use ruvector_solver::types::{Algorithm, ComputeBudget, CsrMatrix};
|
|
|
|
use helpers::{
|
|
compute_residual, dense_solve, f32_to_f64, l2_norm, random_laplacian_csr, random_spd_csr,
|
|
random_vector, relative_error,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: default compute budget
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn default_budget() -> ComputeBudget {
|
|
ComputeBudget {
|
|
max_time: std::time::Duration::from_secs(30),
|
|
max_iterations: 10_000,
|
|
tolerance: 1e-12,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SPD system: solve and verify convergence
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_cg_spd_system() {
|
|
let n = 15;
|
|
let matrix = random_spd_csr(n, 0.4, 42);
|
|
let rhs = random_vector(n, 43);
|
|
let budget = default_budget();
|
|
|
|
let solver = ConjugateGradientSolver::new(1e-8, 500, false);
|
|
let result = solver.solve(&matrix, &rhs, &budget).unwrap();
|
|
|
|
assert_eq!(result.algorithm, Algorithm::CG);
|
|
assert!(
|
|
result.residual_norm < 1e-4,
|
|
"residual too large: {}",
|
|
result.residual_norm
|
|
);
|
|
|
|
// Independent residual check.
|
|
let x = f32_to_f64(&result.solution);
|
|
let residual = compute_residual(&matrix, &x, &rhs);
|
|
let resid_norm = l2_norm(&residual);
|
|
assert!(
|
|
resid_norm < 1e-3,
|
|
"independent residual check: {}",
|
|
resid_norm
|
|
);
|
|
|
|
// Compare with dense solve.
|
|
let exact = dense_solve(&matrix, &rhs);
|
|
let rel_err = relative_error(&x, &exact);
|
|
assert!(rel_err < 1e-2, "relative error vs dense solve: {}", rel_err);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Graph Laplacian system
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_cg_laplacian() {
|
|
let n = 12;
|
|
let laplacian = random_laplacian_csr(n, 0.3, 44);
|
|
|
|
// Laplacians are singular (L * ones = 0), so we add a small regulariser
|
|
// to make it SPD: A = L + epsilon * I.
|
|
let epsilon = 0.01;
|
|
let mut entries: Vec<(usize, usize, f64)> = Vec::new();
|
|
for i in 0..n {
|
|
let start = laplacian.row_ptr[i];
|
|
let end = laplacian.row_ptr[i + 1];
|
|
for idx in start..end {
|
|
let j = laplacian.col_indices[idx];
|
|
let mut v = laplacian.values[idx];
|
|
if i == j {
|
|
v += epsilon;
|
|
}
|
|
entries.push((i, j, v));
|
|
}
|
|
}
|
|
let reg_laplacian = CsrMatrix::<f64>::from_coo(n, n, entries);
|
|
|
|
let rhs = random_vector(n, 45);
|
|
let budget = default_budget();
|
|
|
|
let solver = ConjugateGradientSolver::new(1e-8, 1000, false);
|
|
let result = solver.solve(®_laplacian, &rhs, &budget).unwrap();
|
|
|
|
assert!(
|
|
result.residual_norm < 1e-4,
|
|
"laplacian solve residual: {}",
|
|
result.residual_norm
|
|
);
|
|
|
|
// Verify Ax = b.
|
|
let x = f32_to_f64(&result.solution);
|
|
let residual = compute_residual(®_laplacian, &x, &rhs);
|
|
let resid_norm = l2_norm(&residual);
|
|
assert!(
|
|
resid_norm < 1e-3,
|
|
"laplacian residual check: {}",
|
|
resid_norm
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Preconditioned CG reduces iterations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_cg_preconditioned() {
|
|
let n = 30;
|
|
let matrix = random_spd_csr(n, 0.3, 46);
|
|
let rhs = random_vector(n, 47);
|
|
let budget = default_budget();
|
|
|
|
let unprecond = ConjugateGradientSolver::new(1e-8, 1000, false);
|
|
let precond = ConjugateGradientSolver::new(1e-8, 1000, true);
|
|
|
|
let result_no = unprecond.solve(&matrix, &rhs, &budget).unwrap();
|
|
let result_yes = precond.solve(&matrix, &rhs, &budget).unwrap();
|
|
|
|
// Both should converge.
|
|
assert!(
|
|
result_no.residual_norm < 1e-4,
|
|
"unpreconditioned residual: {}",
|
|
result_no.residual_norm
|
|
);
|
|
assert!(
|
|
result_yes.residual_norm < 1e-4,
|
|
"preconditioned residual: {}",
|
|
result_yes.residual_norm
|
|
);
|
|
|
|
// Preconditioner should take <= iterations (it won't always be strictly
|
|
// fewer on well-conditioned systems, but should not take more).
|
|
assert!(
|
|
result_yes.iterations <= result_no.iterations + 2,
|
|
"preconditioned ({}) should not take much more than unpreconditioned ({})",
|
|
result_yes.iterations,
|
|
result_no.iterations
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Known solution verification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_cg_known_solution() {
|
|
// Diagonal system D*x = b => x_i = b_i / d_i
|
|
let diag_vals = vec![2.0, 5.0, 10.0, 1.0];
|
|
let n = diag_vals.len();
|
|
let entries: Vec<(usize, usize, f64)> = diag_vals
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &d)| (i, i, d))
|
|
.collect();
|
|
let matrix = CsrMatrix::<f64>::from_coo(n, n, entries);
|
|
|
|
let rhs = vec![4.0, 15.0, 30.0, 7.0];
|
|
let expected = vec![2.0, 3.0, 3.0, 7.0]; // b_i / d_i
|
|
|
|
let budget = default_budget();
|
|
let solver = ConjugateGradientSolver::new(1e-10, 100, false);
|
|
let result = solver.solve(&matrix, &rhs, &budget).unwrap();
|
|
|
|
let x = f32_to_f64(&result.solution);
|
|
for i in 0..n {
|
|
assert_relative_eq!(x[i], expected[i], epsilon = 1e-4);
|
|
}
|
|
|
|
// Also test a tridiagonal system with known answer.
|
|
// A = [4 -1 0] b = [3] => solve manually:
|
|
// [-1 4 -1] [2] x0 = (3 + x1)/4
|
|
// [0 -1 4] [3] x2 = (3 + x1)/4 => x0 = x2 (by symmetry)
|
|
// x1 = (2 + x0 + x2)/4 = (2 + 2*x0)/4
|
|
// From row 0: x0 = (3 + x1)/4
|
|
// From row 1: x1 = (2 + 2*x0)/4 = (1 + x0)/2
|
|
// Sub: x0 = (3 + (1+x0)/2)/4 = (3.5 + x0/2)/4 = 7/8 + x0/8
|
|
// => 7x0/8 = 7/8 => x0 = 1, x1 = 1, x2 = 1
|
|
let tri = CsrMatrix::<f64>::from_coo(
|
|
3,
|
|
3,
|
|
vec![
|
|
(0, 0, 4.0),
|
|
(0, 1, -1.0),
|
|
(1, 0, -1.0),
|
|
(1, 1, 4.0),
|
|
(1, 2, -1.0),
|
|
(2, 1, -1.0),
|
|
(2, 2, 4.0),
|
|
],
|
|
);
|
|
let rhs_tri = vec![3.0, 2.0, 3.0];
|
|
let result_tri = solver.solve(&tri, &rhs_tri, &budget).unwrap();
|
|
let x_tri = f32_to_f64(&result_tri.solution);
|
|
|
|
for i in 0..3 {
|
|
assert_relative_eq!(x_tri[i], 1.0, epsilon = 1e-4);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tolerance levels: accuracy scales with epsilon
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_cg_tolerance_levels() {
|
|
let n = 20;
|
|
let matrix = random_spd_csr(n, 0.3, 48);
|
|
let rhs = random_vector(n, 49);
|
|
let exact = dense_solve(&matrix, &rhs);
|
|
let budget = default_budget();
|
|
|
|
let tolerances = [1e-4, 1e-6, 1e-8, 1e-10];
|
|
let mut prev_error = f64::INFINITY;
|
|
|
|
for &tol in &tolerances {
|
|
let solver = ConjugateGradientSolver::new(tol, 5000, false);
|
|
let result = solver.solve(&matrix, &rhs, &budget).unwrap();
|
|
|
|
let x = f32_to_f64(&result.solution);
|
|
let rel_err = relative_error(&x, &exact);
|
|
|
|
// Error should generally decrease with tighter tolerance.
|
|
// Allow some slack for f32 precision limits at very tight tolerances.
|
|
assert!(
|
|
rel_err < tol.sqrt() * 100.0 || rel_err < prev_error * 10.0,
|
|
"tol={:.0e}: relative error {:.2e} is too large (prev={:.2e})",
|
|
tol,
|
|
rel_err,
|
|
prev_error
|
|
);
|
|
|
|
prev_error = rel_err;
|
|
}
|
|
}
|