use crate::error::{all_finite, tolerance, NumericError, NumericResult, Status};
#[derive(Debug, Clone, PartialEq)]
pub struct SparseMatrix {
rows: usize,
cols: usize,
row_start: Vec<usize>,
columns: Vec<usize>,
values: Vec<f64>,
}
impl SparseMatrix {
pub fn from_triplets(
rows: usize,
cols: usize,
triplets: &[(usize, usize, f64)],
) -> NumericResult<Self> {
let mut order: Vec<usize> = (0..triplets.len()).collect();
for &(i, j, v) in triplets {
if i >= rows || j >= cols {
return Err(NumericError::InvalidArgument {
name: "triplet",
reason: "must lie inside the matrix shape",
});
}
if !v.is_finite() {
return Err(NumericError::NonFiniteInput {
name: "triplet value",
});
}
}
order.sort_by_key(|&k| (triplets[k].0, triplets[k].1));
let mut row_start = vec![0usize; rows + 1];
let mut columns: Vec<usize> = Vec::with_capacity(triplets.len());
let mut values: Vec<f64> = Vec::with_capacity(triplets.len());
let mut last: Option<(usize, usize)> = None;
for k in order {
let (i, j, v) = triplets[k];
if last == Some((i, j)) {
*values.last_mut().expect("an entry precedes its duplicate") += v;
} else {
columns.push(j);
values.push(v);
row_start[i + 1] += 1;
last = Some((i, j));
}
}
for i in 0..rows {
row_start[i + 1] += row_start[i];
}
Ok(Self {
rows,
cols,
row_start,
columns,
values,
})
}
#[must_use]
pub fn rows(&self) -> usize {
self.rows
}
#[must_use]
pub fn cols(&self) -> usize {
self.cols
}
#[must_use]
pub fn nnz(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn get(&self, i: usize, j: usize) -> f64 {
assert!(i < self.rows && j < self.cols, "matrix index out of range");
let (cols, vals) = self.row(i);
cols.binary_search(&j).map_or(0.0, |k| vals[k])
}
#[must_use]
pub fn row(&self, i: usize) -> (&[usize], &[f64]) {
let range = self.row_start[i]..self.row_start[i + 1];
(&self.columns[range.clone()], &self.values[range])
}
pub fn mul_vec(&self, x: &[f64]) -> NumericResult<Vec<f64>> {
if x.len() != self.cols {
return Err(NumericError::DimensionMismatch {
name: "vector",
expected: self.cols,
found: x.len(),
});
}
let mut y = vec![0.0; self.rows];
self.mul_into(x, &mut y);
Ok(y)
}
fn mul_into(&self, x: &[f64], y: &mut [f64]) {
for (i, out) in y.iter_mut().enumerate() {
let (cols, vals) = self.row(i);
*out = cols.iter().zip(vals).map(|(&j, &v)| v * x[j]).sum();
}
}
fn require_symmetric(&self) -> NumericResult<()> {
if self.rows != self.cols {
return Err(NumericError::DimensionMismatch {
name: "symmetric matrix",
expected: self.rows,
found: self.cols,
});
}
for i in 0..self.rows {
let (cols, vals) = self.row(i);
for (&j, &v) in cols.iter().zip(vals) {
if j <= i {
continue;
}
let w = self.get(j, i);
if (v - w).abs() > 16.0 * f64::EPSILON * v.abs().max(w.abs()) {
return Err(NumericError::NotSymmetric { row: i, column: j });
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConjugateGradientOptions {
pub relative_tolerance: f64,
pub max_iterations: Option<usize>,
}
impl Default for ConjugateGradientOptions {
fn default() -> Self {
Self {
relative_tolerance: 1e-12,
max_iterations: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[must_use]
pub struct ConjugateGradientSolution {
pub x: Vec<f64>,
pub relative_residual: f64,
pub iterations: usize,
pub status: Status,
}
impl ConjugateGradientSolution {
pub fn converged(self) -> NumericResult<Self> {
if self.status == Status::Converged {
Ok(self)
} else {
Err(NumericError::NotConverged {
name: "conjugate gradient",
error_estimate: self.relative_residual,
})
}
}
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
fn norm2(a: &[f64]) -> f64 {
dot(a, a).sqrt()
}
pub fn conjugate_gradient(
a: &SparseMatrix,
b: &[f64],
initial: Option<&[f64]>,
options: ConjugateGradientOptions,
) -> NumericResult<ConjugateGradientSolution> {
a.require_symmetric()?;
let n = a.rows;
if b.len() != n {
return Err(NumericError::DimensionMismatch {
name: "right-hand side",
expected: n,
found: b.len(),
});
}
all_finite(b, "right-hand side")?;
let relative_tolerance = tolerance(options.relative_tolerance, "relative tolerance")?;
let mut x = match initial {
Some(x0) => {
if x0.len() != n {
return Err(NumericError::DimensionMismatch {
name: "initial guess",
expected: n,
found: x0.len(),
});
}
all_finite(x0, "initial guess")?;
x0.to_vec()
}
None => vec![0.0; n],
};
let mut inverse_diagonal = Vec::with_capacity(n);
for i in 0..n {
let d = a.get(i, i);
if d <= 0.0 {
return Err(NumericError::NotPositiveDefinite { pivot: i });
}
inverse_diagonal.push(1.0 / d);
}
let b_norm = norm2(b);
if b_norm == 0.0 {
return Ok(ConjugateGradientSolution {
x: vec![0.0; n],
relative_residual: 0.0,
iterations: 0,
status: Status::Converged,
});
}
let target = relative_tolerance * b_norm;
let budget = options.max_iterations.unwrap_or(10 * n + 100);
let mut ax = vec![0.0; n];
let true_residual = |x: &[f64], ax: &mut Vec<f64>, r: &mut Vec<f64>| {
a.mul_into(x, ax);
for i in 0..n {
r[i] = b[i] - ax[i];
}
norm2(r)
};
let mut r = vec![0.0; n];
let mut r_norm = true_residual(&x, &mut ax, &mut r);
let mut z: Vec<f64> = r
.iter()
.zip(&inverse_diagonal)
.map(|(r, d)| r * d)
.collect();
let mut p = z.clone();
let mut rz = dot(&r, &z);
let mut ap = vec![0.0; n];
let mut iterations = 0;
loop {
if r_norm <= target {
r_norm = true_residual(&x, &mut ax, &mut r);
if r_norm <= target {
return Ok(ConjugateGradientSolution {
x,
relative_residual: r_norm / b_norm,
iterations,
status: Status::Converged,
});
}
for i in 0..n {
z[i] = r[i] * inverse_diagonal[i];
}
p.clone_from(&z);
rz = dot(&r, &z);
}
if iterations >= budget {
r_norm = true_residual(&x, &mut ax, &mut r);
let status = if r_norm <= target {
Status::Converged
} else {
Status::BudgetExhausted
};
return Ok(ConjugateGradientSolution {
x,
relative_residual: r_norm / b_norm,
iterations,
status,
});
}
a.mul_into(&p, &mut ap);
let curvature = dot(&p, &ap);
if curvature <= 0.0 {
return Err(NumericError::NotPositiveDefinite { pivot: iterations });
}
let alpha = rz / curvature;
for i in 0..n {
x[i] += alpha * p[i];
r[i] -= alpha * ap[i];
}
r_norm = norm2(&r);
for i in 0..n {
z[i] = r[i] * inverse_diagonal[i];
}
let rz_next = dot(&r, &z);
let beta = rz_next / rz;
rz = rz_next;
for i in 0..n {
p[i] = z[i] + beta * p[i];
}
iterations += 1;
}
}