use crate::error::{SparseError, SparseResult};
fn csr_matvec(
indptr: &[usize],
indices: &[usize],
data: &[f64],
x: &[f64],
n: usize,
) -> Vec<f64> {
let mut y = vec![0.0f64; n];
for i in 0..n {
let mut acc = 0.0f64;
for pos in indptr[i]..indptr[i + 1] {
acc += data[pos] * x[indices[pos]];
}
y[i] = acc;
}
y
}
pub struct NeumannPoly {
pub degree: usize,
pub omega: f64,
indptr: Vec<usize>,
indices: Vec<usize>,
data: Vec<f64>,
n: usize,
}
impl NeumannPoly {
pub fn new(
indptr: &[usize],
indices: &[usize],
data: &[f64],
n: usize,
degree: usize,
omega: Option<f64>,
) -> SparseResult<Self> {
if indptr.len() != n + 1 {
return Err(SparseError::InconsistentData {
reason: format!("indptr length {} != n+1={}", indptr.len(), n + 1),
});
}
let omega = match omega {
Some(w) => {
if w <= 0.0 {
return Err(SparseError::ValueError(format!(
"NeumannPoly omega={w} must be positive"
)));
}
w
}
None => {
let row_max: f64 = (0..n)
.map(|i| {
data[indptr[i]..indptr[i + 1]]
.iter()
.map(|v| v.abs())
.sum::<f64>()
})
.fold(0.0f64, f64::max);
if row_max > 1e-300 {
1.0 / row_max
} else {
1.0
}
}
};
Ok(Self {
degree,
omega,
indptr: indptr.to_vec(),
indices: indices.to_vec(),
data: data.to_vec(),
n,
})
}
pub fn apply(&self, x: &[f64]) -> Vec<f64> {
let n = self.n;
let omega = self.omega;
let b_apply = |v: &[f64]| -> Vec<f64> {
let av = csr_matvec(&self.indptr, &self.indices, &self.data, v, n);
let mut bv = v.to_vec();
for (bi, ai) in bv.iter_mut().zip(av.iter()) {
*bi -= omega * ai;
}
bv
};
let mut sum = x.to_vec();
for _ in 0..self.degree {
let b_sum = b_apply(&sum);
for (si, (xi, bi)) in sum.iter_mut().zip(x.iter().zip(b_sum.iter())) {
*si = xi + bi;
}
}
for v in sum.iter_mut() {
*v *= omega;
}
sum
}
pub fn size(&self) -> usize {
self.n
}
}
pub struct ChebyshevPoly {
pub degree: usize,
pub lambda_min: f64,
pub lambda_max: f64,
indptr: Vec<usize>,
indices: Vec<usize>,
data: Vec<f64>,
n: usize,
}
impl ChebyshevPoly {
pub fn new(
indptr: &[usize],
indices: &[usize],
data: &[f64],
n: usize,
degree: usize,
lambda_min: f64,
lambda_max: f64,
) -> SparseResult<Self> {
if indptr.len() != n + 1 {
return Err(SparseError::InconsistentData {
reason: format!("indptr length {} != n+1={}", indptr.len(), n + 1),
});
}
if lambda_min >= lambda_max {
return Err(SparseError::ValueError(format!(
"ChebyshevPoly: lambda_min={lambda_min} must be < lambda_max={lambda_max}"
)));
}
if lambda_min <= 0.0 {
return Err(SparseError::ValueError(format!(
"ChebyshevPoly: lambda_min={lambda_min} must be > 0 for SPD systems"
)));
}
Ok(Self {
degree,
lambda_min,
lambda_max,
indptr: indptr.to_vec(),
indices: indices.to_vec(),
data: data.to_vec(),
n,
})
}
pub fn apply(&self, x: &[f64]) -> Vec<f64> {
let n = self.n;
let lambda_min = self.lambda_min;
let lambda_max = self.lambda_max;
let theta = 0.5 * (lambda_max + lambda_min); let delta = 0.5 * (lambda_max - lambda_min); let sigma = theta / delta.max(1e-300);
let a_apply = |v: &[f64]| -> Vec<f64> {
csr_matvec(&self.indptr, &self.indices, &self.data, v, n)
};
if self.degree == 0 {
return x.iter().map(|v| v / theta).collect();
}
let mut y: Vec<f64> = x.iter().map(|v| v / theta).collect();
if self.degree == 1 {
return y;
}
let mut y_prev = vec![0.0f64; n];
let mut rho = 1.0 / sigma;
for _ in 1..self.degree {
let ay = a_apply(&y);
let r: Vec<f64> = (0..n).map(|i| x[i] - ay[i]).collect();
let rho_new = 1.0 / (2.0 * sigma - rho);
let coeff_r = 2.0 * rho_new / delta;
let coeff_y = rho_new * rho;
let y_next: Vec<f64> = (0..n)
.map(|i| {
y[i] + coeff_r * r[i] + coeff_y * (y[i] - y_prev[i])
})
.collect();
y_prev = y;
y = y_next;
rho = rho_new;
}
y
}
pub fn size(&self) -> usize {
self.n
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_matrix() -> (Vec<usize>, Vec<usize>, Vec<f64>, usize) {
let n = 4usize;
let indptr = vec![0, 2, 5, 8, 10];
let indices = vec![0, 1, 0, 1, 2, 1, 2, 3, 2, 3];
let data = vec![4.0, -1.0, -1.0, 4.0, -1.0, -1.0, 4.0, -1.0, -1.0, 4.0];
(indptr, indices, data, n)
}
#[test]
fn test_neumann_poly_auto_omega() {
let (ip, idx, dat, n) = test_matrix();
let prec = NeumannPoly::new(&ip, &idx, &dat, n, 3, None).expect("NeumannPoly failed");
assert_eq!(prec.size(), n);
assert!(prec.omega > 0.0);
let b = vec![1.0, 0.0, 0.0, 0.0];
let y = prec.apply(&b);
assert_eq!(y.len(), n);
let norm: f64 = y.iter().map(|v| v * v).sum::<f64>().sqrt();
assert!(norm > 0.0, "Neumann should give non-zero output");
}
#[test]
fn test_neumann_poly_explicit_omega() {
let (ip, idx, dat, n) = test_matrix();
let prec = NeumannPoly::new(&ip, &idx, &dat, n, 2, Some(0.1))
.expect("NeumannPoly explicit omega");
let b = vec![1.0; n];
let y = prec.apply(&b);
assert!(y.iter().all(|v| v.is_finite()), "output must be finite");
}
#[test]
fn test_neumann_poly_degree0() {
let (ip, idx, dat, n) = test_matrix();
let prec = NeumannPoly::new(&ip, &idx, &dat, n, 0, Some(0.5))
.expect("NeumannPoly degree=0");
let b = vec![2.0, 4.0, 6.0, 8.0];
let y = prec.apply(&b);
for (&bi, &yi) in b.iter().zip(y.iter()) {
assert!((yi - 0.5 * bi).abs() < 1e-12, "degree=0: y={yi}, expected {}", 0.5 * bi);
}
}
#[test]
fn test_neumann_poly_invalid_omega() {
let (ip, idx, dat, n) = test_matrix();
assert!(NeumannPoly::new(&ip, &idx, &dat, n, 2, Some(-1.0)).is_err());
assert!(NeumannPoly::new(&ip, &idx, &dat, n, 2, Some(0.0)).is_err());
}
#[test]
fn test_chebyshev_poly_basic() {
let (ip, idx, dat, n) = test_matrix();
let prec = ChebyshevPoly::new(&ip, &idx, &dat, n, 3, 2.0, 6.0)
.expect("ChebyshevPoly failed");
assert_eq!(prec.size(), n);
let b = vec![1.0, 2.0, 3.0, 4.0];
let y = prec.apply(&b);
assert_eq!(y.len(), n);
assert!(y.iter().all(|v| v.is_finite()), "Chebyshev output must be finite");
}
#[test]
fn test_chebyshev_poly_degree0() {
let (ip, idx, dat, n) = test_matrix();
let prec = ChebyshevPoly::new(&ip, &idx, &dat, n, 0, 2.0, 6.0)
.expect("Chebyshev degree=0");
let b = vec![4.0, 8.0, 12.0, 16.0];
let y = prec.apply(&b);
let lc = 4.0f64; for (&bi, &yi) in b.iter().zip(y.iter()) {
assert!((yi - bi / lc).abs() < 1e-10, "degree=0: y={yi}, expected {}", bi / lc);
}
}
#[test]
fn test_chebyshev_poly_invalid_range() {
let (ip, idx, dat, n) = test_matrix();
assert!(ChebyshevPoly::new(&ip, &idx, &dat, n, 2, 6.0, 2.0).is_err());
assert!(ChebyshevPoly::new(&ip, &idx, &dat, n, 2, 3.0, 3.0).is_err());
assert!(ChebyshevPoly::new(&ip, &idx, &dat, n, 2, -1.0, 6.0).is_err());
assert!(ChebyshevPoly::new(&ip, &idx, &dat, n, 2, 0.0, 6.0).is_err());
}
#[test]
fn test_chebyshev_approx_inverse() {
let n = 3;
let lambda_min = 1.0;
let lambda_max = 3.0;
let indptr = vec![0, 1, 2, 3];
let indices = vec![0, 1, 2];
let data = vec![2.0, 2.5, 3.0];
let prec = ChebyshevPoly::new(&indptr, &indices, &data, n, 5, lambda_min, lambda_max)
.expect("ChebyshevPoly for diagonal");
let b = vec![1.0; n];
let y = prec.apply(&b);
assert!(y.iter().all(|v| v.is_finite() && *v > 0.0),
"Chebyshev of diagonal SPD should give positive result: {y:?}");
}
}