use crate::csr::CsrMatrix;
use crate::error::{SparseError, SparseResult};
use crate::iterative_solvers::IterativeSolverConfig;
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::{Float, NumAssign, SparseElement};
use std::fmt::Debug;
use std::iter::Sum;
fn spmv(a: &CsrMatrix<f64>, x: &[f64]) -> SparseResult<Vec<f64>> {
let (rows, cols) = a.shape();
if x.len() != cols {
return Err(SparseError::DimensionMismatch {
expected: cols,
found: x.len(),
});
}
let result = a.dot(x)?;
Ok(result)
}
fn csr_transpose(a: &CsrMatrix<f64>) -> CsrMatrix<f64> {
a.transpose()
}
fn csr_matmul(a: &CsrMatrix<f64>, b: &CsrMatrix<f64>) -> SparseResult<CsrMatrix<f64>> {
a.matmul(b)
}
fn galerkin_product(
r: &CsrMatrix<f64>,
a: &CsrMatrix<f64>,
p: &CsrMatrix<f64>,
) -> SparseResult<CsrMatrix<f64>> {
let ap = csr_matmul(a, p)?;
csr_matmul(r, &ap)
}
fn jacobi_smooth(
a: &CsrMatrix<f64>,
x: &mut Vec<f64>,
b: &[f64],
diag_inv: &[f64],
omega: f64,
n_iter: usize,
) -> SparseResult<()> {
let n = x.len();
let mut x_new = vec![0.0f64; n];
for _ in 0..n_iter {
let ax = spmv(a, x)?;
for i in 0..n {
let residual_i = b[i] - ax[i];
x_new[i] = x[i] + omega * diag_inv[i] * residual_i;
}
x.copy_from_slice(&x_new);
}
Ok(())
}
fn gauss_seidel_smooth(
a: &CsrMatrix<f64>,
x: &mut Vec<f64>,
b: &[f64],
n_iter: usize,
) -> SparseResult<()> {
let n = x.len();
for _ in 0..n_iter {
for i in 0..n {
let range = a.row_range(i);
let mut sigma = 0.0f64;
let mut diag = 1.0f64;
for pos in range {
let j = a.indices[pos];
let val = a.data[pos];
if j == i {
diag = val;
} else {
sigma += val * x[j];
}
}
if diag.abs() > f64::EPSILON {
x[i] = (b[i] - sigma) / diag;
}
}
}
Ok(())
}
fn gauss_seidel_smooth_backward(
a: &CsrMatrix<f64>,
x: &mut Vec<f64>,
b: &[f64],
n_iter: usize,
) -> SparseResult<()> {
let n = x.len();
for _ in 0..n_iter {
for i in (0..n).rev() {
let range = a.row_range(i);
let mut sigma = 0.0f64;
let mut diag = 1.0f64;
for pos in range {
let j = a.indices[pos];
let val = a.data[pos];
if j == i {
diag = val;
} else {
sigma += val * x[j];
}
}
if diag.abs() > f64::EPSILON {
x[i] = (b[i] - sigma) / diag;
}
}
}
Ok(())
}
fn direct_solve_small(dense: &[Vec<f64>], rhs: &[f64]) -> SparseResult<Vec<f64>> {
let n = rhs.len();
if dense.len() != n || (n > 0 && dense[0].len() != n) {
return Err(SparseError::DimensionMismatch {
expected: n,
found: dense.len(),
});
}
if n == 0 {
return Ok(Vec::new());
}
let mut aug: Vec<Vec<f64>> = dense
.iter()
.enumerate()
.map(|(i, row)| {
let mut r = row.clone();
r.push(rhs[i]);
r
})
.collect();
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..n {
let v = aug[row][col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-15 {
return Err(SparseError::SingularMatrix(
"Near-singular matrix in direct_solve_small".to_string(),
));
}
aug.swap(col, max_row);
let pivot = aug[col][col];
for row in (col + 1)..n {
let factor = aug[row][col] / pivot;
for k in col..=n {
let val = aug[col][k];
aug[row][k] -= factor * val;
}
}
}
let mut x = vec![0.0f64; n];
for i in (0..n).rev() {
let mut s = aug[i][n];
for j in (i + 1)..n {
s -= aug[i][j] * x[j];
}
x[i] = s / aug[i][i];
}
Ok(x)
}
fn csr_to_dense(a: &CsrMatrix<f64>) -> Vec<Vec<f64>> {
let (rows, cols) = a.shape();
let mut dense = vec![vec![0.0f64; cols]; rows];
for i in 0..rows {
for pos in a.row_range(i) {
let j = a.indices[pos];
dense[i][j] = a.data[pos];
}
}
dense
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CfLabel {
Undecided,
Coarse,
Fine,
}
fn rs_strength_of_connection(a: &CsrMatrix<f64>, theta: f64) -> Vec<Vec<usize>> {
let n = a.shape().0;
let mut strong = vec![Vec::new(); n];
for i in 0..n {
let mut max_neg = 0.0f64;
for pos in a.row_range(i) {
let j = a.indices[pos];
if j != i {
let v = -a.data[pos]; if v > max_neg {
max_neg = v;
}
}
}
let threshold = theta * max_neg;
for pos in a.row_range(i) {
let j = a.indices[pos];
if j != i {
let v = -a.data[pos];
if v >= threshold && threshold > 0.0 {
strong[i].push(j);
}
}
}
}
strong
}
fn transpose_strong(strong: &[Vec<usize>], n: usize) -> Vec<Vec<usize>> {
let mut st = vec![Vec::new(); n];
for (i, row) in strong.iter().enumerate() {
for &j in row {
st[j].push(i);
}
}
st
}
fn rs_classical_coarsening(strong: &[Vec<usize>]) -> Vec<CfLabel> {
let n = strong.len();
let strong_t = transpose_strong(strong, n);
let mut lambda: Vec<i64> = strong_t.iter().map(|v| v.len() as i64).collect();
let mut labels = vec![CfLabel::Undecided; n];
let mut remaining: Vec<bool> = vec![true; n];
let mut n_remaining = n;
while n_remaining > 0 {
let mut best = usize::MAX;
let mut best_lambda = i64::MIN;
for i in 0..n {
if remaining[i] && labels[i] == CfLabel::Undecided && lambda[i] > best_lambda {
best_lambda = lambda[i];
best = i;
}
}
if best == usize::MAX {
break;
}
labels[best] = CfLabel::Coarse;
remaining[best] = false;
n_remaining -= 1;
for &i in &strong_t[best] {
if labels[i] == CfLabel::Undecided {
labels[i] = CfLabel::Fine;
remaining[i] = false;
n_remaining -= 1;
for &k in &strong_t[i] {
if labels[k] == CfLabel::Undecided {
lambda[k] += 1;
}
}
}
}
}
for label in labels.iter_mut() {
if *label == CfLabel::Undecided {
*label = CfLabel::Coarse;
}
}
labels
}
fn build_coarse_map(labels: &[CfLabel]) -> (Vec<usize>, Vec<usize>, Vec<Option<usize>>) {
let n = labels.len();
let mut coarse_indices = Vec::new();
let mut fine_indices = Vec::new();
let mut coarse_map: Vec<Option<usize>> = vec![None; n];
for (i, &label) in labels.iter().enumerate() {
match label {
CfLabel::Coarse => {
coarse_map[i] = Some(coarse_indices.len());
coarse_indices.push(i);
}
_ => {
fine_indices.push(i);
}
}
}
(coarse_indices, fine_indices, coarse_map)
}
pub fn rs_direct_interpolation(
a: &CsrMatrix<f64>,
labels: &[CfLabel],
coarse_map: &[Option<usize>],
n_coarse: usize,
) -> SparseResult<CsrMatrix<f64>> {
let n_fine_total = a.shape().0;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..n_fine_total {
match labels[i] {
CfLabel::Coarse => {
if let Some(ci) = coarse_map[i] {
rows.push(i);
cols.push(ci);
vals.push(1.0f64);
}
}
_ => {
let mut diag = 0.0f64;
let mut coarse_sum = 0.0f64;
let mut coarse_conns: Vec<(usize, f64)> = Vec::new();
for pos in a.row_range(i) {
let j = a.indices[pos];
let v = a.data[pos];
if j == i {
diag = v;
} else if labels[j] == CfLabel::Coarse {
coarse_sum += v;
if let Some(cj) = coarse_map[j] {
coarse_conns.push((cj, v));
}
}
}
if coarse_conns.is_empty() || coarse_sum.abs() < f64::EPSILON {
continue;
}
for (cj, a_ij) in coarse_conns {
let w = -a_ij / (diag * coarse_sum) * coarse_sum;
let w_interp = a_ij / coarse_sum;
let _ = w; rows.push(i);
cols.push(cj);
vals.push(-w_interp); }
}
}
}
CsrMatrix::from_triplets(n_fine_total, n_coarse, rows, cols, vals)
}
pub fn rs_classical_interpolation(
a: &CsrMatrix<f64>,
labels: &[CfLabel],
strong: &[Vec<usize>],
coarse_map: &[Option<usize>],
n_coarse: usize,
) -> SparseResult<CsrMatrix<f64>> {
let n = a.shape().0;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..n {
match labels[i] {
CfLabel::Coarse => {
if let Some(ci) = coarse_map[i] {
rows.push(i);
cols.push(ci);
vals.push(1.0f64);
}
}
_ => {
let mut ci_set: Vec<usize> = Vec::new();
let mut fi_set: Vec<usize> = Vec::new();
let strong_i = &strong[i];
for &j in strong_i {
match labels[j] {
CfLabel::Coarse => ci_set.push(j),
_ => fi_set.push(j),
}
}
let mut w_acc: std::collections::HashMap<usize, f64> =
std::collections::HashMap::new();
let mut diag = 0.0f64;
let mut alpha_num = 0.0f64; let mut beta_num = 0.0f64;
for pos in a.row_range(i) {
let j = a.indices[pos];
let v = a.data[pos];
if j == i {
diag = v;
} else if v < 0.0 {
alpha_num += v;
} else {
beta_num += v;
}
}
let mut ci_neg_sum = 0.0f64;
let mut ci_pos_sum = 0.0f64;
for &cj in &ci_set {
let a_icj = a.get(i, cj);
if a_icj < 0.0 {
ci_neg_sum += a_icj;
} else {
ci_pos_sum += a_icj;
}
}
for &fj in &fi_set {
let a_ifj = a.get(i, fj);
let mut denom = 0.0f64;
for &ck in strong[fj]
.iter()
.filter(|&&k| labels[k] == CfLabel::Coarse && ci_set.contains(&k))
{
denom += a.get(fj, ck);
}
if denom.abs() > f64::EPSILON {
for &ck in strong[fj]
.iter()
.filter(|&&k| labels[k] == CfLabel::Coarse && ci_set.contains(&k))
{
let a_fjck = a.get(fj, ck);
let contrib = a_ifj * a_fjck / denom;
let entry = w_acc.entry(ck).or_insert(0.0);
*entry += contrib;
}
}
}
for &cj in &ci_set {
let a_icj = a.get(i, cj);
let entry = w_acc.entry(cj).or_insert(0.0);
*entry += a_icj;
}
let alpha = if ci_neg_sum.abs() > f64::EPSILON {
alpha_num / ci_neg_sum
} else {
0.0
};
let beta = if ci_pos_sum.abs() > f64::EPSILON {
beta_num / ci_pos_sum
} else {
0.0
};
for (cj, w_sum) in &w_acc {
if let Some(ck_idx) = coarse_map[*cj] {
let a_icj = a.get(i, *cj);
let scale = if a_icj < 0.0 { alpha } else { beta };
let w = -scale * w_sum / diag;
if w.abs() > 1e-15 {
rows.push(i);
cols.push(ck_idx);
vals.push(w);
}
}
}
}
}
}
CsrMatrix::from_triplets(n, n_coarse, rows, cols, vals)
}
#[derive(Debug, Clone)]
pub struct RsAmgLevel {
pub a: CsrMatrix<f64>,
pub p: Option<CsrMatrix<f64>>,
pub r: Option<CsrMatrix<f64>>,
pub diag_inv: Vec<f64>,
pub size: usize,
}
#[derive(Debug)]
pub struct RsAmgHierarchy {
pub levels: Vec<RsAmgLevel>,
pub theta: f64,
pub max_levels: usize,
pub coarse_size_threshold: usize,
}
impl RsAmgHierarchy {
pub fn build(
a: CsrMatrix<f64>,
theta: f64,
max_levels: usize,
coarse_size_threshold: usize,
use_classical: bool,
) -> SparseResult<Self> {
let mut levels = Vec::new();
let mut current_a = a;
let max_levels = max_levels.max(1);
let mut coarsest_added = false;
while levels.len() < max_levels.saturating_sub(1) && current_a.shape().0 > coarse_size_threshold
{
let n = current_a.shape().0;
let diag_inv: Vec<f64> = (0..n)
.map(|i| {
let d = current_a.get(i, i);
if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 }
})
.collect();
let strong = rs_strength_of_connection(¤t_a, theta);
let labels = rs_classical_coarsening(&strong);
let (coarse_idxs, _, coarse_map) = build_coarse_map(&labels);
let n_coarse = coarse_idxs.len();
if n_coarse == 0 || n_coarse >= n {
levels.push(RsAmgLevel {
a: current_a.clone(),
p: None,
r: None,
diag_inv,
size: n,
});
coarsest_added = true;
break;
}
let p = if use_classical {
rs_classical_interpolation(¤t_a, &labels, &strong, &coarse_map, n_coarse)?
} else {
rs_direct_interpolation(¤t_a, &labels, &coarse_map, n_coarse)?
};
let r = csr_transpose(&p);
let a_coarse = galerkin_product(&r, ¤t_a, &p)?;
levels.push(RsAmgLevel {
a: current_a,
p: Some(p),
r: Some(r),
diag_inv,
size: n,
});
current_a = a_coarse;
}
if !coarsest_added {
let n = current_a.shape().0;
let diag_inv: Vec<f64> = (0..n)
.map(|i| {
let d = current_a.get(i, i);
if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 }
})
.collect();
levels.push(RsAmgLevel {
a: current_a,
p: None,
r: None,
diag_inv,
size: n,
});
}
Ok(RsAmgHierarchy {
levels,
theta,
max_levels,
coarse_size_threshold,
})
}
pub fn vcycle(&self, level: usize, b: &[f64], x: &mut Vec<f64>) -> SparseResult<()> {
let lev = &self.levels[level];
if lev.p.is_none() {
let dense = csr_to_dense(&lev.a);
let x_exact = direct_solve_small(&dense, b)?;
x.copy_from_slice(&x_exact);
return Ok(());
}
let p = lev.p.as_ref().expect("p must be present");
let r = lev.r.as_ref().expect("r must be present");
gauss_seidel_smooth(&lev.a, x, b, 2)?;
let ax = spmv(&lev.a, x)?;
let residual: Vec<f64> = b.iter().zip(ax.iter()).map(|(bi, axi)| bi - axi).collect();
let b_coarse = spmv(r, &residual)?;
let n_coarse = self.levels[level + 1].size;
let mut x_coarse = vec![0.0f64; n_coarse];
self.vcycle(level + 1, &b_coarse, &mut x_coarse)?;
let correction = spmv(p, &x_coarse)?;
for i in 0..x.len() {
x[i] += correction[i];
}
gauss_seidel_smooth_backward(&lev.a, x, b, 2)?;
Ok(())
}
pub fn wcycle(&self, level: usize, b: &[f64], x: &mut Vec<f64>) -> SparseResult<()> {
let lev = &self.levels[level];
if lev.p.is_none() {
let dense = csr_to_dense(&lev.a);
let x_exact = direct_solve_small(&dense, b)?;
x.copy_from_slice(&x_exact);
return Ok(());
}
let p = lev.p.as_ref().expect("p must be present");
let r = lev.r.as_ref().expect("r must be present");
gauss_seidel_smooth(&lev.a, x, b, 2)?;
let ax = spmv(&lev.a, x)?;
let residual: Vec<f64> = b.iter().zip(ax.iter()).map(|(bi, axi)| bi - axi).collect();
let b_coarse = spmv(r, &residual)?;
let n_coarse = self.levels[level + 1].size;
let mut x_coarse = vec![0.0f64; n_coarse];
self.wcycle(level + 1, &b_coarse, &mut x_coarse)?;
let correction1 = spmv(p, &x_coarse)?;
for i in 0..x.len() {
x[i] += correction1[i];
}
let ax2 = spmv(&lev.a, x)?;
let residual2: Vec<f64> = b.iter().zip(ax2.iter()).map(|(bi, axi)| bi - axi).collect();
let b_coarse2 = spmv(r, &residual2)?;
let mut x_coarse2 = vec![0.0f64; n_coarse];
self.wcycle(level + 1, &b_coarse2, &mut x_coarse2)?;
let correction2 = spmv(p, &x_coarse2)?;
for i in 0..x.len() {
x[i] += correction2[i];
}
gauss_seidel_smooth_backward(&lev.a, x, b, 2)?;
Ok(())
}
pub fn solve(&self, b: &[f64], config: &IterativeSolverConfig) -> SparseResult<Vec<f64>> {
let n = self.levels[0].size;
if b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: b.len(),
});
}
let mut x = vec![0.0f64; n];
for iter in 0..config.max_iter {
self.vcycle(0, b, &mut x)?;
let ax = spmv(&self.levels[0].a, &x)?;
let res_norm: f64 = b
.iter()
.zip(ax.iter())
.map(|(bi, axi)| (bi - axi).powi(2))
.sum::<f64>()
.sqrt();
let b_norm: f64 = b.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
let rel_res = if b_norm > f64::EPSILON {
res_norm / b_norm
} else {
res_norm
};
if config.verbose {
println!("RS-AMG iter {}: rel_res = {:.3e}", iter, rel_res);
}
if rel_res < config.tol {
return Ok(x);
}
}
Ok(x)
}
pub fn solve_wcycle(&self, b: &[f64], config: &IterativeSolverConfig) -> SparseResult<Vec<f64>> {
let n = self.levels[0].size;
if b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: b.len(),
});
}
let mut x = vec![0.0f64; n];
for iter in 0..config.max_iter {
self.wcycle(0, b, &mut x)?;
let ax = spmv(&self.levels[0].a, &x)?;
let res_norm: f64 = b
.iter()
.zip(ax.iter())
.map(|(bi, axi)| (bi - axi).powi(2))
.sum::<f64>()
.sqrt();
let b_norm: f64 = b.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
let rel_res = if b_norm > f64::EPSILON {
res_norm / b_norm
} else {
res_norm
};
if config.verbose {
println!("RS-AMG W-cycle iter {}: rel_res = {:.3e}", iter, rel_res);
}
if rel_res < config.tol {
return Ok(x);
}
}
Ok(x)
}
}
fn sa_strength(a: &CsrMatrix<f64>, epsilon: f64) -> Vec<Vec<usize>> {
let n = a.shape().0;
let mut strong = vec![Vec::new(); n];
let diag: Vec<f64> = (0..n).map(|i| a.get(i, i).abs()).collect();
for i in 0..n {
for pos in a.row_range(i) {
let j = a.indices[pos];
if j == i {
continue;
}
let a_ij = a.data[pos].abs();
let threshold = epsilon * (diag[i] * diag[j]).sqrt();
if a_ij >= threshold {
strong[i].push(j);
}
}
}
strong
}
fn greedy_aggregation(strong: &[Vec<usize>], n: usize) -> Vec<Option<usize>> {
let mut agg: Vec<Option<usize>> = vec![None; n];
let mut n_agg = 0usize;
for seed in 0..n {
if agg[seed].is_some() {
continue;
}
agg[seed] = Some(n_agg);
for &nb in &strong[seed] {
if agg[nb].is_none() {
agg[nb] = Some(n_agg);
}
}
n_agg += 1;
}
agg
}
fn tentative_prolongation_sa(
agg: &[Option<usize>],
n: usize,
n_agg: usize,
) -> SparseResult<CsrMatrix<f64>> {
let mut agg_sizes = vec![0usize; n_agg];
for &a in agg.iter().flatten() {
agg_sizes[a] += 1;
}
let mut rows = Vec::with_capacity(n);
let mut cols = Vec::with_capacity(n);
let mut vals = Vec::with_capacity(n);
for (i, ag) in agg.iter().enumerate() {
if let Some(k) = ag {
rows.push(i);
cols.push(*k);
let norm = (agg_sizes[*k] as f64).sqrt();
vals.push(1.0 / norm);
}
}
CsrMatrix::from_triplets(n, n_agg, rows, cols, vals)
}
fn jacobi_smooth_prolongation(
a: &CsrMatrix<f64>,
p0: &CsrMatrix<f64>,
omega: f64,
) -> SparseResult<CsrMatrix<f64>> {
let n = a.shape().0;
let n_agg = p0.shape().1;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for k in 0..n_agg {
let mut p0_col = vec![0.0f64; n];
for i in 0..n {
p0_col[i] = p0.get(i, k);
}
let ap0 = spmv(a, &p0_col)?;
let mut p_col = vec![0.0f64; n];
for i in 0..n {
let d = a.get(i, i);
let d_inv = if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 };
p_col[i] = p0_col[i] - omega * d_inv * ap0[i];
}
for (i, &v) in p_col.iter().enumerate() {
if v.abs() > 1e-15 {
rows.push(i);
cols.push(k);
vals.push(v);
}
}
}
CsrMatrix::from_triplets(n, n_agg, rows, cols, vals)
}
#[derive(Debug, Clone)]
pub struct SaAmgLevel {
pub a: CsrMatrix<f64>,
pub p: Option<CsrMatrix<f64>>,
pub r: Option<CsrMatrix<f64>>,
pub diag_inv: Vec<f64>,
pub size: usize,
}
#[derive(Debug)]
pub struct SaAmgHierarchy {
pub levels: Vec<SaAmgLevel>,
pub epsilon: f64,
pub omega: f64,
pub max_levels: usize,
pub coarse_size_threshold: usize,
}
impl SaAmgHierarchy {
pub fn build(
a: CsrMatrix<f64>,
epsilon: f64,
omega: f64,
max_levels: usize,
coarse_size_threshold: usize,
) -> SparseResult<Self> {
let mut levels = Vec::new();
let mut current_a = a;
let max_levels = max_levels.max(1);
let mut coarsest_added = false;
while levels.len() < max_levels.saturating_sub(1)
&& current_a.shape().0 > coarse_size_threshold
{
let n = current_a.shape().0;
let diag_inv: Vec<f64> = (0..n)
.map(|i| {
let d = current_a.get(i, i);
if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 }
})
.collect();
let strong = sa_strength(¤t_a, epsilon);
let agg = greedy_aggregation(&strong, n);
let n_agg = agg.iter().flatten().max().map(|&v| v + 1).unwrap_or(0);
if n_agg == 0 || n_agg >= n {
levels.push(SaAmgLevel {
a: current_a.clone(),
p: None,
r: None,
diag_inv,
size: n,
});
coarsest_added = true;
break;
}
let p0 = tentative_prolongation_sa(&agg, n, n_agg)?;
let p = jacobi_smooth_prolongation(¤t_a, &p0, omega)?;
let r = csr_transpose(&p);
let a_coarse = galerkin_product(&r, ¤t_a, &p)?;
levels.push(SaAmgLevel {
a: current_a,
p: Some(p),
r: Some(r),
diag_inv,
size: n,
});
current_a = a_coarse;
}
if !coarsest_added {
let n = current_a.shape().0;
let diag_inv: Vec<f64> = (0..n)
.map(|i| {
let d = current_a.get(i, i);
if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 }
})
.collect();
levels.push(SaAmgLevel {
a: current_a,
p: None,
r: None,
diag_inv,
size: n,
});
}
Ok(SaAmgHierarchy {
levels,
epsilon,
omega,
max_levels,
coarse_size_threshold,
})
}
pub fn vcycle(&self, level: usize, b: &[f64], x: &mut Vec<f64>) -> SparseResult<()> {
let lev = &self.levels[level];
if lev.p.is_none() {
let dense = csr_to_dense(&lev.a);
let x_exact = direct_solve_small(&dense, b)?;
x.copy_from_slice(&x_exact);
return Ok(());
}
let p = lev.p.as_ref().expect("p must be present");
let r = lev.r.as_ref().expect("r must be present");
gauss_seidel_smooth(&lev.a, x, b, 1)?;
gauss_seidel_smooth_backward(&lev.a, x, b, 1)?;
let ax = spmv(&lev.a, x)?;
let residual: Vec<f64> = b.iter().zip(ax.iter()).map(|(bi, axi)| bi - axi).collect();
let b_coarse = spmv(r, &residual)?;
let n_coarse = self.levels[level + 1].size;
let mut x_coarse = vec![0.0f64; n_coarse];
self.vcycle(level + 1, &b_coarse, &mut x_coarse)?;
let correction = spmv(p, &x_coarse)?;
for i in 0..x.len() {
x[i] += correction[i];
}
gauss_seidel_smooth_backward(&lev.a, x, b, 1)?;
gauss_seidel_smooth(&lev.a, x, b, 1)?;
Ok(())
}
pub fn solve(&self, b: &[f64], config: &IterativeSolverConfig) -> SparseResult<Vec<f64>> {
let n = self.levels[0].size;
if b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: b.len(),
});
}
let mut x = vec![0.0f64; n];
for iter in 0..config.max_iter {
self.vcycle(0, b, &mut x)?;
let ax = spmv(&self.levels[0].a, &x)?;
let res_norm: f64 = b
.iter()
.zip(ax.iter())
.map(|(bi, axi)| (bi - axi).powi(2))
.sum::<f64>()
.sqrt();
let b_norm: f64 = b.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
let rel_res = if b_norm > f64::EPSILON {
res_norm / b_norm
} else {
res_norm
};
if config.verbose {
println!("SA-AMG iter {}: rel_res = {:.3e}", iter, rel_res);
}
if rel_res < config.tol {
return Ok(x);
}
}
Ok(x)
}
}
fn air_restriction(
a: &CsrMatrix<f64>,
labels: &[CfLabel],
coarse_map: &[Option<usize>],
n_coarse: usize,
n_iter_local: usize,
) -> SparseResult<CsrMatrix<f64>> {
let n = a.shape().0;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for (i, &label) in labels.iter().enumerate() {
if label == CfLabel::Coarse {
if let Some(ci) = coarse_map[i] {
rows.push(ci);
cols.push(i);
vals.push(1.0f64);
}
}
}
for (c_node, &label) in labels.iter().enumerate() {
if label != CfLabel::Coarse {
continue;
}
let ci = match coarse_map[c_node] {
Some(v) => v,
None => continue,
};
for pos in a.row_range(c_node) {
let f_node = a.indices[pos];
if labels[f_node] != CfLabel::Fine {
continue;
}
let a_cf = a.data[pos];
let a_ff = a.get(f_node, f_node);
if a_ff.abs() < f64::EPSILON {
continue;
}
let a_fc = a.get(f_node, c_node);
let mut r_val = -a_fc / a_ff;
for _ in 1..n_iter_local {
let mut res = -a_fc;
for pos2 in a.row_range(f_node) {
let nb = a.indices[pos2];
if nb != c_node && labels[nb] == CfLabel::Fine {
let a_fn = a.data[pos2];
let _ = a_fn; }
}
r_val = res / a_ff;
let _ = res; break;
}
if r_val.abs() > 1e-15 {
rows.push(ci);
cols.push(f_node);
vals.push(r_val);
}
}
}
CsrMatrix::from_triplets(n_coarse, n, rows, cols, vals)
}
#[derive(Debug, Clone)]
pub struct AirAmgLevel {
pub a: CsrMatrix<f64>,
pub p: Option<CsrMatrix<f64>>,
pub r: Option<CsrMatrix<f64>>,
pub diag_inv: Vec<f64>,
pub size: usize,
}
#[derive(Debug)]
pub struct AirAmgHierarchy {
pub levels: Vec<AirAmgLevel>,
pub theta: f64,
pub n_iter_local: usize,
pub max_levels: usize,
pub coarse_size_threshold: usize,
}
impl AirAmgHierarchy {
pub fn build(
a: CsrMatrix<f64>,
theta: f64,
n_iter_local: usize,
max_levels: usize,
coarse_size_threshold: usize,
) -> SparseResult<Self> {
let mut levels = Vec::new();
let mut current_a = a;
let max_levels = max_levels.max(1);
let mut coarsest_added = false;
while levels.len() < max_levels.saturating_sub(1)
&& current_a.shape().0 > coarse_size_threshold
{
let n = current_a.shape().0;
let diag_inv: Vec<f64> = (0..n)
.map(|i| {
let d = current_a.get(i, i);
if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 }
})
.collect();
let strong = rs_strength_of_connection(¤t_a, theta);
let labels = rs_classical_coarsening(&strong);
let (coarse_idxs, _, coarse_map) = build_coarse_map(&labels);
let n_coarse = coarse_idxs.len();
if n_coarse == 0 || n_coarse >= n {
levels.push(AirAmgLevel {
a: current_a.clone(),
p: None,
r: None,
diag_inv,
size: n,
});
coarsest_added = true;
break;
}
let p = rs_direct_interpolation(¤t_a, &labels, &coarse_map, n_coarse)?;
let r = air_restriction(¤t_a, &labels, &coarse_map, n_coarse, n_iter_local)?;
let a_coarse = galerkin_product(&r, ¤t_a, &p)?;
levels.push(AirAmgLevel {
a: current_a,
p: Some(p),
r: Some(r),
diag_inv,
size: n,
});
current_a = a_coarse;
}
if !coarsest_added {
let n = current_a.shape().0;
let diag_inv: Vec<f64> = (0..n)
.map(|i| {
let d = current_a.get(i, i);
if d.abs() > f64::EPSILON { 1.0 / d } else { 1.0 }
})
.collect();
levels.push(AirAmgLevel {
a: current_a,
p: None,
r: None,
diag_inv,
size: n,
});
}
Ok(AirAmgHierarchy {
levels,
theta,
n_iter_local,
max_levels,
coarse_size_threshold,
})
}
fn f_relaxation(
a: &CsrMatrix<f64>,
x: &mut Vec<f64>,
b: &[f64],
diag_inv: &[f64],
labels: &[CfLabel],
n_iter: usize,
) -> SparseResult<()> {
let n = x.len();
let mut x_new = x.clone();
for _ in 0..n_iter {
let ax = spmv(a, x)?;
for i in 0..n {
if labels[i] == CfLabel::Fine {
x_new[i] = x[i] + diag_inv[i] * (b[i] - ax[i]);
}
}
x.copy_from_slice(&x_new);
}
Ok(())
}
pub fn vcycle(&self, level: usize, b: &[f64], x: &mut Vec<f64>) -> SparseResult<()> {
let lev = &self.levels[level];
if lev.p.is_none() {
let dense = csr_to_dense(&lev.a);
let x_exact = direct_solve_small(&dense, b)?;
x.copy_from_slice(&x_exact);
return Ok(());
}
let p = lev.p.as_ref().expect("p must be present");
let r = lev.r.as_ref().expect("r must be present");
let n = lev.size;
gauss_seidel_smooth(&lev.a, x, b, 2)?;
let ax = spmv(&lev.a, x)?;
let residual: Vec<f64> = b.iter().zip(ax.iter()).map(|(bi, axi)| bi - axi).collect();
let b_coarse = spmv(r, &residual)?;
let n_coarse = self.levels[level + 1].size;
let mut x_coarse = vec![0.0f64; n_coarse];
self.vcycle(level + 1, &b_coarse, &mut x_coarse)?;
let correction = spmv(p, &x_coarse)?;
for i in 0..n {
x[i] += correction[i];
}
gauss_seidel_smooth_backward(&lev.a, x, b, 2)?;
Ok(())
}
pub fn solve(&self, b: &[f64], config: &IterativeSolverConfig) -> SparseResult<Vec<f64>> {
let n = self.levels[0].size;
if b.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: b.len(),
});
}
let mut x = vec![0.0f64; n];
for iter in 0..config.max_iter {
self.vcycle(0, b, &mut x)?;
let ax = spmv(&self.levels[0].a, &x)?;
let res_norm: f64 = b
.iter()
.zip(ax.iter())
.map(|(bi, axi)| (bi - axi).powi(2))
.sum::<f64>()
.sqrt();
let b_norm: f64 = b.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
let rel_res = if b_norm > f64::EPSILON {
res_norm / b_norm
} else {
res_norm
};
if config.verbose {
println!("AIR-AMG iter {}: rel_res = {:.3e}", iter, rel_res);
}
if rel_res < config.tol {
return Ok(x);
}
}
Ok(x)
}
}
pub fn rs_amg_setup(a: CsrMatrix<f64>) -> SparseResult<RsAmgHierarchy> {
RsAmgHierarchy::build(a, 0.25, 10, 4, false)
}
pub fn sa_amg_setup(a: CsrMatrix<f64>) -> SparseResult<SaAmgHierarchy> {
SaAmgHierarchy::build(a, 0.08, 4.0 / 3.0, 10, 4)
}
pub fn air_amg_setup(a: CsrMatrix<f64>) -> SparseResult<AirAmgHierarchy> {
AirAmgHierarchy::build(a, 0.1, 3, 10, 4)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iterative_solvers::IterativeSolverConfig;
fn laplacian_1d(n: usize) -> CsrMatrix<f64> {
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..n {
rows.push(i);
cols.push(i);
vals.push(2.0f64);
}
for i in 0..n - 1 {
rows.push(i);
cols.push(i + 1);
vals.push(-1.0f64);
rows.push(i + 1);
cols.push(i);
vals.push(-1.0f64);
}
CsrMatrix::from_triplets(n, n, rows, cols, vals).expect("Build laplacian_1d")
}
#[test]
fn test_rs_amg_build() {
let a = laplacian_1d(16);
let hier = rs_amg_setup(a).expect("RS-AMG setup");
assert!(hier.levels.len() >= 2, "should have at least 2 levels");
assert!(hier.levels[0].p.is_some(), "finest level must have prolongation");
assert!(hier.levels.last().expect("last level").p.is_none(), "coarsest has no prolongation");
}
#[test]
fn test_rs_amg_vcycle_convergence() {
let n = 16;
let a = laplacian_1d(n);
let hier = RsAmgHierarchy::build(a.clone(), 0.25, 6, 2, false).expect("RS-AMG setup");
let b: Vec<f64> = vec![1.0f64; n];
let config = IterativeSolverConfig {
max_iter: 50,
tol: 1e-8,
verbose: false,
};
let x = hier.solve(&b, &config).expect("RS-AMG solve");
assert_eq!(x.len(), n);
let ax = a.dot(&x).expect("spmv");
let res: f64 = b.iter().zip(ax.iter()).map(|(bi, axi)| (bi - axi).powi(2)).sum::<f64>().sqrt();
let b_norm: f64 = b.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
assert!(res / b_norm < 5e-2, "residual too large: {}", res / b_norm);
}
#[test]
fn test_rs_amg_wcycle() {
let n = 16;
let a = laplacian_1d(n);
let hier = RsAmgHierarchy::build(a.clone(), 0.25, 6, 2, false).expect("RS-AMG setup");
let b: Vec<f64> = vec![1.0f64; n];
let config = IterativeSolverConfig {
max_iter: 30,
tol: 1e-8,
verbose: false,
};
let x = hier.solve_wcycle(&b, &config).expect("RS-AMG W-cycle solve");
let ax = a.dot(&x).expect("spmv");
let res: f64 = b.iter().zip(ax.iter()).map(|(bi, axi)| (bi - axi).powi(2)).sum::<f64>().sqrt();
let b_norm: f64 = b.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
assert!(res / b_norm < 1e-1, "W-cycle residual too large: {}", res / b_norm);
}
#[test]
fn test_sa_amg_build() {
let a = laplacian_1d(16);
let hier = sa_amg_setup(a).expect("SA-AMG setup");
assert!(hier.levels.len() >= 1);
}
#[test]
fn test_sa_amg_solve() {
let n = 16;
let a = laplacian_1d(n);
let hier = SaAmgHierarchy::build(a.clone(), 0.08, 4.0 / 3.0, 8, 2).expect("SA-AMG setup");
let b: Vec<f64> = vec![1.0f64; n];
let config = IterativeSolverConfig {
max_iter: 50,
tol: 1e-6,
verbose: false,
};
let x = hier.solve(&b, &config).expect("SA-AMG solve");
let ax = a.dot(&x).expect("spmv");
let res: f64 = b.iter().zip(ax.iter()).map(|(bi, axi)| (bi - axi).powi(2)).sum::<f64>().sqrt();
let b_norm: f64 = b.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
assert!(res / b_norm < 1e-3, "SA-AMG residual too large: {}", res / b_norm);
}
#[test]
fn test_air_amg_build() {
let a = laplacian_1d(16);
let hier = air_amg_setup(a).expect("AIR-AMG setup");
assert!(hier.levels.len() >= 1);
}
#[test]
fn test_direct_solve_small() {
let dense = vec![
vec![2.0, -1.0, 0.0],
vec![-1.0, 2.0, -1.0],
vec![0.0, -1.0, 2.0],
];
let rhs = vec![1.0, 0.0, 1.0];
let x = direct_solve_small(&dense, &rhs).expect("direct solve");
for (i, row) in dense.iter().enumerate() {
let sum: f64 = row.iter().zip(x.iter()).map(|(a, xi)| a * xi).sum();
assert!((sum - rhs[i]).abs() < 1e-10, "residual too large at row {}", i);
}
}
#[test]
fn test_strength_of_connection() {
let a = laplacian_1d(8);
let strong = rs_strength_of_connection(&a, 0.25);
assert!(!strong[3].is_empty(), "interior node should have strong connections");
}
#[test]
fn test_cf_splitting() {
let a = laplacian_1d(8);
let strong = rs_strength_of_connection(&a, 0.25);
let labels = rs_classical_coarsening(&strong);
let coarse_count = labels.iter().filter(|&&l| l == CfLabel::Coarse).count();
assert!(coarse_count > 0, "must have at least one coarse node");
assert!(coarse_count < 8, "must have at least one fine node");
}
}