use crate::error::{SparseError, SparseResult};
use crate::learned_smoother::types::Smoother;
fn csr_matvec(a_values: &[f64], a_row_ptr: &[usize], a_col_idx: &[usize], x: &[f64]) -> Vec<f64> {
let n = a_row_ptr.len().saturating_sub(1);
let mut y = vec![0.0; n];
for i in 0..n {
let start = a_row_ptr[i];
let end = a_row_ptr[i + 1];
let mut sum = 0.0;
for pos in start..end {
sum += a_values[pos] * x[a_col_idx[pos]];
}
y[i] = sum;
}
y
}
fn compute_residual(
a_values: &[f64],
a_row_ptr: &[usize],
a_col_idx: &[usize],
x: &[f64],
b: &[f64],
) -> Vec<f64> {
let ax = csr_matvec(a_values, a_row_ptr, a_col_idx, x);
let n = b.len();
let mut r = vec![0.0; n];
for i in 0..n {
r[i] = b[i] - ax[i];
}
r
}
fn extract_diagonal(
a_values: &[f64],
a_row_ptr: &[usize],
a_col_idx: &[usize],
n: usize,
) -> Vec<f64> {
let mut diag = vec![0.0; n];
for i in 0..n {
let start = a_row_ptr[i];
let end = a_row_ptr[i + 1];
for pos in start..end {
if a_col_idx[pos] == i {
diag[i] = a_values[pos];
break;
}
}
}
diag
}
fn vec_norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
#[derive(Debug, Clone)]
pub struct LinearSmoother {
weights: Vec<f64>,
n: usize,
omega: f64,
}
impl LinearSmoother {
pub fn new(a_diag: &[f64], n: usize, omega: f64) -> Self {
let weights: Vec<f64> = a_diag
.iter()
.map(|&d| {
if d.abs() > f64::EPSILON {
omega / d
} else {
omega
}
})
.collect();
Self { weights, n, omega }
}
pub fn from_csr(
a_values: &[f64],
a_row_ptr: &[usize],
a_col_idx: &[usize],
omega: f64,
) -> Self {
let n = a_row_ptr.len().saturating_sub(1);
let diag = extract_diagonal(a_values, a_row_ptr, a_col_idx, n);
Self::new(&diag, n, omega)
}
fn apply_one_sweep(
&self,
a_values: &[f64],
a_row_ptr: &[usize],
a_col_idx: &[usize],
x: &mut [f64],
b: &[f64],
) {
let r = compute_residual(a_values, a_row_ptr, a_col_idx, x, b);
for i in 0..self.n {
x[i] += self.weights[i] * r[i];
}
}
pub fn spectral_radius_estimate(
&self,
a_values: &[f64],
a_row_ptr: &[usize],
a_col_idx: &[usize],
) -> f64 {
let n = self.n;
if n == 0 {
return 0.0;
}
let mut v: Vec<f64> = (0..n).map(|i| (i as f64 + 1.0).sin()).collect();
let mut nrm = vec_norm(&v);
if nrm < f64::EPSILON {
return 0.0;
}
for x in v.iter_mut() {
*x /= nrm;
}
let max_iter = 100;
let mut lambda = 0.0;
for _ in 0..max_iter {
let av = csr_matvec(a_values, a_row_ptr, a_col_idx, &v);
let mut w = vec![0.0; n];
for i in 0..n {
w[i] = v[i] - self.weights[i] * av[i];
}
nrm = vec_norm(&w);
if nrm < f64::EPSILON {
return 0.0;
}
let new_lambda = nrm;
for x in w.iter_mut() {
*x /= nrm;
}
v = w;
if (new_lambda - lambda).abs() < 1e-10 {
return new_lambda;
}
lambda = new_lambda;
}
lambda
}
pub fn weights(&self) -> &[f64] {
&self.weights
}
pub fn dim(&self) -> usize {
self.n
}
pub fn omega(&self) -> f64 {
self.omega
}
}
impl Smoother for LinearSmoother {
fn smooth(
&self,
a_values: &[f64],
a_row_ptr: &[usize],
a_col_idx: &[usize],
x: &mut [f64],
b: &[f64],
n_sweeps: usize,
) -> SparseResult<()> {
let n = a_row_ptr.len().saturating_sub(1);
if x.len() != n || b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: x.len(),
});
}
for _ in 0..n_sweeps {
self.apply_one_sweep(a_values, a_row_ptr, a_col_idx, x, b);
}
Ok(())
}
fn train_step(
&mut self,
a_values: &[f64],
a_row_ptr: &[usize],
a_col_idx: &[usize],
x: &mut [f64],
b: &[f64],
x_exact: &[f64],
lr: f64,
) -> SparseResult<f64> {
let n = self.n;
if x.len() != n || b.len() != n || x_exact.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: x.len(),
});
}
let r = compute_residual(a_values, a_row_ptr, a_col_idx, x, b);
let mut x_new = vec![0.0; n];
for i in 0..n {
x_new[i] = x[i] + self.weights[i] * r[i];
}
let mut error = vec![0.0; n];
for i in 0..n {
error[i] = x_new[i] - x_exact[i];
}
let loss: f64 = error.iter().map(|e| e * e).sum();
for i in 0..n {
let grad = 2.0 * error[i] * r[i];
self.weights[i] -= lr * grad;
}
x[..n].copy_from_slice(&x_new[..n]);
Ok(loss)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tridiag_3() -> (Vec<f64>, Vec<usize>, Vec<usize>) {
let values = vec![2.0, -1.0, -1.0, 2.0, -1.0, -1.0, 2.0];
let row_ptr = vec![0, 2, 5, 7];
let col_idx = vec![0, 1, 0, 1, 2, 1, 2];
(values, row_ptr, col_idx)
}
#[test]
fn test_linear_smoother_reduces_residual() {
let (vals, rp, ci) = tridiag_3();
let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
let b = vec![1.0, 0.0, 1.0];
let mut x = vec![0.0; 3];
let r0 = compute_residual(&vals, &rp, &ci, &x, &b);
let norm0 = vec_norm(&r0);
smoother
.smooth(&vals, &rp, &ci, &mut x, &b, 10)
.expect("smooth failed");
let r1 = compute_residual(&vals, &rp, &ci, &x, &b);
let norm1 = vec_norm(&r1);
assert!(norm1 < norm0, "Residual should decrease after smoothing");
}
#[test]
fn test_linear_smoother_training_reduces_loss() {
let (vals, rp, ci) = tridiag_3();
let mut smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
let b = vec![1.0, 0.0, 1.0];
let x_exact = vec![1.0, 1.0, 1.0];
let mut losses = Vec::new();
for _ in 0..20 {
let mut x = vec![0.0; 3];
let loss = smoother
.train_step(&vals, &rp, &ci, &mut x, &b, &x_exact, 0.01)
.expect("train failed");
losses.push(loss);
}
let first_losses: f64 = losses[..5].iter().sum::<f64>() / 5.0;
let last_losses: f64 = losses[15..].iter().sum::<f64>() / 5.0;
assert!(
last_losses < first_losses,
"Training should reduce average loss: first={first_losses}, last={last_losses}"
);
}
#[test]
fn test_spectral_radius_less_than_one() {
let (vals, rp, ci) = tridiag_3();
let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
let rho = smoother.spectral_radius_estimate(&vals, &rp, &ci);
assert!(
rho < 1.0,
"Spectral radius should be < 1 for convergent smoother, got {rho}"
);
}
#[test]
fn test_dimension_mismatch() {
let (vals, rp, ci) = tridiag_3();
let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 2.0 / 3.0);
let b = vec![1.0, 0.0]; let mut x = vec![0.0; 3];
let result = smoother.smooth(&vals, &rp, &ci, &mut x, &b, 1);
assert!(result.is_err());
}
#[test]
fn test_zero_dimension_smoother() {
let vals: Vec<f64> = vec![];
let rp = vec![0usize];
let ci: Vec<usize> = vec![];
let smoother = LinearSmoother::from_csr(&vals, &rp, &ci, 0.5);
assert_eq!(smoother.dim(), 0);
let rho = smoother.spectral_radius_estimate(&vals, &rp, &ci);
assert!((rho - 0.0).abs() < f64::EPSILON);
}
}