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,
};
fn default_budget() -> ComputeBudget {
ComputeBudget {
max_time: std::time::Duration::from_secs(30),
max_iterations: 10_000,
tolerance: 1e-12,
}
}
#[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
);
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
);
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);
}
#[test]
fn test_cg_laplacian() {
let n = 12;
let laplacian = random_laplacian_csr(n, 0.3, 44);
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
);
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
);
}
#[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();
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
);
assert!(
result_yes.iterations <= result_no.iterations + 2,
"preconditioned ({}) should not take much more than unpreconditioned ({})",
result_yes.iterations,
result_no.iterations
);
}
#[test]
fn test_cg_known_solution() {
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];
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);
}
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);
}
}
#[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);
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;
}
}