use crate::csr::CsrMatrix;
use crate::error::{SparseError, SparseResult};
use crate::iterative_solvers::{IterativeSolverConfig, Preconditioner, SolverResult};
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::{Float, NumAssign, SparseElement};
use std::fmt::Debug;
use std::iter::Sum;
fn spmv_local<F>(a: &CsrMatrix<F>, x: &Array1<F>) -> SparseResult<Array1<F>>
where
F: Float + NumAssign + Sum + SparseElement + 'static,
{
let (m, n) = a.shape();
if x.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: x.len(),
});
}
let mut y = Array1::zeros(m);
for i in 0..m {
let range = a.row_range(i);
let cols = &a.indices[range.clone()];
let vals = &a.data[range];
let mut acc = F::sparse_zero();
for (idx, &col) in cols.iter().enumerate() {
acc += vals[idx] * x[col];
}
y[i] = acc;
}
Ok(y)
}
#[inline]
fn dot<F: Float + Sum>(a: &Array1<F>, b: &Array1<F>) -> F {
a.iter().zip(b.iter()).map(|(&ai, &bi)| ai * bi).sum()
}
#[inline]
fn norm2<F: Float + Sum>(v: &Array1<F>) -> F {
dot(v, v).sqrt()
}
#[inline]
fn axpy<F: Float>(y: &mut Array1<F>, alpha: F, x: &Array1<F>) {
for (yi, &xi) in y.iter_mut().zip(x.iter()) {
*yi = *yi + alpha * xi;
}
}
pub fn sor<F>(
a: &CsrMatrix<F>,
b: &Array1<F>,
omega: F,
config: &IterativeSolverConfig,
) -> SparseResult<SolverResult<F>>
where
F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
{
let (m, n) = a.shape();
if m != n {
return Err(SparseError::ValueError(
"SOR requires a square matrix".to_string(),
));
}
if b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: b.len(),
});
}
let zero = F::sparse_zero();
let two = F::from(2.0).ok_or_else(|| {
SparseError::ValueError("Failed to convert 2.0 to float".to_string())
})?;
if omega <= zero || omega >= two {
return Err(SparseError::ValueError(
"SOR omega must be in the open interval (0, 2)".to_string(),
));
}
let tol = F::from(config.tol).ok_or_else(|| {
SparseError::ValueError("Failed to convert tolerance to float type".to_string())
})?;
let bnorm = norm2(b);
let tolerance = if bnorm <= F::epsilon() {
tol
} else {
tol * bnorm
};
let mut x = Array1::zeros(n);
let mut diag = vec![F::sparse_zero(); n];
for i in 0..n {
let d = a.get(i, i);
if d.abs() < F::epsilon() {
return Err(SparseError::ValueError(format!(
"Zero diagonal at row {i} in SOR"
)));
}
diag[i] = d;
}
let one_minus_omega = F::sparse_one() - omega;
for iter in 0..config.max_iter {
let x_old = x.clone();
for i in 0..n {
let range = a.row_range(i);
let cols = &a.indices[range.clone()];
let vals = &a.data[range];
let mut sigma = b[i];
for (idx, &col) in cols.iter().enumerate() {
if col != i {
sigma = sigma - vals[idx] * x[col];
}
}
x[i] = one_minus_omega * x[i] + omega * sigma / diag[i];
}
let mut diff_norm = F::sparse_zero();
for i in 0..n {
let d = x[i] - x_old[i];
diff_norm = diff_norm + d * d;
}
diff_norm = diff_norm.sqrt();
if diff_norm <= tolerance {
let ax = spmv_local(a, &x)?;
let mut r = b.clone();
axpy(&mut r, -F::sparse_one(), &ax);
let rnorm = norm2(&r);
return Ok(SolverResult {
solution: x,
n_iter: iter + 1,
residual_norm: rnorm,
converged: rnorm <= tolerance,
});
}
}
let ax = spmv_local(a, &x)?;
let mut r = b.clone();
axpy(&mut r, -F::sparse_one(), &ax);
let rnorm = norm2(&r);
Ok(SolverResult {
solution: x,
n_iter: config.max_iter,
residual_norm: rnorm,
converged: rnorm <= tolerance,
})
}
pub fn ssor<F>(
a: &CsrMatrix<F>,
b: &Array1<F>,
omega: F,
config: &IterativeSolverConfig,
) -> SparseResult<SolverResult<F>>
where
F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
{
let (m, n) = a.shape();
if m != n {
return Err(SparseError::ValueError(
"SSOR requires a square matrix".to_string(),
));
}
if b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: b.len(),
});
}
let zero = F::sparse_zero();
let two = F::from(2.0).ok_or_else(|| {
SparseError::ValueError("Failed to convert 2.0 to float".to_string())
})?;
if omega <= zero || omega >= two {
return Err(SparseError::ValueError(
"SSOR omega must be in the open interval (0, 2)".to_string(),
));
}
let tol = F::from(config.tol).ok_or_else(|| {
SparseError::ValueError("Failed to convert tolerance to float type".to_string())
})?;
let bnorm = norm2(b);
let tolerance = if bnorm <= F::epsilon() {
tol
} else {
tol * bnorm
};
let mut x = Array1::zeros(n);
let mut diag = vec![F::sparse_zero(); n];
for i in 0..n {
let d = a.get(i, i);
if d.abs() < F::epsilon() {
return Err(SparseError::ValueError(format!(
"Zero diagonal at row {i} in SSOR"
)));
}
diag[i] = d;
}
let one_minus_omega = F::sparse_one() - omega;
for iter in 0..config.max_iter {
let x_before = x.clone();
for i in 0..n {
let range = a.row_range(i);
let cols = &a.indices[range.clone()];
let vals = &a.data[range];
let mut sigma = b[i];
for (idx, &col) in cols.iter().enumerate() {
if col != i {
sigma = sigma - vals[idx] * x[col];
}
}
x[i] = one_minus_omega * x[i] + omega * sigma / diag[i];
}
for ii in 0..n {
let i = n - 1 - ii;
let range = a.row_range(i);
let cols = &a.indices[range.clone()];
let vals = &a.data[range];
let mut sigma = b[i];
for (idx, &col) in cols.iter().enumerate() {
if col != i {
sigma = sigma - vals[idx] * x[col];
}
}
x[i] = one_minus_omega * x[i] + omega * sigma / diag[i];
}
let mut diff_norm = F::sparse_zero();
for i in 0..n {
let d = x[i] - x_before[i];
diff_norm = diff_norm + d * d;
}
diff_norm = diff_norm.sqrt();
if diff_norm <= tolerance {
let ax = spmv_local(a, &x)?;
let mut r = b.clone();
axpy(&mut r, -F::sparse_one(), &ax);
let rnorm = norm2(&r);
return Ok(SolverResult {
solution: x,
n_iter: iter + 1,
residual_norm: rnorm,
converged: rnorm <= tolerance,
});
}
}
let ax = spmv_local(a, &x)?;
let mut r = b.clone();
axpy(&mut r, -F::sparse_one(), &ax);
let rnorm = norm2(&r);
Ok(SolverResult {
solution: x,
n_iter: config.max_iter,
residual_norm: rnorm,
converged: rnorm <= tolerance,
})
}
pub fn idrs<F>(
a: &CsrMatrix<F>,
b: &Array1<F>,
s: usize,
config: &IterativeSolverConfig,
precond: Option<&dyn Preconditioner<F>>,
) -> SparseResult<SolverResult<F>>
where
F: Float + NumAssign + Sum + SparseElement + Debug + 'static,
{
let (m, n) = a.shape();
if m != n {
return Err(SparseError::ValueError(
"IDR(s) requires a square matrix".to_string(),
));
}
if b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: b.len(),
});
}
if s == 0 {
return Err(SparseError::ValueError(
"IDR(s) subspace dimension s must be >= 1".to_string(),
));
}
let tol = F::from(config.tol).ok_or_else(|| {
SparseError::ValueError("Failed to convert tolerance to float type".to_string())
})?;
let bnorm = norm2(b);
if bnorm <= F::epsilon() {
return Ok(SolverResult {
solution: Array1::zeros(n),
n_iter: 0,
residual_norm: F::sparse_zero(),
converged: true,
});
}
let tolerance = tol * bnorm;
let ten_eps = F::epsilon()
* F::from(10.0).ok_or_else(|| {
SparseError::ValueError("Failed to convert 10.0 to float".to_string())
})?;
let mut x = Array1::zeros(n);
let mut r = b.clone();
let s_dim = s.min(n);
let mut p: Vec<Array1<F>> = Vec::with_capacity(s_dim);
for k in 0..s_dim {
let mut pk = Array1::zeros(n);
if k == 0 {
for i in 0..n {
pk[i] = r[i];
}
} else {
for i in 0..n {
let val = ((i * (k + 1) * 7 + 3) % 17) as f64 + 1.0;
let sign_val = if (i + k) % 2 == 0 { 1.0 } else { -1.0 };
pk[i] = F::from(val * sign_val).unwrap_or_else(|| F::sparse_one());
}
}
let pnorm = norm2(&pk);
if pnorm > F::epsilon() {
let inv = F::sparse_one() / pnorm;
for v in pk.iter_mut() {
*v = *v * inv;
}
}
p.push(pk);
}
let mut g_cols: Vec<Array1<F>> = vec![Array1::zeros(n); s_dim];
let mut u_cols: Vec<Array1<F>> = vec![Array1::zeros(n); s_dim];
let mut f = vec![F::sparse_zero(); s_dim];
let mut c = vec![F::sparse_zero(); s_dim];
for (k, pk) in p.iter().enumerate().take(s_dim) {
f[k] = dot(pk, &r);
}
let mut omega = F::sparse_one();
let mut total_iter = 0usize;
'outer: while total_iter < config.max_iter {
for k in 0..s_dim {
let mr = match precond {
Some(pc) => pc.apply(&r)?,
None => r.clone(),
};
let mut v = mr.clone();
for vi in v.iter_mut() {
*vi = omega * *vi;
}
for j in 0..k {
axpy(&mut v, -c[j], &u_cols[j]);
}
u_cols[k] = v.clone();
g_cols[k] = spmv_local(a, &u_cols[k])?;
if let Some(pc) = precond {
g_cols[k] = pc.apply(&g_cols[k])?;
}
let pk_gk = dot(&p[k], &g_cols[k]);
if pk_gk.abs() < ten_eps {
continue;
}
c[k] = f[k] / pk_gk;
axpy(&mut r, -c[k], &g_cols[k]);
axpy(&mut x, c[k], &u_cols[k]);
total_iter += 1;
let rnorm = norm2(&r);
if rnorm <= tolerance {
return Ok(SolverResult {
solution: x,
n_iter: total_iter,
residual_norm: rnorm,
converged: true,
});
}
if total_iter >= config.max_iter {
break 'outer;
}
for j in (k + 1)..s_dim {
f[j] = f[j] - c[k] * dot(&p[j], &g_cols[k]);
}
}
let mr_final = match precond {
Some(pc) => pc.apply(&r)?,
None => r.clone(),
};
let t = spmv_local(a, &mr_final)?;
let t = match precond {
Some(pc) => pc.apply(&t)?,
None => t,
};
let tt = dot(&t, &t);
if tt < ten_eps {
break 'outer;
}
omega = dot(&t, &r) / tt;
if omega.abs() < ten_eps {
break 'outer;
}
axpy(&mut x, omega, &mr_final);
axpy(&mut r, -omega, &t);
total_iter += 1;
let rnorm = norm2(&r);
if rnorm <= tolerance {
return Ok(SolverResult {
solution: x,
n_iter: total_iter,
residual_norm: rnorm,
converged: true,
});
}
if total_iter >= config.max_iter {
break 'outer;
}
for k in 0..s_dim {
f[k] = dot(&p[k], &r);
}
}
let ax = spmv_local(a, &x)?;
let mut r_final = b.clone();
axpy(&mut r_final, -F::sparse_one(), &ax);
let rnorm = norm2(&r_final);
Ok(SolverResult {
solution: x,
n_iter: total_iter,
residual_norm: rnorm,
converged: rnorm <= tolerance,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iterative_solvers::IterativeSolverConfig;
fn spd_3x3() -> CsrMatrix<f64> {
let rows = vec![0, 0, 0, 1, 1, 1, 2, 2, 2];
let cols = vec![0, 1, 2, 0, 1, 2, 0, 1, 2];
let data = vec![4.0, -1.0, -1.0, -1.0, 4.0, -1.0, -1.0, -1.0, 4.0];
CsrMatrix::new(data, rows, cols, (3, 3)).expect("valid matrix")
}
fn spd_5x5() -> CsrMatrix<f64> {
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut data = Vec::new();
for i in 0..5 {
rows.push(i);
cols.push(i);
data.push(4.0);
if i > 0 {
rows.push(i);
cols.push(i - 1);
data.push(-1.0);
}
if i < 4 {
rows.push(i);
cols.push(i + 1);
data.push(-1.0);
}
}
CsrMatrix::new(data, rows, cols, (5, 5)).expect("valid matrix")
}
fn dd_nonsym_4x4() -> CsrMatrix<f64> {
let rows = vec![0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3];
let cols = vec![0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 1, 2, 3];
let data = vec![
10.0, -1.0, 2.0, -1.0, 11.0, -1.0, 3.0, 2.0, -1.0, 10.0, -1.0, 3.0, -1.0, 8.0,
];
CsrMatrix::new(data, rows, cols, (4, 4)).expect("valid matrix")
}
fn verify(a: &CsrMatrix<f64>, x: &Array1<f64>, b: &Array1<f64>, tol: f64) {
let ax = spmv_local(a, x).expect("spmv");
for (i, (&axi, &bi)) in ax.iter().zip(b.iter()).enumerate() {
assert!(
(axi - bi).abs() < tol,
"Ax[{i}]={axi:.6} != b[{i}]={bi:.6} (diff={:.2e})",
(axi - bi).abs()
);
}
}
#[test]
fn test_sor_spd_3x3() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let cfg = IterativeSolverConfig {
max_iter: 5000,
tol: 1e-8,
verbose: false,
};
let res = sor(&a, &b, 1.0, &cfg).expect("SOR failed");
assert!(res.converged, "SOR did not converge (residual={})", res.residual_norm);
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_sor_spd_5x5() {
let a = spd_5x5();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let cfg = IterativeSolverConfig {
max_iter: 10000,
tol: 1e-8,
verbose: false,
};
let res = sor(&a, &b, 1.2, &cfg).expect("SOR failed");
assert!(res.converged, "SOR 5x5 did not converge");
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_sor_dd_nonsym() {
let a = dd_nonsym_4x4();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let cfg = IterativeSolverConfig {
max_iter: 5000,
tol: 1e-8,
verbose: false,
};
let res = sor(&a, &b, 1.0, &cfg).expect("SOR non-symmetric failed");
assert!(res.converged, "SOR DD non-sym did not converge");
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_sor_invalid_omega() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let cfg = IterativeSolverConfig::default();
assert!(sor(&a, &b, 0.0_f64, &cfg).is_err());
assert!(sor(&a, &b, 2.0_f64, &cfg).is_err());
assert!(sor(&a, &b, -0.5_f64, &cfg).is_err());
}
#[test]
fn test_sor_dimension_mismatch() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0]);
let cfg = IterativeSolverConfig::default();
assert!(sor(&a, &b, 1.0_f64, &cfg).is_err());
}
#[test]
fn test_ssor_spd_3x3() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let cfg = IterativeSolverConfig {
max_iter: 5000,
tol: 1e-8,
verbose: false,
};
let res = ssor(&a, &b, 1.0, &cfg).expect("SSOR failed");
assert!(res.converged, "SSOR did not converge (residual={})", res.residual_norm);
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_ssor_spd_5x5() {
let a = spd_5x5();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let cfg = IterativeSolverConfig {
max_iter: 10000,
tol: 1e-8,
verbose: false,
};
let res = ssor(&a, &b, 1.0, &cfg).expect("SSOR 5x5 failed");
assert!(res.converged, "SSOR 5x5 did not converge");
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_ssor_invalid_omega() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let cfg = IterativeSolverConfig::default();
assert!(ssor(&a, &b, 0.0_f64, &cfg).is_err());
assert!(ssor(&a, &b, 2.0_f64, &cfg).is_err());
}
#[test]
fn test_ssor_vs_sor_convergence() {
let a = spd_5x5();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let cfg = IterativeSolverConfig {
max_iter: 10000,
tol: 1e-8,
verbose: false,
};
let res_ssor = ssor(&a, &b, 1.0, &cfg).expect("SSOR");
let res_sor = sor(&a, &b, 1.0, &cfg).expect("SOR");
assert!(res_ssor.converged);
assert!(res_sor.converged);
assert!(
res_ssor.n_iter <= res_sor.n_iter + 10, "SSOR used {} iters vs SOR {} iters",
res_ssor.n_iter,
res_sor.n_iter
);
}
#[test]
fn test_idrs_s1_spd_3x3() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let cfg = IterativeSolverConfig {
max_iter: 500,
tol: 1e-8,
verbose: false,
};
let res = idrs(&a, &b, 1, &cfg, None).expect("IDR(1) failed");
assert!(res.converged, "IDR(1) did not converge (residual={})", res.residual_norm);
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_idrs_s4_spd_5x5() {
let a = spd_5x5();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let cfg = IterativeSolverConfig {
max_iter: 500,
tol: 1e-8,
verbose: false,
};
let res = idrs(&a, &b, 4, &cfg, None).expect("IDR(4) failed");
assert!(res.converged, "IDR(4) 5x5 did not converge (residual={})", res.residual_norm);
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_idrs_nonsym() {
let a = dd_nonsym_4x4();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let cfg = IterativeSolverConfig {
max_iter: 500,
tol: 1e-8,
verbose: false,
};
let res = idrs(&a, &b, 2, &cfg, None).expect("IDR(2) nonsym failed");
assert!(res.converged, "IDR(2) non-sym did not converge");
verify(&a, &res.solution, &b, 1e-6);
}
#[test]
fn test_idrs_zero_rhs() {
let a = spd_3x3();
let b = Array1::zeros(3);
let cfg = IterativeSolverConfig::default();
let res = idrs(&a, &b, 2, &cfg, None).expect("IDR(s) zero rhs");
assert!(res.converged);
assert!(res.residual_norm <= 1e-14);
}
#[test]
fn test_idrs_zero_s_error() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let cfg = IterativeSolverConfig::default();
assert!(idrs(&a, &b, 0, &cfg, None).is_err());
}
#[test]
fn test_idrs_dimension_mismatch() {
let a = spd_3x3();
let b = Array1::from_vec(vec![1.0, 2.0]);
let cfg = IterativeSolverConfig::default();
assert!(idrs(&a, &b, 2, &cfg, None).is_err());
}
#[test]
fn test_idrs_nonsquare_error() {
let rows = vec![0, 1];
let cols = vec![0, 1];
let data = vec![1.0, 2.0];
let a =
CsrMatrix::new(data, rows, cols, (2, 3)).expect("valid nonsquare");
let b = Array1::from_vec(vec![1.0, 2.0]);
let cfg = IterativeSolverConfig::default();
assert!(idrs(&a, &b, 2, &cfg, None).is_err());
}
}