use math_audio_solvers::CsrMatrix;
use ndarray::Array1;
use num_complex::Complex64;
pub fn chebyshev_smooth(
a: &CsrMatrix<Complex64>,
x: &mut Array1<Complex64>,
b: &Array1<Complex64>,
iterations: usize,
alpha: f64,
) {
if iterations == 0 {
return;
}
let n = x.len();
let lambda_max = estimate_spectral_radius(a);
let d = lambda_max * (1.0 + alpha) / 2.0;
let c = lambda_max * (1.0 - alpha) / 2.0;
if d.abs() < 1e-15 || c.abs() < 1e-15 {
return;
}
let ah_b = matvec_hermitian(a, b);
let ah_ax = matvec_hermitian(a, &a.matvec(x));
let mut residual = &ah_b - &ah_ax;
let beta_0 = 1.0 / d;
*x = &*x + &(residual.mapv(|v| v * beta_0));
if iterations == 1 {
return;
}
let mut x_prev = x.clone();
let mut rho_prev = 1.0_f64;
for _q in 1..iterations {
let rho = 1.0 / (2.0 * d / c - rho_prev);
let ah_ax = matvec_hermitian(a, &a.matvec(x));
residual = &ah_b - &ah_ax;
let factor1 = 1.0 + rho * (rho_prev - 1.0);
let factor2 = rho * (rho_prev - 1.0);
let factor3 = 2.0 * rho / c;
let x_next = Array1::from_shape_fn(n, |i| {
Complex64::new(factor1, 0.0) * x[i] - Complex64::new(factor2, 0.0) * x_prev[i]
+ Complex64::new(factor3, 0.0) * residual[i]
});
x_prev = x.clone();
*x = x_next;
rho_prev = rho;
}
}
pub fn jacobi_damped_smooth(
a: &CsrMatrix<Complex64>,
x: &mut Array1<Complex64>,
b: &Array1<Complex64>,
omega: f64,
) {
let n = x.len();
let diag = extract_diagonal(a);
let ax = a.matvec(x);
let residual: Array1<Complex64> = b - &ax;
for i in 0..n {
if diag[i].norm() > 1e-15 {
x[i] += Complex64::new(omega, 0.0) * residual[i] / diag[i];
}
}
}
pub fn optimal_jacobi_damping(k: f64, h: f64) -> f64 {
let kh_sq = k * k * h * h;
if kh_sq < 2.0 {
(2.0 - kh_sq) / (3.0 - kh_sq)
} else {
2.0 / 3.0
}
}
pub fn estimate_alpha(k: f64, h: f64, _level: usize) -> f64 {
let kh = k * h;
let alpha = (kh * kh / 4.0).min(0.9);
alpha.max(0.01) }
fn matvec_hermitian(a: &CsrMatrix<Complex64>, x: &Array1<Complex64>) -> Array1<Complex64> {
let n = a.num_cols;
let mut y = Array1::zeros(n);
for row in 0..a.num_rows {
let start = a.row_ptrs[row];
let end = a.row_ptrs[row + 1];
for idx in start..end {
let col = a.col_indices[idx];
let val = a.values[idx].conj();
y[col] += val * x[row];
}
}
y
}
fn extract_diagonal(a: &CsrMatrix<Complex64>) -> Vec<Complex64> {
let n = a.num_rows;
let mut diag = vec![Complex64::new(0.0, 0.0); n];
for (row, diag_entry) in diag.iter_mut().enumerate() {
let start = a.row_ptrs[row];
let end = a.row_ptrs[row + 1];
for idx in start..end {
if a.col_indices[idx] == row {
*diag_entry = a.values[idx];
}
}
}
diag
}
fn estimate_spectral_radius(a: &CsrMatrix<Complex64>) -> f64 {
let mut max_radius = 0.0_f64;
for row in 0..a.num_rows {
let start = a.row_ptrs[row];
let end = a.row_ptrs[row + 1];
let mut row_sum = 0.0;
for idx in start..end {
row_sum += a.values[idx].norm_sqr();
}
max_radius = max_radius.max(row_sum);
}
max_radius
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assembly::HelmholtzProblem;
use crate::basis::PolynomialDegree;
use crate::mesh::unit_square_triangles;
#[test]
fn test_optimal_jacobi_damping() {
let omega = optimal_jacobi_damping(1.0, 0.1);
assert!(omega > 0.5 && omega < 1.0);
let omega_large = optimal_jacobi_damping(10.0, 1.0);
assert!((omega_large - 2.0 / 3.0).abs() < 1e-10);
}
#[test]
fn test_estimate_alpha() {
let alpha = estimate_alpha(1.0, 0.1, 0);
assert!(alpha >= 0.01);
assert!(alpha <= 0.9);
let alpha_high = estimate_alpha(10.0, 0.5, 0);
assert!(alpha_high > alpha);
}
#[test]
fn test_chebyshev_reduces_residual() {
let mesh = unit_square_triangles(8);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |x, y, _z| {
Complex64::new(
(x * std::f64::consts::PI).sin() * (y * std::f64::consts::PI).sin(),
0.0,
)
});
let csr = problem.matrix.to_csr();
let rhs = Array1::from(problem.rhs.clone());
let mut x = Array1::zeros(rhs.len());
let initial_residual = compute_residual_norm(&csr, &x, &rhs);
chebyshev_smooth(&csr, &mut x, &rhs, 5, 0.1);
let final_residual = compute_residual_norm(&csr, &x, &rhs);
assert!(
final_residual < initial_residual,
"Chebyshev should reduce residual: {} -> {}",
initial_residual,
final_residual
);
}
#[test]
fn test_jacobi_smooth_reduces_residual() {
let mesh = unit_square_triangles(8);
let k = Complex64::new(0.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let csr = problem.matrix.to_csr();
let rhs = Array1::from(problem.rhs.clone());
let mut x = Array1::zeros(rhs.len());
let initial_residual = compute_residual_norm(&csr, &x, &rhs);
for _ in 0..10 {
jacobi_damped_smooth(&csr, &mut x, &rhs, 2.0 / 3.0);
}
let final_residual = compute_residual_norm(&csr, &x, &rhs);
assert!(
final_residual < initial_residual,
"Jacobi should reduce residual on Laplacian: {} -> {}",
initial_residual,
final_residual
);
}
fn compute_residual_norm(
a: &CsrMatrix<Complex64>,
x: &Array1<Complex64>,
b: &Array1<Complex64>,
) -> f64 {
let ax = a.matvec(x);
let residual = b - &ax;
residual.iter().map(|v| v.norm_sqr()).sum::<f64>().sqrt()
}
}