use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::{Float, NumAssign, One, Zero};
use std::iter::Sum;
use crate::error::{LinalgError, LinalgResult};
pub trait PrecondApply {
fn apply(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>>;
fn apply_transpose(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
self.apply(x)
}
fn size(&self) -> usize;
}
#[derive(Debug, Clone)]
pub struct SPAI {
m: Array2<f64>,
n: usize,
pub frobenius_residual: f64,
}
impl SPAI {
pub fn new(
a: &ArrayView2<f64>,
bandwidth: usize,
n_fill_steps: usize,
drop_tol: Option<f64>,
) -> LinalgResult<Self> {
let (m_rows, n) = a.dim();
if m_rows != n {
return Err(LinalgError::ShapeError(
"SPAI: A must be square".to_string(),
));
}
if n == 0 {
return Ok(Self {
m: Array2::zeros((0, 0)),
n: 0,
frobenius_residual: 0.0,
});
}
let tol = drop_tol.unwrap_or(1e-3);
let mut m_mat = Array2::zeros((n, n));
for j in 0..n {
let mut pattern: Vec<usize> = {
let lo = j.saturating_sub(bandwidth);
let hi = (j + bandwidth + 1).min(n);
(lo..hi).collect()
};
for _step in 0..n_fill_steps {
let col = Self::solve_column(a, j, &pattern)?;
let mut new_rows: Vec<(f64, usize)> = Vec::new();
for i in 0..n {
if pattern.contains(&i) {
continue;
}
let mut val = 0.0f64;
for (pi, &p) in pattern.iter().enumerate() {
val += a[[i, p]] * col[pi];
}
if i == j {
val -= 1.0;
}
new_rows.push((val.abs(), i));
}
new_rows.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let n_new = (bandwidth + 1).min(new_rows.len());
for k in 0..n_new {
let (val, row) = new_rows[k];
if val > 1e-10 {
pattern.push(row);
}
}
pattern.sort_unstable();
pattern.dedup();
}
let col = Self::solve_column(a, j, &pattern)?;
let col_norm = col.iter().map(|&v| v * v).sum::<f64>().sqrt();
for (pi, &p) in pattern.iter().enumerate() {
if col[pi].abs() >= tol * col_norm {
m_mat[[p, j]] = col[pi];
}
}
}
let mut frob_sq = 0.0f64;
for i in 0..n {
for j in 0..n {
let mut ma_ij = 0.0f64;
for k in 0..n {
ma_ij += m_mat[[i, k]] * a[[k, j]];
}
let delta = if i == j { 1.0 } else { 0.0 };
frob_sq += (ma_ij - delta).powi(2);
}
}
Ok(Self {
m: m_mat,
n,
frobenius_residual: frob_sq.sqrt(),
})
}
fn solve_column(
a: &ArrayView2<f64>,
j: usize,
pattern: &[usize],
) -> LinalgResult<Vec<f64>> {
let n = a.nrows();
let p = pattern.len();
if p == 0 {
return Ok(Vec::new());
}
let mut a_sub = vec![0.0f64; n * p];
for k in 0..p {
for i in 0..n {
a_sub[i * p + k] = a[[i, pattern[k]]];
}
}
let mut ej = vec![0.0f64; n];
ej[j] = 1.0;
let mut g = vec![0.0f64; p * p];
for i in 0..p {
for k in 0..p {
let mut val = 0.0f64;
for row in 0..n {
val += a_sub[row * p + i] * a_sub[row * p + k];
}
g[i * p + k] = val;
}
}
let mut rhs = vec![0.0f64; p];
for k in 0..p {
rhs[k] = a_sub[j * p + k]; }
let regularization = 1e-12 * g.iter().enumerate()
.filter(|(i, _)| i % (p + 1) == 0)
.map(|(_, &v)| v.abs())
.sum::<f64>()
.max(1e-12);
for i in 0..p {
g[i * p + i] += regularization;
}
let x = cholesky_solve_small(&g, &rhs, p)?;
Ok(x)
}
pub fn matrix(&self) -> &Array2<f64> {
&self.m
}
}
impl PrecondApply for SPAI {
fn apply(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
if x.len() != self.n {
return Err(LinalgError::DimensionError(format!(
"SPAI::apply: x has length {} but n={}",
x.len(),
self.n
)));
}
let mut y = Array1::zeros(self.n);
for i in 0..self.n {
let mut val = 0.0f64;
for j in 0..self.n {
val += self.m[[i, j]] * x[j];
}
y[i] = val;
}
Ok(y)
}
fn size(&self) -> usize {
self.n
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PolynomialType {
Neumann,
Chebyshev {
lambda_min: f64,
lambda_max: f64,
},
}
#[derive(Debug, Clone)]
pub struct PolynomialAdvancedPreconditioner {
pub poly_type: PolynomialType,
pub degree: usize,
n: usize,
diag_inv: Array1<f64>,
cheb_coeffs: Option<Vec<f64>>,
}
impl PolynomialAdvancedPreconditioner {
pub fn new_neumann(a: &ArrayView2<f64>, degree: usize) -> LinalgResult<Self> {
let (m, n) = a.dim();
if m != n {
return Err(LinalgError::ShapeError(
"PolynomialAdvancedPreconditioner: A must be square".to_string(),
));
}
let mut diag_inv = Array1::zeros(n);
for i in 0..n {
let d = a[[i, i]];
if d.abs() < 1e-300 {
diag_inv[i] = 1.0;
} else {
diag_inv[i] = 1.0 / d;
}
}
Ok(Self {
poly_type: PolynomialType::Neumann,
degree,
n,
diag_inv,
cheb_coeffs: None,
})
}
pub fn new_chebyshev(
a: &ArrayView2<f64>,
degree: usize,
lambda_min: f64,
lambda_max: f64,
) -> LinalgResult<Self> {
let (m, n) = a.dim();
if m != n {
return Err(LinalgError::ShapeError(
"PolynomialAdvancedPreconditioner: A must be square".to_string(),
));
}
if lambda_min <= 0.0 || lambda_max <= lambda_min {
return Err(LinalgError::InvalidInputError(format!(
"Chebyshev: need 0 < lambda_min < lambda_max, got {} < {}",
lambda_min, lambda_max
)));
}
let alpha = (lambda_max + lambda_min) / 2.0;
let beta = (lambda_max - lambda_min) / 2.0;
let coeffs = vec![alpha, beta, 0.0];
let mut diag_inv = Array1::zeros(n);
for i in 0..n {
let d = a[[i, i]];
if d.abs() < 1e-300 {
diag_inv[i] = 1.0;
} else {
diag_inv[i] = 1.0 / d;
}
}
Ok(Self {
poly_type: PolynomialType::Chebyshev {
lambda_min,
lambda_max,
},
degree,
n,
diag_inv,
cheb_coeffs: Some(coeffs),
})
}
pub fn apply_matrixfree<F>(&self, matvec: F, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>>
where
F: Fn(&ArrayView1<f64>) -> LinalgResult<Array1<f64>>,
{
if x.len() != self.n {
return Err(LinalgError::DimensionError(format!(
"PolynomialAdvancedPreconditioner: x has length {} but n={}",
x.len(),
self.n
)));
}
match self.poly_type {
PolynomialType::Neumann => self.apply_neumann_matrixfree(&matvec, x),
PolynomialType::Chebyshev { .. } => self.apply_chebyshev_matrixfree(&matvec, x),
}
}
fn apply_neumann_matrixfree<F>(
&self,
matvec: &F,
x: &ArrayView1<f64>,
) -> LinalgResult<Array1<f64>>
where
F: Fn(&ArrayView1<f64>) -> LinalgResult<Array1<f64>>,
{
let dx: Array1<f64> = x.iter().zip(self.diag_inv.iter()).map(|(&xi, &di)| di * xi).collect();
let mut result = dx.clone();
let mut term = dx;
for _ in 0..self.degree {
let at = matvec(&term.view())?;
let dat: Array1<f64> = at.iter().zip(self.diag_inv.iter()).map(|(&v, &d)| d * v).collect();
term = &term - &dat;
result = result + &term;
let term_norm = term.iter().map(|&v| v * v).sum::<f64>().sqrt();
if term_norm < 1e-15 {
break;
}
}
Ok(result)
}
fn apply_chebyshev_matrixfree<F>(
&self,
matvec: &F,
x: &ArrayView1<f64>,
) -> LinalgResult<Array1<f64>>
where
F: Fn(&ArrayView1<f64>) -> LinalgResult<Array1<f64>>,
{
let coeffs = self.cheb_coeffs.as_ref().ok_or_else(|| {
LinalgError::ComputationError("Chebyshev coefficients not initialized".to_string())
})?;
let alpha = coeffs[0]; let beta = coeffs[1];
if alpha.abs() < 1e-300 {
return Err(LinalgError::ComputationError(
"Chebyshev: spectral center is zero".to_string(),
));
}
let mu = (beta / alpha).powi(2) / 2.0;
let mut p_prev = Array1::zeros(self.n);
let mut p_curr: Array1<f64> = x.iter().map(|&v| 2.0 / alpha * v).collect();
let mut rho_prev = 1.0f64;
for _ in 0..self.degree.saturating_sub(1) {
let rho_curr = 1.0 / (1.0 - mu * rho_prev);
let ap = matvec(&p_curr.view())?;
let mut p_next = Array1::zeros(self.n);
for i in 0..self.n {
let residual = x[i] - ap[i];
p_next[i] = rho_curr * (2.0 / alpha * residual + rho_prev * p_prev[i]);
}
p_prev = p_curr;
p_curr = p_next;
rho_prev = rho_curr;
}
Ok(p_curr)
}
}
impl PrecondApply for PolynomialAdvancedPreconditioner {
fn apply(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
Err(LinalgError::NotImplementedError(
"PolynomialAdvancedPreconditioner::apply requires a matrix; use apply_matrixfree instead".to_string(),
))
}
fn size(&self) -> usize {
self.n
}
}
impl PolynomialAdvancedPreconditioner {
pub fn apply_with_matrix(
&self,
a: &ArrayView2<f64>,
x: &ArrayView1<f64>,
) -> LinalgResult<Array1<f64>> {
let matvec = |v: &ArrayView1<f64>| -> LinalgResult<Array1<f64>> {
let (m, n) = a.dim();
let mut result = Array1::zeros(m);
for i in 0..m {
let mut val = 0.0f64;
for j in 0..n {
val += a[[i, j]] * v[j];
}
result[i] = val;
}
Ok(result)
};
self.apply_matrixfree(matvec, x)
}
}
#[derive(Debug, Clone)]
pub struct BlockDiagonalPreconditioner {
blocks: Vec<Array2<f64>>,
block_starts: Vec<usize>,
block_sizes: Vec<usize>,
n: usize,
}
impl BlockDiagonalPreconditioner {
pub fn new_uniform(a: &ArrayView2<f64>, block_size: usize) -> LinalgResult<Self> {
let (m, n) = a.dim();
if m != n {
return Err(LinalgError::ShapeError(
"BlockDiagonalPreconditioner: A must be square".to_string(),
));
}
if block_size == 0 || block_size > n {
return Err(LinalgError::InvalidInputError(format!(
"BlockDiagonalPreconditioner: block_size {} is invalid for n={}",
block_size, n
)));
}
let n_blocks = (n + block_size - 1) / block_size;
let block_starts: Vec<usize> = (0..n_blocks).map(|i| i * block_size).collect();
let block_sizes: Vec<usize> = (0..n_blocks)
.map(|i| {
let start = i * block_size;
let end = (start + block_size).min(n);
end - start
})
.collect();
let mut blocks = Vec::with_capacity(n_blocks);
for (b, &start) in block_starts.iter().enumerate() {
let bsize = block_sizes[b];
let end = start + bsize;
let block = a.slice(scirs2_core::ndarray::s![start..end, start..end]).to_owned();
let block_inv = block_inverse(&block.view())?;
blocks.push(block_inv);
}
Ok(Self {
blocks,
block_starts,
block_sizes,
n,
})
}
pub fn new_variable(a: &ArrayView2<f64>, block_sizes: Vec<usize>) -> LinalgResult<Self> {
let (m, n) = a.dim();
if m != n {
return Err(LinalgError::ShapeError(
"BlockDiagonalPreconditioner: A must be square".to_string(),
));
}
let total: usize = block_sizes.iter().sum();
if total != n {
return Err(LinalgError::DimensionError(format!(
"BlockDiagonalPreconditioner: block sizes sum to {} but n={}",
total, n
)));
}
let mut block_starts = Vec::with_capacity(block_sizes.len());
let mut start = 0;
for &bs in &block_sizes {
block_starts.push(start);
start += bs;
}
let mut blocks = Vec::with_capacity(block_sizes.len());
for (b, &bs) in block_sizes.iter().enumerate() {
let s = block_starts[b];
let e = s + bs;
let block = a.slice(scirs2_core::ndarray::s![s..e, s..e]).to_owned();
let block_inv = block_inverse(&block.view())?;
blocks.push(block_inv);
}
Ok(Self {
blocks,
block_starts,
block_sizes,
n,
})
}
}
impl PrecondApply for BlockDiagonalPreconditioner {
fn apply(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
if x.len() != self.n {
return Err(LinalgError::DimensionError(format!(
"BlockDiagonalPreconditioner::apply: x has {} elements but n={}",
x.len(),
self.n
)));
}
let mut y = Array1::zeros(self.n);
for (b, &start) in self.block_starts.iter().enumerate() {
let bs = self.block_sizes[b];
let end = start + bs;
let x_b = x.slice(scirs2_core::ndarray::s![start..end]);
let block_inv = &self.blocks[b];
for i in 0..bs {
let mut val = 0.0f64;
for j in 0..bs {
val += block_inv[[i, j]] * x_b[j];
}
y[start + i] = val;
}
}
Ok(y)
}
fn size(&self) -> usize {
self.n
}
}
#[derive(Debug, Clone)]
pub struct SchurComplementPreconditioner {
n1: usize,
n2: usize,
n: usize,
a11_inv: Array2<f64>,
a12: Array2<f64>,
a21: Array2<f64>,
schur_inv: Array2<f64>,
}
impl SchurComplementPreconditioner {
pub fn new(
a: &ArrayView2<f64>,
n1: usize,
approx_a11_inv: Option<Array2<f64>>,
approx_schur_inv: Option<Array2<f64>>,
) -> LinalgResult<Self> {
let (m, n) = a.dim();
if m != n {
return Err(LinalgError::ShapeError(
"SchurComplementPreconditioner: A must be square".to_string(),
));
}
if n1 == 0 || n1 >= n {
return Err(LinalgError::InvalidInputError(format!(
"SchurComplementPreconditioner: n1={} must satisfy 0 < n1 < n={}",
n1, n
)));
}
let n2 = n - n1;
let a11 = a.slice(scirs2_core::ndarray::s![..n1, ..n1]).to_owned();
let a12 = a.slice(scirs2_core::ndarray::s![..n1, n1..]).to_owned();
let a21 = a.slice(scirs2_core::ndarray::s![n1.., ..n1]).to_owned();
let a22 = a.slice(scirs2_core::ndarray::s![n1.., n1..]).to_owned();
let a11_inv = match approx_a11_inv {
Some(inv) => inv,
None => block_inverse(&a11.view())?,
};
let mut a21_a11inv = Array2::zeros((n2, n1));
for i in 0..n2 {
for j in 0..n1 {
let mut val = 0.0f64;
for k in 0..n1 {
val += a21[[i, k]] * a11_inv[[k, j]];
}
a21_a11inv[[i, j]] = val;
}
}
let mut schur = a22.clone();
for i in 0..n2 {
for j in 0..n2 {
let mut val = 0.0f64;
for k in 0..n1 {
val += a21_a11inv[[i, k]] * a12[[k, j]];
}
schur[[i, j]] -= val;
}
}
let schur_inv = match approx_schur_inv {
Some(inv) => inv,
None => block_inverse(&schur.view())?,
};
Ok(Self {
n1,
n2,
n,
a11_inv,
a12,
a21,
schur_inv,
})
}
pub fn schur_inv(&self) -> &Array2<f64> {
&self.schur_inv
}
}
impl PrecondApply for SchurComplementPreconditioner {
fn apply(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
if x.len() != self.n {
return Err(LinalgError::DimensionError(format!(
"SchurComplementPreconditioner::apply: x has {} elements but n={}",
x.len(),
self.n
)));
}
let x1 = x.slice(scirs2_core::ndarray::s![..self.n1]);
let x2 = x.slice(scirs2_core::ndarray::s![self.n1..]);
let mut a11inv_x1 = Array1::zeros(self.n1);
for i in 0..self.n1 {
let mut val = 0.0f64;
for j in 0..self.n1 {
val += self.a11_inv[[i, j]] * x1[j];
}
a11inv_x1[i] = val;
}
let mut z2 = Array1::zeros(self.n2);
for i in 0..self.n2 {
let mut a21_val = 0.0f64;
for j in 0..self.n1 {
a21_val += self.a21[[i, j]] * a11inv_x1[j];
}
z2[i] = x2[i] - a21_val;
}
let mut w2 = Array1::zeros(self.n2);
for i in 0..self.n2 {
let mut val = 0.0f64;
for j in 0..self.n2 {
val += self.schur_inv[[i, j]] * z2[j];
}
w2[i] = val;
}
let mut a12_w2 = Array1::zeros(self.n1);
for i in 0..self.n1 {
let mut val = 0.0f64;
for j in 0..self.n2 {
val += self.a12[[i, j]] * w2[j];
}
a12_w2[i] = val;
}
let mut a11inv_a12w2 = Array1::zeros(self.n1);
for i in 0..self.n1 {
let mut val = 0.0f64;
for j in 0..self.n1 {
val += self.a11_inv[[i, j]] * a12_w2[j];
}
a11inv_a12w2[i] = val;
}
let mut y = Array1::zeros(self.n);
for i in 0..self.n1 {
y[i] = a11inv_x1[i] - a11inv_a12w2[i];
}
for i in 0..self.n2 {
y[self.n1 + i] = w2[i];
}
Ok(y)
}
fn size(&self) -> usize {
self.n
}
}
#[derive(Debug, Clone)]
struct AMGLevel {
a: Array2<f64>,
p: Array2<f64>,
r: Array2<f64>,
diag_inv: Array1<f64>,
n: usize,
}
#[derive(Debug, Clone)]
pub struct AMGPreconditioner {
n: usize,
smoothing_steps: usize,
levels: Vec<AMGLevel>,
max_coarse_size: usize,
strength_threshold: f64,
}
impl AMGPreconditioner {
pub fn new(a: &ArrayView2<f64>, smoothing_steps: Option<usize>) -> LinalgResult<Self> {
Self::with_options(a, smoothing_steps, None, None)
}
pub fn with_options(
a: &ArrayView2<f64>,
smoothing_steps: Option<usize>,
max_coarse_size: Option<usize>,
strength_threshold: Option<f64>,
) -> LinalgResult<Self> {
let (m, n) = a.dim();
if m != n {
return Err(LinalgError::ShapeError(
"AMGPreconditioner: A must be square".to_string(),
));
}
let steps = smoothing_steps.unwrap_or(2);
let coarse_max = max_coarse_size.unwrap_or(32);
let theta = strength_threshold.unwrap_or(0.25);
let mut precond = Self {
n,
smoothing_steps: steps,
levels: Vec::new(),
max_coarse_size: coarse_max,
strength_threshold: theta,
};
precond.build_hierarchy(&a.to_owned())?;
Ok(precond)
}
fn build_hierarchy(&mut self, a_fine: &Array2<f64>) -> LinalgResult<()> {
let mut a_current = a_fine.clone();
let max_levels = 20;
for _ in 0..max_levels {
let n_cur = a_current.nrows();
let diag_inv = Self::compute_diag_inv(&a_current);
if n_cur <= self.max_coarse_size {
self.levels.push(AMGLevel {
a: a_current.clone(),
p: Array2::zeros((0, 0)),
r: Array2::zeros((0, 0)),
diag_inv,
n: n_cur,
});
break;
}
let strong = Self::strength_of_connection(&a_current, self.strength_threshold);
let cf_splitting = Self::coarsen(&strong, n_cur);
let n_coarse: usize = cf_splitting.iter().filter(|&&c| c).count();
if n_coarse == 0 || n_coarse >= n_cur {
self.levels.push(AMGLevel {
a: a_current.clone(),
p: Array2::zeros((0, 0)),
r: Array2::zeros((0, 0)),
diag_inv,
n: n_cur,
});
break;
}
let p = Self::build_interpolation(&a_current, &strong, &cf_splitting, n_coarse);
let r = p.t().to_owned();
let ap = Self::matmul_dense(&a_current, &p);
let a_coarse = Self::matmul_dense(&r, &ap);
self.levels.push(AMGLevel {
a: a_current.clone(),
p: p.clone(),
r,
diag_inv,
n: n_cur,
});
a_current = a_coarse;
}
if self.levels.is_empty() {
let diag_inv = Self::compute_diag_inv(a_fine);
self.levels.push(AMGLevel {
a: a_fine.clone(),
p: Array2::zeros((0, 0)),
r: Array2::zeros((0, 0)),
diag_inv,
n: a_fine.nrows(),
});
}
Ok(())
}
fn compute_diag_inv(a: &Array2<f64>) -> Array1<f64> {
let n = a.nrows();
let mut diag_inv = Array1::zeros(n);
for i in 0..n {
let d = a[[i, i]];
if d.abs() < 1e-300 {
diag_inv[i] = 1.0;
} else {
diag_inv[i] = 1.0 / d;
}
}
diag_inv
}
fn strength_of_connection(a: &Array2<f64>, theta: f64) -> Vec<Vec<bool>> {
let n = a.nrows();
let mut strong = vec![vec![false; n]; n];
for i in 0..n {
let mut max_off = 0.0_f64;
for j in 0..n {
if j != i {
let v = a[[i, j]].abs();
if v > max_off {
max_off = v;
}
}
}
let threshold = theta * max_off;
for j in 0..n {
if j != i && a[[i, j]].abs() >= threshold {
strong[i][j] = true;
}
}
}
strong
}
fn coarsen(strong: &[Vec<bool>], n: usize) -> Vec<bool> {
let mut status = vec![0u8; n];
let mut lambda: Vec<usize> = (0..n)
.map(|i| strong[i].iter().filter(|&&s| s).count())
.collect();
loop {
let mut best_i = None;
let mut best_lambda = 0;
for i in 0..n {
if status[i] == 0 && lambda[i] >= best_lambda {
best_lambda = lambda[i];
best_i = Some(i);
}
}
let i = match best_i {
Some(idx) => idx,
None => break, };
status[i] = 1;
for j in 0..n {
if strong[i][j] && status[j] == 0 {
status[j] = 2;
for k in 0..n {
if strong[j][k] && status[k] == 0 {
lambda[k] = lambda[k].saturating_add(1);
}
}
}
}
lambda[i] = 0;
for j in 0..n {
if strong[i][j] && status[j] == 2 {
lambda[j] = 0;
}
}
}
status.iter().map(|&s| s == 1).collect()
}
fn build_interpolation(
a: &Array2<f64>,
strong: &[Vec<bool>],
cf_splitting: &[bool],
n_coarse: usize,
) -> Array2<f64> {
let n = a.nrows();
let mut coarse_idx = vec![0usize; n];
let mut idx = 0;
for i in 0..n {
if cf_splitting[i] {
coarse_idx[i] = idx;
idx += 1;
}
}
let mut p = Array2::zeros((n, n_coarse));
for i in 0..n {
if cf_splitting[i] {
p[[i, coarse_idx[i]]] = 1.0;
} else {
let mut strong_coarse_neighbors = Vec::new();
let mut weight_sum = 0.0_f64;
for j in 0..n {
if strong[i][j] && cf_splitting[j] {
let w = a[[i, j]].abs();
strong_coarse_neighbors.push((j, w));
weight_sum += w;
}
}
if weight_sum > 1e-300 && !strong_coarse_neighbors.is_empty() {
let diag = a[[i, i]];
let sign_factor = if diag > 0.0 { -1.0 } else { 1.0 };
for (j, w) in &strong_coarse_neighbors {
let normalized = sign_factor * (*w / weight_sum);
let strong_sum: f64 = strong_coarse_neighbors.iter().map(|(_, ww)| ww).sum();
let scale = if diag.abs() > 1e-300 {
strong_sum / diag.abs()
} else {
1.0
};
p[[i, coarse_idx[*j]]] = normalized * scale;
}
} else if !strong_coarse_neighbors.is_empty() {
let w = 1.0 / strong_coarse_neighbors.len() as f64;
for (j, _) in &strong_coarse_neighbors {
p[[i, coarse_idx[*j]]] = w;
}
}
}
}
p
}
fn matmul_dense(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
let m = a.nrows();
let k = a.ncols();
let n = b.ncols();
let mut c = Array2::zeros((m, n));
for i in 0..m {
for p in 0..k {
let aip = a[[i, p]];
if aip.abs() < 1e-300 {
continue;
}
for j in 0..n {
c[[i, j]] += aip * b[[p, j]];
}
}
}
c
}
fn vcycle(&self, level: usize, b: &Array1<f64>, x: &mut Array1<f64>) {
let lev = &self.levels[level];
let n = lev.n;
if level == self.levels.len() - 1 || lev.p.nrows() == 0 {
let coarse_iters = if n <= 4 { 50 } else { 20 };
Self::gauss_seidel(&lev.a, &lev.diag_inv, b, x, coarse_iters);
return;
}
Self::gauss_seidel(&lev.a, &lev.diag_inv, b, x, self.smoothing_steps);
let ax = Self::matvec_dense(&lev.a, x);
let r: Array1<f64> = Array1::from_iter(
b.iter().zip(ax.iter()).map(|(&bi, &ai)| bi - ai),
);
let r_coarse = Self::matvec_dense(&lev.r, &r);
let n_coarse = self.levels[level + 1].n;
let mut e_coarse = Array1::zeros(n_coarse);
self.vcycle(level + 1, &r_coarse, &mut e_coarse);
let correction = Self::matvec_dense(&lev.p, &e_coarse);
for i in 0..n {
x[i] += correction[i];
}
Self::gauss_seidel_backward(&lev.a, &lev.diag_inv, b, x, self.smoothing_steps);
}
fn matvec_dense(a: &Array2<f64>, x: &Array1<f64>) -> Array1<f64> {
let m = a.nrows();
let n = a.ncols();
let mut y = Array1::zeros(m);
for i in 0..m {
let mut s = 0.0_f64;
for j in 0..n {
s += a[[i, j]] * x[j];
}
y[i] = s;
}
y
}
fn gauss_seidel(
a: &Array2<f64>,
diag_inv: &Array1<f64>,
b: &Array1<f64>,
x: &mut Array1<f64>,
steps: usize,
) {
let n = a.nrows();
for _ in 0..steps {
for i in 0..n {
let mut s = 0.0_f64;
for j in 0..n {
if j != i {
s += a[[i, j]] * x[j];
}
}
x[i] = (b[i] - s) * diag_inv[i];
}
}
}
fn gauss_seidel_backward(
a: &Array2<f64>,
diag_inv: &Array1<f64>,
b: &Array1<f64>,
x: &mut Array1<f64>,
steps: usize,
) {
let n = a.nrows();
for _ in 0..steps {
for i in (0..n).rev() {
let mut s = 0.0_f64;
for j in 0..n {
if j != i {
s += a[[i, j]] * x[j];
}
}
x[i] = (b[i] - s) * diag_inv[i];
}
}
}
}
impl PrecondApply for AMGPreconditioner {
fn apply(&self, b: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
if b.len() != self.n {
return Err(LinalgError::DimensionError(format!(
"AMGPreconditioner::apply: b has {} elements but n={}",
b.len(),
self.n
)));
}
let mut x = Array1::zeros(self.n);
let b_owned = b.to_owned();
self.vcycle(0, &b_owned, &mut x);
Ok(x)
}
fn size(&self) -> usize {
self.n
}
}
fn block_inverse(a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
let n = a.nrows();
if n == 0 {
return Ok(Array2::zeros((0, 0)));
}
if n == 1 {
let v = a[[0, 0]];
if v.abs() < 1e-300 {
return Err(LinalgError::SingularMatrixError(
"block_inverse: 1×1 block is zero".to_string(),
));
}
let mut result = Array2::zeros((1, 1));
result[[0, 0]] = 1.0 / v;
return Ok(result);
}
let mut lu = a.to_owned();
let mut pivot = vec![0usize; n];
for k in 0..n {
let mut max_val = lu[[k, k]].abs();
let mut max_row = k;
for i in (k + 1)..n {
let v = lu[[i, k]].abs();
if v > max_val {
max_val = v;
max_row = i;
}
}
pivot[k] = max_row;
if max_row != k {
for j in 0..n {
let tmp = lu[[k, j]];
lu[[k, j]] = lu[[max_row, j]];
lu[[max_row, j]] = tmp;
}
}
let diag = lu[[k, k]];
if diag.abs() < 1e-300 {
return Err(LinalgError::SingularMatrixError(format!(
"block_inverse: singular at column {k}"
)));
}
for i in (k + 1)..n {
lu[[i, k]] /= diag;
for j in (k + 1)..n {
let lk = lu[[i, k]];
let lkj = lu[[k, j]];
lu[[i, j]] -= lk * lkj;
}
}
}
let mut inv = Array2::eye(n);
for k in 0..n {
let p = pivot[k];
if p != k {
for j in 0..n {
let tmp = inv[[k, j]];
inv[[k, j]] = inv[[p, j]];
inv[[p, j]] = tmp;
}
}
}
for k in 0..n {
for i in (k + 1)..n {
let lk = lu[[i, k]];
for j in 0..n {
let ik = inv[[k, j]];
inv[[i, j]] -= lk * ik;
}
}
}
for k in (0..n).rev() {
let diag = lu[[k, k]];
for j in 0..n {
inv[[k, j]] /= diag;
}
for i in 0..k {
let uk = lu[[i, k]];
for j in 0..n {
let kj = inv[[k, j]];
inv[[i, j]] -= uk * kj;
}
}
}
Ok(inv)
}
fn cholesky_solve_small(a: &[f64], b: &[f64], n: usize) -> LinalgResult<Vec<f64>> {
let mut l = vec![0.0f64; n * n];
let mut chol_ok = true;
for i in 0..n {
for j in 0..=i {
let mut sum = a[i * n + j];
for k in 0..j {
sum -= l[i * n + k] * l[j * n + k];
}
if i == j {
if sum < 0.0 {
chol_ok = false;
break;
}
l[i * n + j] = sum.sqrt();
} else {
let ljj = l[j * n + j];
if ljj.abs() < 1e-300 {
chol_ok = false;
break;
}
l[i * n + j] = sum / ljj;
}
}
if !chol_ok {
break;
}
}
if !chol_ok {
return gauss_solve_small(a, b, n);
}
let mut y = b.to_vec();
for i in 0..n {
let mut s = y[i];
for j in 0..i {
s -= l[i * n + j] * y[j];
}
let lii = l[i * n + i];
if lii.abs() < 1e-300 {
return Err(LinalgError::SingularMatrixError(
"cholesky_solve_small: zero diagonal in L".to_string(),
));
}
y[i] = s / lii;
}
let mut x = y;
for i in (0..n).rev() {
let mut s = x[i];
for j in (i + 1)..n {
s -= l[j * n + i] * x[j];
}
let lii = l[i * n + i];
x[i] = s / lii;
}
Ok(x)
}
fn gauss_solve_small(a: &[f64], b: &[f64], n: usize) -> LinalgResult<Vec<f64>> {
let mut aug: Vec<f64> = vec![0.0; n * (n + 1)];
for i in 0..n {
for j in 0..n {
aug[i * (n + 1) + j] = a[i * n + j];
}
aug[i * (n + 1) + n] = b[i];
}
for k in 0..n {
let mut max_val = aug[k * (n + 1) + k].abs();
let mut max_row = k;
for i in (k + 1)..n {
let v = aug[i * (n + 1) + k].abs();
if v > max_val {
max_val = v;
max_row = i;
}
}
if max_row != k {
for j in 0..=(n) {
aug.swap(k * (n + 1) + j, max_row * (n + 1) + j);
}
}
let pivot = aug[k * (n + 1) + k];
if pivot.abs() < 1e-300 {
return Err(LinalgError::SingularMatrixError(format!(
"gauss_solve_small: singular at column {k}"
)));
}
for i in (k + 1)..n {
let factor = aug[i * (n + 1) + k] / pivot;
for j in k..=(n) {
let akj = aug[k * (n + 1) + j];
aug[i * (n + 1) + j] -= factor * akj;
}
}
}
let mut x = vec![0.0f64; n];
for i in (0..n).rev() {
let mut s = aug[i * (n + 1) + n];
for j in (i + 1)..n {
s -= aug[i * (n + 1) + j] * x[j];
}
let pivot = aug[i * (n + 1) + i];
if pivot.abs() < 1e-300 {
return Err(LinalgError::SingularMatrixError(
"gauss_solve_small: zero pivot in back substitution".to_string(),
));
}
x[i] = s / pivot;
}
Ok(x)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_spai_diagonal_matrix() {
let a = array![
[3.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
[0.0, 0.0, 4.0],
];
let spai = SPAI::new(&a.view(), 0, 0, None).expect("SPAI::new failed");
let x = array![1.0, 1.0, 1.0];
let y = spai.apply(&x.view()).expect("SPAI::apply failed");
assert!(y[0] > 0.0, "SPAI y[0] should be positive");
assert!(y[1] > 0.0, "SPAI y[1] should be positive");
assert!(y[2] > 0.0, "SPAI y[2] should be positive");
}
#[test]
fn test_polynomial_preconditioner_neumann() {
let a = array![
[4.0, 1.0, 0.0],
[1.0, 4.0, 1.0],
[0.0, 1.0, 4.0],
];
let precond = PolynomialAdvancedPreconditioner::new_neumann(&a.view(), 3)
.expect("PolynomialAdvancedPreconditioner::new_neumann failed");
let x = array![1.0, 2.0, 3.0];
let y = precond
.apply_with_matrix(&a.view(), &x.view())
.expect("apply_with_matrix failed");
assert_eq!(y.len(), 3);
let ay: Vec<f64> = (0..3)
.map(|i| (0..3).map(|j| a[[i, j]] * y[j]).sum::<f64>())
.collect();
for i in 0..3 {
assert!(
(ay[i] - x[i]).abs() < 2.0,
"Neumann preconditioner: A*y ≈ x failed at {i}: A*y[i]={}, x[i]={}",
ay[i],
x[i]
);
}
}
#[test]
fn test_polynomial_preconditioner_chebyshev() {
let a = array![
[4.0, 0.5, 0.0],
[0.5, 4.0, 0.5],
[0.0, 0.5, 4.0],
];
let precond = PolynomialAdvancedPreconditioner::new_chebyshev(
&a.view(),
5,
3.0,
5.0,
)
.expect("Chebyshev preconditioner failed");
let x = array![1.0, 0.0, 0.0];
let y = precond
.apply_with_matrix(&a.view(), &x.view())
.expect("Chebyshev apply failed");
assert_eq!(y.len(), 3);
}
#[test]
fn test_block_diagonal_preconditioner() {
let a = array![
[2.0, 1.0, 0.0, 0.0],
[1.0, 3.0, 0.0, 0.0],
[0.0, 0.0, 4.0, 1.0],
[0.0, 0.0, 1.0, 2.0],
];
let precond = BlockDiagonalPreconditioner::new_uniform(&a.view(), 2)
.expect("BlockDiagonalPreconditioner failed");
let x = array![1.0, 0.0, 0.0, 1.0];
let y = precond.apply(&x.view()).expect("apply failed");
assert_eq!(y.len(), 4);
let ay: Vec<f64> = (0..4)
.map(|i| (0..4).map(|j| a[[i, j]] * y[j]).sum::<f64>())
.collect();
for i in 0..4 {
assert!(
(ay[i] - x[i]).abs() < 1e-10,
"BlockDiagonal: A*y != x at {i}: {} vs {}",
ay[i],
x[i]
);
}
}
#[test]
fn test_schur_complement_preconditioner() {
let a = array![
[4.0, 1.0, 0.5, 0.0],
[1.0, 3.0, 0.0, 0.5],
[0.5, 0.0, 5.0, 1.0],
[0.0, 0.5, 1.0, 4.0],
];
let precond = SchurComplementPreconditioner::new(&a.view(), 2, None, None)
.expect("SchurComplementPreconditioner failed");
let x = array![1.0, 2.0, 3.0, 4.0];
let y = precond.apply(&x.view()).expect("SchurComplement apply failed");
assert_eq!(y.len(), 4);
let ay: Vec<f64> = (0..4)
.map(|i| (0..4).map(|j| a[[i, j]] * y[j]).sum::<f64>())
.collect();
for i in 0..4 {
assert!(
(ay[i] - x[i]).abs() < 1e-8,
"Schur: A*y != x at {i}: {} vs {}",
ay[i],
x[i]
);
}
}
#[test]
fn test_amg_preconditioner_small() {
let a = array![
[3.0, -1.0, 0.0],
[-1.0, 3.0, -1.0],
[0.0, -1.0, 3.0],
];
let precond = AMGPreconditioner::new(&a.view(), Some(5))
.expect("AMGPreconditioner::new failed");
let b = array![1.0, 2.0, 3.0];
let x = precond.apply(&b.view()).expect("AMG apply failed");
assert_eq!(x.len(), 3);
let ax: f64 = (0..3)
.map(|i| {
let row_sum = (0..3).map(|j| a[[i, j]] * x[j]).sum::<f64>();
(row_sum - b[i]).abs()
})
.sum();
assert!(ax < 10.0, "AMG: residual too large: {ax}");
}
#[test]
fn test_amg_preconditioner_multilevel() {
let n = 16;
let mut a = Array2::zeros((n, n));
for i in 0..n {
a[[i, i]] = 2.0;
if i > 0 {
a[[i, i - 1]] = -1.0;
}
if i < n - 1 {
a[[i, i + 1]] = -1.0;
}
}
let precond = AMGPreconditioner::with_options(
&a.view(),
Some(2), Some(4), Some(0.25), )
.expect("AMG multilevel setup failed");
assert!(precond.levels.len() >= 2, "Expected multiple AMG levels, got {}", precond.levels.len());
let b: Array1<f64> = Array1::from_iter((0..n).map(|i| (i + 1) as f64));
let x = precond.apply(&b.view()).expect("AMG V-cycle apply failed");
assert_eq!(x.len(), n);
let mut res_norm = 0.0_f64;
for i in 0..n {
let mut row_sum = 0.0_f64;
for j in 0..n {
row_sum += a[[i, j]] * x[j];
}
let diff = row_sum - b[i];
res_norm += diff * diff;
}
res_norm = res_norm.sqrt();
let b_norm: f64 = b.iter().map(|&v| v * v).sum::<f64>().sqrt();
let rel_res = res_norm / b_norm;
assert!(
rel_res < 1.0,
"AMG V-cycle relative residual too large: {rel_res}"
);
}
}