use super::ConvergenceError;
use crate::algorithm::matrix::mul_vector;
use crate::algorithm::vector::dot;
use crate::scalar::Scalar;
use crate::storage::Storage;
#[allow(clippy::too_many_arguments)]
pub fn conjugate_gradient<S, T>(
a: &S,
n: usize,
b: &[T],
x0: &[T],
max_iter: usize,
tol: T,
out_x: &mut [T],
scratch_r: &mut [T],
scratch_p: &mut [T],
scratch_ap: &mut [T],
) -> Result<(), ConvergenceError>
where
S: Storage<Item = T>,
T: Scalar + PartialOrd,
{
if a.len() != n * n
|| b.len() != n
|| x0.len() != n
|| out_x.len() != n
|| scratch_r.len() != n
|| scratch_p.len() != n
|| scratch_ap.len() != n
{
return Err(ConvergenceError::DimensionMismatch);
}
for (i, slot) in out_x.iter_mut().enumerate() {
let Some(&x) = x0.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
*slot = x;
}
mul_vector(a, n, n, &Slice { data: out_x }, scratch_ap)
.map_err(|_| ConvergenceError::DimensionMismatch)?;
for (i, slot) in scratch_r.iter_mut().enumerate() {
let Some(&b_i) = b.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let Some(&ap_i) = scratch_ap.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
*slot = b_i.sub(ap_i);
}
let mut r_norm_sq = dot(&Slice { data: &*scratch_r }, &Slice { data: &*scratch_r })
.map_err(|_| ConvergenceError::DimensionMismatch)?;
let r_norm = r_norm_sq.sqrt();
if !(r_norm > T::zero() || r_norm == T::zero()) {
return Err(ConvergenceError::NonFinite);
}
if r_norm <= tol {
return Ok(());
}
for (i, slot) in scratch_p.iter_mut().enumerate() {
let Some(&r_i) = scratch_r.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
*slot = r_i;
}
for _ in 0..max_iter {
mul_vector(a, n, n, &Slice { data: &*scratch_p }, scratch_ap)
.map_err(|_| ConvergenceError::DimensionMismatch)?;
let p_ap = dot(&Slice { data: &*scratch_p }, &Slice { data: &*scratch_ap })
.map_err(|_| ConvergenceError::DimensionMismatch)?;
#[allow(clippy::neg_cmp_op_on_partial_ord)]
if !(p_ap > T::zero()) {
return Err(ConvergenceError::NonFinite);
}
let alpha = r_norm_sq.div(p_ap);
for i in 0..n {
let Some(&x_i) = out_x.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let Some(&p_i) = scratch_p.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let slot = out_x
.get_mut(i)
.ok_or(ConvergenceError::DimensionMismatch)?;
*slot = x_i.add(alpha.mul(p_i));
}
for i in 0..n {
let Some(&r_i) = scratch_r.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let Some(&ap_i) = scratch_ap.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let slot = scratch_r
.get_mut(i)
.ok_or(ConvergenceError::DimensionMismatch)?;
*slot = r_i.sub(alpha.mul(ap_i));
}
let new_r_norm_sq = dot(&Slice { data: &*scratch_r }, &Slice { data: &*scratch_r })
.map_err(|_| ConvergenceError::DimensionMismatch)?;
let new_r_norm = new_r_norm_sq.sqrt();
if !(new_r_norm > T::zero() || new_r_norm == T::zero()) {
return Err(ConvergenceError::NonFinite);
}
if new_r_norm <= tol {
return Ok(());
}
let beta = new_r_norm_sq.div(r_norm_sq);
for i in 0..n {
let Some(&r_i) = scratch_r.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let Some(&p_i) = scratch_p.get(i) else {
return Err(ConvergenceError::DimensionMismatch)?;
};
let slot = scratch_p
.get_mut(i)
.ok_or(ConvergenceError::DimensionMismatch)?;
*slot = r_i.add(beta.mul(p_i));
}
r_norm_sq = new_r_norm_sq;
}
Err(ConvergenceError::MaxIterationsExceeded)
}
struct Slice<'a, T> {
data: &'a [T],
}
impl<T> Storage for Slice<'_, T> {
type Item = T;
fn len(&self) -> usize {
self.data.len()
}
fn get(&self, index: usize) -> Option<&Self::Item> {
self.data.get(index)
}
}
#[cfg(test)]
mod tests {
use super::conjugate_gradient;
use crate::krylov::ConvergenceError;
use crate::storage::{StaticStorage, Storage};
fn assert_close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() < tol,
"expected {expected}, got {actual}"
);
}
#[test]
fn solves_known_2x2_spd_system() {
let a = StaticStorage::new([2.0_f64, 1.0, 1.0, 2.0]);
let b = [1.0, 2.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 0.0, 1e-8);
assert_close(x[1], 1.0, 1e-8);
}
#[test]
fn solves_diagonal_matrix() {
let a = StaticStorage::new([2.0_f64, 0.0, 0.0, 3.0]);
let b = [2.0, 6.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.0, 1e-10);
assert_close(x[1], 2.0, 1e-10);
}
#[test]
fn one_by_one_system() {
let a = StaticStorage::new([5.0_f64]);
let b = [10.0];
let x0 = [0.0];
let mut x = [0.0; 1];
let mut r = [0.0; 1];
let mut p = [0.0; 1];
let mut ap = [0.0; 1];
let result =
conjugate_gradient(&a, 1, &b, &x0, 100, 1e-12, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 2.0, 1e-12);
}
#[test]
fn ill_conditioned_matrix_converges_slowly() {
let a = StaticStorage::new([100.0_f64, 0.0, 0.0, 1.0]);
let b = [100.0, 1.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 1000, 1e-3, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.0, 1e-2);
assert_close(x[1], 1.0, 1e-2);
}
#[test]
fn negative_definite_matrix_is_detected_as_non_spd() {
let a = StaticStorage::new([-1.0_f64, 0.0, 0.0, -1.0]);
let b = [1.0, 1.0];
let x0 = [1.0, 1.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Err(ConvergenceError::NonFinite));
}
#[test]
fn indefinite_matrix_is_detected_as_non_spd() {
let a = StaticStorage::new([1.0_f64, 0.0, 0.0, -1.0]);
let b = [1.0, 1.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Err(ConvergenceError::NonFinite));
}
#[test]
fn early_convergence_on_initial_small_residual() {
let a = StaticStorage::new([1.0_f64, 0.0, 0.0, 1.0]);
let b = [1.0, 1.0];
let x0 = [1.0, 1.0]; let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.0, 1e-10);
assert_close(x[1], 1.0, 1e-10);
}
#[test]
fn mismatched_dimensions_is_an_error() {
let a = StaticStorage::new([2.0_f64, 1.0, 1.0, 2.0]);
let b = [1.0]; let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Err(ConvergenceError::DimensionMismatch));
}
#[test]
fn output_buffer_too_short_is_an_error() {
let a = StaticStorage::new([2.0_f64, 1.0, 1.0, 2.0]);
let b = [1.0, 2.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 1]; let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Err(ConvergenceError::DimensionMismatch));
}
#[test]
fn max_iterations_exceeded_on_slow_convergence() {
let a = StaticStorage::new([1000.0_f64, 0.0, 0.0, 1.0]);
let b = [1000.0, 1.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result = conjugate_gradient(&a, 2, &b, &x0, 2, 1e-15, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Err(ConvergenceError::MaxIterationsExceeded));
}
#[test]
fn converges_with_nonzero_initial_guess() {
let a = StaticStorage::new([2.0_f64, 1.0, 1.0, 2.0]);
let b = [1.0, 2.0];
let x0 = [0.5, 0.5]; let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 0.0, 1e-8);
assert_close(x[1], 1.0, 1e-8);
}
#[test]
fn larger_3x3_spd_matrix() {
let a = StaticStorage::new([4.0_f64, 1.0, 1.0, 1.0, 3.0, 0.0, 1.0, 0.0, 2.0]);
let b = [6.0, 4.0, 3.0];
let x0 = [0.0, 0.0, 0.0];
let mut x = [0.0; 3];
let mut r = [0.0; 3];
let mut p = [0.0; 3];
let mut ap = [0.0; 3];
let result =
conjugate_gradient(&a, 3, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
let mut ax = [0.0; 3];
for (i, ax_i) in ax.iter_mut().enumerate() {
*ax_i = 0.0;
for (j, x_j) in x.iter().enumerate() {
*ax_i += a.get(i * 3 + j).unwrap_or(&0.0) * x_j;
}
}
for (ax_i, b_i) in ax.iter().zip(b.iter()) {
assert_close(*ax_i, *b_i, 1e-8);
}
}
#[test]
fn repeated_eigenvalues_still_converges() {
let a = StaticStorage::new([2.0_f64, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0]);
let b = [2.0, 4.0, 1.0];
let x0 = [0.0, 0.0, 0.0];
let mut x = [0.0; 3];
let mut r = [0.0; 3];
let mut p = [0.0; 3];
let mut ap = [0.0; 3];
let result =
conjugate_gradient(&a, 3, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.0, 1e-8);
assert_close(x[1], 2.0, 1e-8);
assert_close(x[2], 1.0, 1e-8);
}
#[test]
fn ill_conditioned_spd_matrix_still_solves() {
let a = StaticStorage::new([0.01_f64, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0, 1.0]);
let b = [0.01, 0.01, 1.0];
let x0 = [0.0, 0.0, 0.0];
let mut x = [0.0; 3];
let mut r = [0.0; 3];
let mut p = [0.0; 3];
let mut ap = [0.0; 3];
let result =
conjugate_gradient(&a, 3, &b, &x0, 1000, 1e-8, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.0, 1e-6);
assert_close(x[1], 1.0, 1e-6);
assert_close(x[2], 1.0, 1e-6);
}
#[test]
fn identity_matrix_converges_immediately() {
let a = StaticStorage::new([1.0_f64, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
let b = [1.5, 2.5, 3.5];
let x0 = [0.0, 0.0, 0.0];
let mut x = [0.0; 3];
let mut r = [0.0; 3];
let mut p = [0.0; 3];
let mut ap = [0.0; 3];
let result =
conjugate_gradient(&a, 3, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.5, 1e-10);
assert_close(x[1], 2.5, 1e-10);
assert_close(x[2], 3.5, 1e-10);
}
#[test]
fn negative_rhs_still_works() {
let a = StaticStorage::new([2.0_f64, 1.0, 1.0, 2.0]);
let b = [-1.0, -2.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 0.0, 1e-8);
assert_close(x[1], -1.0, 1e-8);
}
#[test]
fn sparse_system_with_dominant_diagonal() {
let a = StaticStorage::new([100.0_f64, 1.0, 1.0, 100.0]);
let b = [101.0, 101.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result = conjugate_gradient(&a, 2, &b, &x0, 10, 1e-10, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.0, 1e-8);
assert_close(x[1], 1.0, 1e-8);
}
#[test]
fn residual_decreases_monotonically_for_spd() {
let a = StaticStorage::new([3.0_f64, 1.0, 1.0, 3.0]);
let b = [4.0, 4.0];
let x0 = [0.0, 0.0];
let mut x = [0.0; 2];
let mut r = [0.0; 2];
let mut p = [0.0; 2];
let mut ap = [0.0; 2];
let result =
conjugate_gradient(&a, 2, &b, &x0, 100, 1e-12, &mut x, &mut r, &mut p, &mut ap);
assert_eq!(result, Ok(()));
assert_close(x[0], 1.0, 1e-10);
assert_close(x[1], 1.0, 1e-10);
let mut final_r = [0.0; 2];
let mut ax = [0.0; 2];
for i in 0..2 {
ax[i] = a.get(i * 2).unwrap_or(&0.0) * x[0] + a.get(i * 2 + 1).unwrap_or(&0.0) * x[1];
final_r[i] = b[i] - ax[i];
}
let residual_norm_sq = final_r[0] * final_r[0] + final_r[1] * final_r[1];
assert!(residual_norm_sq < 1e-20);
}
}