use crate::krylov::{KrylovResult, KrylovSettings};
use crate::ldu_matrix::LduMatrix;
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be used as a Krylov system matrix: it does not implement `LinearOperator`",
label = "not a linear operator",
note = "implemented by `outram_foam_basic_lib::ldu_matrix::LduMatrix` (finite volume) and `farrer_park::sparse::CsrMatrix` (finite element)",
note = "to add your own, implement `n_rows`, `n_cols`, `apply` and `diagonal`"
)]
pub trait LinearOperator {
fn n_rows(&self) -> usize;
fn n_cols(&self) -> usize;
fn apply(&self, x: &[f64], y: &mut [f64]);
fn diagonal(&self, d: &mut [f64]);
fn residual(&self, x: &[f64], b: &[f64], r: &mut [f64]) {
self.apply(x, r);
for i in 0..r.len() {
r[i] = b[i] - r[i];
}
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be used as a Krylov preconditioner: it does not implement `OperatorPreconditioner`",
label = "not a preconditioner",
note = "implemented by `IdentityPreconditioner`, `JacobiOperatorPreconditioner`, and `outram_foam_basic_lib::krylov::Preconditioner`",
note = "pass `&IdentityPreconditioner` for an unpreconditioned solve"
)]
pub trait OperatorPreconditioner {
fn apply(&self, r: &[f64], z: &mut [f64]);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IdentityPreconditioner;
impl OperatorPreconditioner for IdentityPreconditioner {
fn apply(&self, r: &[f64], z: &mut [f64]) {
z.copy_from_slice(r);
}
}
#[derive(Debug, Clone)]
pub struct JacobiOperatorPreconditioner {
inv_diag: Vec<f64>,
}
impl JacobiOperatorPreconditioner {
pub fn new<A: LinearOperator + ?Sized>(a: &A) -> Self {
let n = a.n_rows();
let mut d = vec![0.0; n];
a.diagonal(&mut d);
for v in d.iter_mut() {
*v = if v.abs() > 1.0e-300 { 1.0 / *v } else { 1.0 };
}
Self { inv_diag: d }
}
pub fn reciprocal_diagonal(&self) -> &[f64] {
&self.inv_diag
}
}
impl OperatorPreconditioner for JacobiOperatorPreconditioner {
fn apply(&self, r: &[f64], z: &mut [f64]) {
for i in 0..z.len() {
z[i] = r[i] * self.inv_diag[i];
}
}
}
impl LinearOperator for LduMatrix {
fn n_rows(&self) -> usize {
self.n_cells
}
fn n_cols(&self) -> usize {
self.n_cells
}
fn apply(&self, x: &[f64], y: &mut [f64]) {
let prod = self.multiply(x);
y.copy_from_slice(&prod);
}
fn diagonal(&self, d: &mut [f64]) {
d.copy_from_slice(&self.diag);
}
fn residual(&self, x: &[f64], b: &[f64], r: &mut [f64]) {
let res = LduMatrix::residual(self, x, b);
r.copy_from_slice(&res);
}
}
impl OperatorPreconditioner for crate::krylov::Preconditioner {
fn apply(&self, r: &[f64], z: &mut [f64]) {
crate::krylov::Preconditioner::apply(self, r, z)
}
}
fn norm2(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn axpy(alpha: f64, x: &[f64], y: &mut [f64]) {
for i in 0..y.len() {
y[i] += alpha * x[i];
}
}
fn zero_rhs_result(n: usize) -> (Vec<f64>, KrylovResult) {
(
vec![0.0; n],
KrylovResult {
n_iterations: 0,
final_residual: 0.0,
converged: true,
},
)
}
fn true_relative_residual<A: LinearOperator + ?Sized>(
a: &A,
x: &[f64],
b: &[f64],
bnorm: f64,
work: &mut [f64],
) -> f64 {
a.residual(x, b, work);
norm2(work) / bnorm
}
pub fn cg_op<A, M>(
a: &A,
b: &[f64],
x0: Option<&[f64]>,
precond: &M,
settings: &KrylovSettings,
) -> (Vec<f64>, KrylovResult)
where
A: LinearOperator + ?Sized,
M: OperatorPreconditioner + ?Sized,
{
let n = a.n_rows();
debug_assert_eq!(b.len(), n, "cg_op: rhs length mismatch");
let bnorm = norm2(b);
if bnorm == 0.0 {
return zero_rhs_result(n);
}
let mut x = match x0 {
Some(g) => g.to_vec(),
None => vec![0.0; n],
};
let mut r = vec![0.0; n];
a.residual(&x, b, &mut r);
let mut rel = norm2(&r) / bnorm;
if rel <= settings.tolerance {
return (
x,
KrylovResult {
n_iterations: 0,
final_residual: rel,
converged: true,
},
);
}
let mut z = vec![0.0; n];
precond.apply(&r, &mut z);
let mut p = z.clone();
let mut rz = dot(&r, &z);
let mut ap = vec![0.0; n];
let mut n_iter = 0usize;
let mut converged = false;
for iter in 0..settings.max_iter {
a.apply(&p, &mut ap);
let pap = dot(&p, &ap);
if !pap.is_finite() || pap.abs() < 1.0e-300 {
n_iter = iter;
break;
}
let alpha = rz / pap;
axpy(alpha, &p, &mut x);
axpy(-alpha, &ap, &mut r);
rel = norm2(&r) / bnorm;
n_iter = iter + 1;
if !rel.is_finite() {
break;
}
if rel <= settings.tolerance {
converged = true;
break;
}
precond.apply(&r, &mut z);
let rz_new = dot(&r, &z);
if rz.abs() < 1.0e-300 {
break;
}
let beta = rz_new / rz;
rz = rz_new;
for i in 0..n {
p[i] = z[i] + beta * p[i];
}
}
let final_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
let final_rel = if final_rel.is_finite() {
final_rel
} else {
f64::INFINITY
};
(
x,
KrylovResult {
n_iterations: n_iter,
final_residual: final_rel,
converged: converged && final_rel <= settings.tolerance,
},
)
}
pub fn bicgstab_op<A, M>(
a: &A,
b: &[f64],
x0: Option<&[f64]>,
precond: &M,
settings: &KrylovSettings,
) -> (Vec<f64>, KrylovResult)
where
A: LinearOperator + ?Sized,
M: OperatorPreconditioner + ?Sized,
{
let n = a.n_rows();
debug_assert_eq!(b.len(), n, "bicgstab_op: rhs length mismatch");
let bnorm = norm2(b);
if bnorm == 0.0 {
return zero_rhs_result(n);
}
let mut x = match x0 {
Some(g) => g.to_vec(),
None => vec![0.0; n],
};
let mut r = vec![0.0; n];
a.residual(&x, b, &mut r);
let r0 = r.clone();
let mut rho = 1.0;
let mut alpha = 1.0;
let mut omega = 1.0;
let mut p = vec![0.0; n];
let mut v = vec![0.0; n];
let mut y = vec![0.0; n];
let mut z = vec![0.0; n];
let mut s = vec![0.0; n];
let mut t = vec![0.0; n];
let mut n_iter = 0usize;
let mut converged = norm2(&r) / bnorm <= settings.tolerance;
if !converged {
for iter in 0..settings.max_iter {
n_iter = iter + 1;
let rho_new = dot(&r0, &r);
if rho_new.abs() < 1.0e-300 || !rho_new.is_finite() {
break; }
let beta = (rho_new / rho) * (alpha / omega);
rho = rho_new;
for i in 0..n {
p[i] = r[i] + beta * (p[i] - omega * v[i]);
}
precond.apply(&p, &mut y);
a.apply(&y, &mut v);
let r0v = dot(&r0, &v);
if r0v.abs() < 1.0e-300 || !r0v.is_finite() {
break;
}
alpha = rho / r0v;
for i in 0..n {
s[i] = r[i] - alpha * v[i];
}
axpy(alpha, &y, &mut x);
if norm2(&s) / bnorm <= settings.tolerance {
converged = true;
break;
}
precond.apply(&s, &mut z);
a.apply(&z, &mut t);
let tt = dot(&t, &t);
if tt.abs() < 1.0e-300 || !tt.is_finite() {
break;
}
omega = dot(&t, &s) / tt;
axpy(omega, &z, &mut x);
for i in 0..n {
r[i] = s[i] - omega * t[i];
}
let rel = norm2(&r) / bnorm;
if !rel.is_finite() {
break;
}
if rel <= settings.tolerance {
converged = true;
break;
}
if omega.abs() < 1.0e-300 {
break;
}
}
}
let final_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
let final_rel = if final_rel.is_finite() {
final_rel
} else {
f64::INFINITY
};
(
x,
KrylovResult {
n_iterations: n_iter,
final_residual: final_rel,
converged: converged && final_rel <= settings.tolerance,
},
)
}
pub fn gmres_op<A, M>(
a: &A,
b: &[f64],
x0: Option<&[f64]>,
precond: &M,
settings: &KrylovSettings,
) -> (Vec<f64>, KrylovResult)
where
A: LinearOperator + ?Sized,
M: OperatorPreconditioner + ?Sized,
{
const HAPPY_TOL: f64 = 1.0e-300;
let n = a.n_rows();
debug_assert_eq!(b.len(), n, "gmres_op: rhs length mismatch");
let bnorm = norm2(b);
if bnorm == 0.0 {
return zero_rhs_result(n);
}
let m = if settings.restart == 0 {
settings.max_iter.max(1)
} else {
settings.restart
};
let mut x = match x0 {
Some(g) => g.to_vec(),
None => vec![0.0; n],
};
let tol = settings.tolerance;
let mut total_iters = 0usize;
let mut converged = false;
let mut mz = vec![0.0; n];
let mut r = vec![0.0; n];
while total_iters < settings.max_iter {
a.residual(&x, b, &mut r);
let beta = norm2(&r);
if beta / bnorm <= tol {
converged = true;
break;
}
let mut v: Vec<Vec<f64>> = Vec::with_capacity(m + 1);
let mut v0 = r.clone();
for e in v0.iter_mut() {
*e /= beta;
}
v.push(v0);
let mut cs = vec![0.0f64; m];
let mut sn = vec![0.0f64; m];
let mut g = vec![0.0f64; m + 1];
g[0] = beta;
let mut hcols: Vec<Vec<f64>> = Vec::with_capacity(m);
let mut k = 0usize;
for j in 0..m {
if total_iters >= settings.max_iter {
break;
}
total_iters += 1;
precond.apply(&v[j], &mut mz);
let mut w = vec![0.0; n];
a.apply(&mz, &mut w);
let mut hcol = vec![0.0f64; j + 2];
for i in 0..=j {
let hij = dot(&w, &v[i]);
hcol[i] = hij;
axpy(-hij, &v[i], &mut w);
}
let hnext = norm2(&w);
hcol[j + 1] = hnext;
for i in 0..j {
let temp = cs[i] * hcol[i] + sn[i] * hcol[i + 1];
hcol[i + 1] = -sn[i] * hcol[i] + cs[i] * hcol[i + 1];
hcol[i] = temp;
}
let (c, s) = {
let (h1, h2) = (hcol[j], hcol[j + 1]);
if h2 == 0.0 {
(1.0, 0.0)
} else {
let den = (h1 * h1 + h2 * h2).sqrt();
(h1 / den, h2 / den)
}
};
cs[j] = c;
sn[j] = s;
hcol[j] = c * hcol[j] + s * hcol[j + 1];
hcol[j + 1] = 0.0;
let g_temp = c * g[j] + s * g[j + 1];
g[j + 1] = -s * g[j] + c * g[j + 1];
g[j] = g_temp;
hcols.push(hcol);
k = j + 1;
let resid = g[j + 1].abs() / bnorm;
if hnext > HAPPY_TOL {
let mut vnew = w;
for e in vnew.iter_mut() {
*e /= hnext;
}
v.push(vnew);
}
if resid <= tol || hnext <= HAPPY_TOL {
break;
}
}
let mut y = vec![0.0f64; k];
for i in (0..k).rev() {
let mut sum = g[i];
for l in (i + 1)..k {
sum -= hcols[l][i] * y[l];
}
let diag = hcols[i][i];
y[i] = if diag.abs() > HAPPY_TOL {
sum / diag
} else {
0.0
};
}
let mut zc = vec![0.0f64; n];
for i in 0..k {
axpy(y[i], &v[i], &mut zc);
}
precond.apply(&zc, &mut mz);
axpy(1.0, &mz, &mut x);
let true_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
if !true_rel.is_finite() {
break;
}
if true_rel <= tol {
converged = true;
break;
}
}
let final_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
let final_rel = if final_rel.is_finite() {
final_rel
} else {
f64::INFINITY
};
(
x,
KrylovResult {
n_iterations: total_iters,
final_residual: final_rel,
converged: converged && final_rel <= tol,
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::krylov::{bicgstab, gmres, Preconditioner};
fn spd_tridiag(n: usize, d: f64, e: f64) -> LduMatrix {
let owner: Vec<usize> = (0..n - 1).collect();
let neigh: Vec<usize> = (1..n).collect();
let mut a = LduMatrix::new(n, owner, neigh);
a.diag = vec![d; n];
a.lower = vec![e; n - 1];
a.upper = vec![e; n - 1];
a
}
fn nonsym_tridiag(n: usize, d: f64, lo: f64, up: f64) -> LduMatrix {
let owner: Vec<usize> = (0..n - 1).collect();
let neigh: Vec<usize> = (1..n).collect();
let mut a = LduMatrix::new(n, owner, neigh);
a.diag = vec![d; n];
a.lower = vec![lo; n - 1];
a.upper = vec![up; n - 1];
a
}
fn rel_err(x: &[f64], y: &[f64]) -> f64 {
let num: f64 = x.iter().zip(y).map(|(a, b)| (a - b) * (a - b)).sum();
let den: f64 = y.iter().map(|b| b * b).sum();
(num / den).sqrt()
}
#[test]
fn ldu_operator_apply_matches_multiply_bitwise() {
let a = nonsym_tridiag(7, 4.0, -1.0, -2.0);
let x: Vec<f64> = (0..7).map(|i| 0.3 * i as f64 - 1.1).collect();
let direct = a.multiply(&x);
let mut via = vec![0.0; 7];
LinearOperator::apply(&a, &x, &mut via);
assert_eq!(direct, via);
let b: Vec<f64> = (0..7).map(|i| i as f64).collect();
let direct_r = LduMatrix::residual(&a, &x, &b);
let mut via_r = vec![0.0; 7];
LinearOperator::residual(&a, &x, &b, &mut via_r);
assert_eq!(direct_r, via_r);
}
#[test]
fn cg_op_spd_known_solution() {
let n = 25;
let a = spd_tridiag(n, 2.0, -1.0);
let x_true: Vec<f64> = (0..n).map(|i| ((i + 1) as f64).sqrt()).collect();
let b = a.multiply(&x_true);
let s = KrylovSettings {
tolerance: 1e-12,
max_iter: 500,
restart: 30,
};
let m = JacobiOperatorPreconditioner::new(&a);
let (x, r) = cg_op(&a, &b, None, &m, &s);
assert!(r.converged, "cg_op did not converge: {:?}", r);
assert!(
rel_err(&x, &x_true) < 1e-9,
"rel err {}",
rel_err(&x, &x_true)
);
}
#[test]
fn generic_drivers_agree_with_existing_fv_solvers() {
let n = 30;
let a = nonsym_tridiag(n, 4.0, -1.0, -2.0);
let b: Vec<f64> = (0..n).map(|i| (i as f64) - 12.5).collect();
let s = KrylovSettings {
tolerance: 1e-10,
max_iter: 500,
restart: 30,
};
let pc = Preconditioner::ilu0(&a);
let (x_old_b, r_old_b) = bicgstab(&a, &b, None, &pc, &s);
let (x_new_b, r_new_b) = bicgstab_op(&a, &b, None, &pc, &s);
assert!(r_old_b.converged && r_new_b.converged);
assert!(rel_err(&x_new_b, &x_old_b) < 1e-8);
let (x_old_g, r_old_g) = gmres(&a, &b, None, &pc, &s);
let (x_new_g, r_new_g) = gmres_op(&a, &b, None, &pc, &s);
assert!(r_old_g.converged && r_new_g.converged);
assert!(rel_err(&x_new_g, &x_old_g) < 1e-8);
}
#[test]
fn zero_rhs_shortcircuits() {
let a = spd_tridiag(6, 3.0, -1.0);
let b = vec![0.0; 6];
let s = KrylovSettings::default();
let m = IdentityPreconditioner;
for (x, r) in [
cg_op(&a, &b, None, &m, &s),
bicgstab_op(&a, &b, None, &m, &s),
gmres_op(&a, &b, None, &m, &s),
] {
assert!(r.converged && r.n_iterations == 0);
assert!(x.iter().all(|v| *v == 0.0));
}
}
#[test]
fn warm_start_is_honoured() {
let a = spd_tridiag(10, 2.0, -0.5);
let x_true: Vec<f64> = (0..10).map(|i| i as f64 * 0.25).collect();
let b = a.multiply(&x_true);
let s = KrylovSettings {
tolerance: 1e-10,
max_iter: 100,
restart: 20,
};
let m = JacobiOperatorPreconditioner::new(&a);
let (_, r) = cg_op(&a, &b, Some(&x_true), &m, &s);
assert!(r.converged);
assert_eq!(r.n_iterations, 0);
}
}