use crate::sparse::CscMatrix;
#[derive(Debug, Clone, Copy)]
pub struct MinresStats {
pub iters: usize,
pub residual_estimate: f64,
pub converged: bool,
}
pub fn pminres<MV, PV, SS>(
matvec: MV,
precond: PV,
b: &[f64],
x: &mut [f64],
tol: f64,
max_iter: usize,
mut should_stop: SS,
) -> MinresStats
where
MV: Fn(&[f64], &mut [f64]),
PV: Fn(&[f64], &mut [f64]),
SS: FnMut() -> bool,
{
let n = b.len();
debug_assert_eq!(x.len(), n);
if n == 0 {
return MinresStats {
iters: 0,
residual_estimate: 0.0,
converged: true,
};
}
let b_norm = norm2(b).max(f64::MIN_POSITIVE);
let mut kx = vec![0.0f64; n];
matvec(x, &mut kx);
let mut r1: Vec<f64> = b.iter().zip(kx.iter()).map(|(&bi, &ki)| bi - ki).collect();
let r1_norm = norm2(&r1);
if r1_norm <= tol * b_norm {
return MinresStats {
iters: 0,
residual_estimate: r1_norm,
converged: true,
};
}
let mut y = vec![0.0f64; n];
precond(&r1, &mut y);
let beta1_sq = dot(&r1, &y);
if beta1_sq <= 0.0 {
return MinresStats {
iters: 0,
residual_estimate: r1_norm,
converged: r1_norm <= tol * b_norm,
};
}
let beta1 = beta1_sq.sqrt();
let mut r2 = r1.clone();
let mut beta = beta1;
let mut oldb = 0.0_f64;
let mut dbar = 0.0_f64;
let mut epsln = 0.0_f64;
let mut phibar = beta1;
let mut cs = -1.0_f64;
let mut sn = 0.0_f64;
let mut w = vec![0.0_f64; n];
let mut w2 = vec![0.0_f64; n];
let mut residual_estimate = beta1;
let mut last_iter = 0usize;
let mut converged = false;
for itn in 1..=max_iter {
last_iter = itn;
if should_stop() {
break;
}
let s_inv = 1.0 / beta;
let v: Vec<f64> = y.iter().map(|&yi| yi * s_inv).collect();
matvec(&v, &mut y);
if itn >= 2 {
let factor = beta / oldb;
for i in 0..n {
y[i] -= factor * r1[i];
}
}
let alfa = dot(&v, &y);
let factor = alfa / beta;
for i in 0..n {
y[i] -= factor * r2[i];
}
std::mem::swap(&mut r1, &mut r2);
r2.copy_from_slice(&y);
precond(&r2, &mut y);
oldb = beta;
let beta_sq = dot(&r2, &y);
if beta_sq < 0.0 {
break;
}
beta = beta_sq.sqrt();
let oldeps = epsln;
let delta = cs * dbar + sn * alfa;
let gbar = sn * dbar - cs * alfa;
epsln = sn * beta;
dbar = -cs * beta;
let gamma = (gbar * gbar + beta * beta).sqrt().max(f64::MIN_POSITIVE);
cs = gbar / gamma;
sn = beta / gamma;
let phi = cs * phibar;
phibar *= sn;
let inv_gamma = 1.0 / gamma;
let mut w_new = vec![0.0_f64; n];
for i in 0..n {
w_new[i] = (v[i] - oldeps * w2[i] - delta * w[i]) * inv_gamma;
}
for i in 0..n {
x[i] += phi * w_new[i];
}
residual_estimate = phibar.abs();
if residual_estimate <= tol * b_norm {
converged = true;
break;
}
std::mem::swap(&mut w2, &mut w);
w = w_new;
if beta < f64::MIN_POSITIVE {
converged = residual_estimate <= tol * b_norm;
break;
}
}
MinresStats {
iters: last_iter,
residual_estimate,
converged,
}
}
#[inline]
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(&ai, &bi)| ai * bi).sum()
}
#[inline]
fn norm2(v: &[f64]) -> f64 {
dot(v, v).sqrt()
}
pub fn matvec_sym_upper(k: &CscMatrix, x: &[f64], y: &mut [f64]) {
let n = k.nrows;
debug_assert_eq!(x.len(), n);
debug_assert_eq!(y.len(), n);
for yi in y.iter_mut() {
*yi = 0.0;
}
for j in 0..n {
let xj = x[j];
for k_idx in k.col_ptr[j]..k.col_ptr[j + 1] {
let i = k.row_ind[k_idx];
let v = k.values[k_idx];
if i == j {
y[j] += v * xj;
} else {
y[i] += v * xj;
y[j] += v * x[i];
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::CscMatrix;
fn no_precond(r: &[f64], z: &mut [f64]) {
z.copy_from_slice(r);
}
#[test]
fn minres_2x2_spd() {
let k = CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[4.0, 1.0, 3.0], 2, 2).unwrap();
let b = vec![1.0, 2.0];
let mut x = vec![0.0; 2];
let stats = pminres(
|v, y| matvec_sym_upper(&k, v, y),
no_precond,
&b,
&mut x,
1e-12,
100,
|| false,
);
assert!(
stats.converged,
"should converge for 2x2 SPD, iters={} resid={:.2e}",
stats.iters, stats.residual_estimate
);
assert!((x[0] - 1.0 / 11.0).abs() < 1e-9, "x[0]≈1/11, got {}", x[0]);
assert!((x[1] - 7.0 / 11.0).abs() < 1e-9, "x[1]≈7/11, got {}", x[1]);
}
#[test]
fn minres_2x2_indefinite() {
let k = CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[2.0, 1.0, -1.0], 2, 2).unwrap();
let b = vec![3.0, 0.0];
let mut x = vec![0.0; 2];
let stats = pminres(
|v, y| matvec_sym_upper(&k, v, y),
no_precond,
&b,
&mut x,
1e-12,
100,
|| false,
);
assert!(
stats.converged,
"should converge for 2x2 indef, iters={}",
stats.iters
);
assert!((x[0] - 1.0).abs() < 1e-9, "x[0]≈1, got {}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-9, "x[1]≈1, got {}", x[1]);
}
#[test]
fn minres_5x5_matches_ldl() {
let entries = [
(0, 0, 4.0),
(0, 1, 0.5),
(1, 1, 4.0),
(1, 2, 0.5),
(2, 2, 4.0),
(0, 3, 0.3),
(3, 3, -2.0),
(3, 4, 0.4),
(4, 4, -2.0),
];
let rows: Vec<usize> = entries.iter().map(|(r, _, _)| *r).collect();
let cols: Vec<usize> = entries.iter().map(|(_, c, _)| *c).collect();
let vals: Vec<f64> = entries.iter().map(|(_, _, v)| *v).collect();
let k = CscMatrix::from_triplets(&rows, &cols, &vals, 5, 5).unwrap();
let b = vec![1.0, 2.0, -1.0, 0.5, -0.5];
let factor = crate::linalg::ldl::factorize_quasidefinite_with_amd(&k, None)
.expect("LDL should succeed");
let mut x_ldl = vec![0.0; 5];
factor.solve(&b, &mut x_ldl);
let mut x_minres = vec![0.0; 5];
let stats = pminres(
|v, y| matvec_sym_upper(&k, v, y),
no_precond,
&b,
&mut x_minres,
1e-10,
200,
|| false,
);
assert!(stats.converged, "should converge, iters={}", stats.iters);
for i in 0..5 {
assert!(
(x_ldl[i] - x_minres[i]).abs() < 1e-7,
"x[{}]: LDL={}, MINRES={}, diff={:.2e}",
i,
x_ldl[i],
x_minres[i],
(x_ldl[i] - x_minres[i]).abs()
);
}
}
#[test]
fn minres_can_be_stopped_early() {
let k = CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[4.0, 1.0, 3.0], 2, 2).unwrap();
let b = vec![1.0, 2.0];
let mut x = vec![0.0; 2];
let mut count = 0;
let stats = pminres(
|v, y| matvec_sym_upper(&k, v, y),
no_precond,
&b,
&mut x,
1e-12,
100,
|| {
count += 1;
count > 1
},
);
assert!(stats.iters <= 1 || !stats.converged, "should stop early");
}
#[test]
fn minres_zero_rhs() {
let k = CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[4.0, 1.0, 3.0], 2, 2).unwrap();
let b = vec![0.0; 2];
let mut x = vec![0.0; 2];
let stats = pminres(
|v, y| matvec_sym_upper(&k, v, y),
no_precond,
&b,
&mut x,
1e-12,
100,
|| false,
);
assert_eq!(stats.iters, 0);
assert!(stats.converged);
assert_eq!(x, vec![0.0; 2]);
}
#[test]
fn minres_nonzero_initial_guess() {
let k = CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[4.0, 1.0, 3.0], 2, 2).unwrap();
let b = vec![1.0, 2.0];
let mut x = vec![0.1, 0.7];
let stats = pminres(
|v, y| matvec_sym_upper(&k, v, y),
no_precond,
&b,
&mut x,
1e-12,
100,
|| false,
);
assert!(stats.converged, "should converge with warm start");
assert!((x[0] - 1.0 / 11.0).abs() < 1e-9);
assert!((x[1] - 7.0 / 11.0).abs() < 1e-9);
}
}