use crate::assembly::HelmholtzProblem;
use crate::assembly::{MassMatrix, StiffnessMatrix};
use math_audio_solvers::iterative::{
gmres_pipelined, gmres_preconditioned, gmres_preconditioned_with_guess,
};
use math_audio_solvers::{
AdditiveSchwarzPreconditioner, AmgConfig, AmgPreconditioner, CsrMatrix, DiagonalPreconditioner,
GmresConfig, IdentityPreconditioner, IluColoringPreconditioner, IluFixedPointPreconditioner,
IluPreconditioner, gmres, lu_solve,
};
use ndarray::Array1;
use num_complex::Complex64;
use std::time::Instant;
use thiserror::Error;
pub type GmresConfigF64 = GmresConfig<f64>;
#[derive(Debug, Clone)]
pub struct SolverConfig {
pub solver_type: SolverType,
pub gmres: GmresConfigF64,
pub verbosity: usize,
pub schwarz_subdomains: usize,
pub schwarz_overlap: usize,
pub shifted_laplacian: Option<ShiftedLaplacianConfig>,
pub wavenumber: Option<f64>,
}
impl Default for SolverConfig {
fn default() -> Self {
Self {
solver_type: SolverType::GmresIlu,
gmres: GmresConfigF64 {
max_iterations: 1000,
restart: 50,
tolerance: 1e-10,
print_interval: 0,
},
verbosity: 0,
schwarz_subdomains: 8,
schwarz_overlap: 2,
shifted_laplacian: None,
wavenumber: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverType {
Direct,
Gmres,
GmresIlu,
GmresJacobi,
GmresIluColoring,
GmresIluFixedPoint,
GmresSchwarz,
GmresAmg,
GmresPipelined,
GmresPipelinedIlu,
GmresPipelinedAmg,
GmresShiftedLaplacian,
GmresShiftedLaplacianMg,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShiftedLaplacianConfig {
pub alpha: f64,
pub beta: f64,
pub mg_cycles: usize,
pub amg_levels: usize,
pub omega: f64,
pub presmooth: usize,
pub postsmooth: usize,
}
impl Default for ShiftedLaplacianConfig {
fn default() -> Self {
Self {
alpha: 1.0, beta: 1.0, mg_cycles: 2,
amg_levels: 0, omega: 0.8,
presmooth: 2,
postsmooth: 2,
}
}
}
impl ShiftedLaplacianConfig {
pub fn for_wavenumber(k: f64) -> Self {
Self {
alpha: 0.5 * k * k,
beta: 0.5 * k,
mg_cycles: 2,
amg_levels: 0,
omega: 0.8,
presmooth: 2,
postsmooth: 2,
}
}
pub fn aggressive(k: f64) -> Self {
Self {
alpha: k * k,
beta: k,
mg_cycles: 3,
amg_levels: 0,
omega: 0.7,
presmooth: 3,
postsmooth: 3,
}
}
pub fn conservative(k: f64) -> Self {
Self {
alpha: 0.25 * k * k,
beta: 0.25 * k,
mg_cycles: 1,
amg_levels: 0,
omega: 0.9,
presmooth: 1,
postsmooth: 1,
}
}
}
#[derive(Debug, Clone)]
pub struct Solution {
pub values: Array1<Complex64>,
pub iterations: usize,
pub residual: f64,
pub converged: bool,
}
#[derive(Debug, Error)]
pub enum SolverError {
#[error("Solver failed to converge after {0} iterations (residual: {1})")]
ConvergenceFailure(usize, f64),
#[error("Direct solver failed: singular matrix")]
SingularMatrix,
#[error("Matrix dimension mismatch: expected {expected}, got {actual}")]
DimensionMismatch { expected: usize, actual: usize },
#[error("Invalid solver configuration: {0}")]
InvalidConfiguration(String),
}
pub fn solve(problem: &HelmholtzProblem, config: &SolverConfig) -> Result<Solution, SolverError> {
let start = Instant::now();
let csr = problem.matrix.to_csr();
let rhs = Array1::from(problem.rhs.clone());
let csr_time = start.elapsed();
if config.verbosity > 0 {
println!(
" [FEM] System: {} DOFs, {} nnz, sparsity {:.4}%, CSR convert: {:.1}ms",
csr.num_rows,
csr.nnz(),
csr.sparsity() * 100.0,
csr_time.as_secs_f64() * 1000.0
);
}
let solve_start = Instant::now();
let result = match config.solver_type {
SolverType::Direct => solve_direct(&csr, &rhs, config),
SolverType::Gmres => solve_gmres(&csr, &rhs, config),
SolverType::GmresIlu => solve_gmres_ilu(&csr, &rhs, config),
SolverType::GmresJacobi => solve_gmres_jacobi(&csr, &rhs, config),
SolverType::GmresIluColoring => solve_gmres_ilu_coloring(&csr, &rhs, config),
SolverType::GmresIluFixedPoint => solve_gmres_ilu_fixedpoint(&csr, &rhs, config),
SolverType::GmresSchwarz => solve_gmres_schwarz(&csr, &rhs, config),
SolverType::GmresAmg => solve_gmres_amg(&csr, &rhs, config),
SolverType::GmresPipelined => solve_gmres_pipelined(&csr, &rhs, config),
SolverType::GmresPipelinedIlu => solve_gmres_pipelined_ilu(&csr, &rhs, config),
SolverType::GmresPipelinedAmg => solve_gmres_pipelined_amg(&csr, &rhs, config),
SolverType::GmresShiftedLaplacian => {
solve_gmres_shifted_laplacian(problem, &csr, &rhs, config)
}
SolverType::GmresShiftedLaplacianMg => {
solve_gmres_shifted_laplacian_mg(problem, &csr, &rhs, config)
}
};
let solve_time = solve_start.elapsed();
if config.verbosity > 0
&& let Ok(ref sol) = result
{
println!(
" [FEM] Solve: {} iters, residual {:.2e}, time {:.1}ms",
sol.iterations,
sol.residual,
solve_time.as_secs_f64() * 1000.0
);
}
result
}
fn solve_direct(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!("Using direct LU solver");
}
let dense = csr.to_dense();
let solution = lu_solve(&dense, rhs).map_err(|_| SolverError::SingularMatrix)?;
let residual_vec = csr.matvec(&solution);
let residual: f64 = residual_vec
.iter()
.zip(rhs.iter())
.map(|(r, b)| (r - b).norm())
.sum::<f64>()
/ rhs.len() as f64;
Ok(Solution {
values: solution,
iterations: 0,
residual,
converged: true,
})
}
fn solve_gmres(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using GMRES solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let result = gmres(csr, rhs, &config.gmres);
if config.verbosity > 0 {
log::info!(
"GMRES {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_ilu(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using GMRES+ILU solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let ilu_start = Instant::now();
let precond = IluPreconditioner::from_csr(csr);
let ilu_time = ilu_start.elapsed();
if config.verbosity > 0 {
println!(
" [FEM] ILU factorization: {:.1}ms",
ilu_time.as_secs_f64() * 1000.0
);
}
let result = gmres_preconditioned(csr, &precond, rhs, &config.gmres);
if config.verbosity > 0 {
log::info!(
"GMRES+ILU {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_jacobi(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using GMRES+Jacobi solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let precond = DiagonalPreconditioner::from_csr(csr);
let result = gmres_preconditioned(csr, &precond, rhs, &config.gmres);
if config.verbosity > 0 {
log::info!(
"GMRES+Jacobi {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_ilu_coloring(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using GMRES+ILU-Coloring solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let ilu_start = Instant::now();
let precond = IluColoringPreconditioner::from_csr(csr);
let ilu_time = ilu_start.elapsed();
if config.verbosity > 0 {
let (fwd_levels, bwd_levels, avg_fwd, avg_bwd) = precond.level_stats();
println!(
" [FEM] ILU-Coloring: {:.1}ms, {} fwd levels (avg {:.1}), {} bwd levels (avg {:.1})",
ilu_time.as_secs_f64() * 1000.0,
fwd_levels,
avg_fwd,
bwd_levels,
avg_bwd
);
}
let result = gmres_preconditioned(csr, &precond, rhs, &config.gmres);
if config.verbosity > 0 {
log::info!(
"GMRES+ILU-Coloring {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_ilu_fixedpoint(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
const FP_ITERATIONS: usize = 10;
if config.verbosity > 0 {
log::info!(
"Using GMRES+ILU-FixedPoint solver (restart={}, tol={}, fp_iters={})",
config.gmres.restart,
config.gmres.tolerance,
FP_ITERATIONS
);
}
let ilu_start = Instant::now();
let precond = IluFixedPointPreconditioner::from_csr(csr, FP_ITERATIONS);
let ilu_time = ilu_start.elapsed();
if config.verbosity > 0 {
println!(
" [FEM] ILU-FixedPoint: {:.1}ms ({} iterations per apply)",
ilu_time.as_secs_f64() * 1000.0,
FP_ITERATIONS
);
}
let result = gmres_preconditioned(csr, &precond, rhs, &config.gmres);
if config.verbosity > 0 {
log::info!(
"GMRES+ILU-FixedPoint {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_schwarz(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let num_subdomains = config.schwarz_subdomains;
let overlap = config.schwarz_overlap;
if config.verbosity > 0 {
log::info!(
"Using GMRES+Schwarz solver (restart={}, tol={}, {} subdomains, {} overlap)",
config.gmres.restart,
config.gmres.tolerance,
num_subdomains,
overlap
);
}
let schwarz_start = Instant::now();
let precond = AdditiveSchwarzPreconditioner::from_csr(csr, num_subdomains, overlap);
let schwarz_time = schwarz_start.elapsed();
if config.verbosity > 0 {
let (num_subdomains, min_size, max_size, avg_size) = precond.stats();
println!(
" [FEM] Schwarz DD: {:.1}ms, {} subdomains (size: min={}, max={}, avg={:.1})",
schwarz_time.as_secs_f64() * 1000.0,
num_subdomains,
min_size,
max_size,
avg_size
);
}
let result = gmres_preconditioned(csr, &precond, rhs, &config.gmres);
if config.verbosity > 0 {
log::info!(
"GMRES+Schwarz {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_amg(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using GMRES+AMG solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let amg_start = Instant::now();
let mut amg_config = AmgConfig::for_parallel();
amg_config.smoother = math_audio_solvers::AmgSmoother::L1Jacobi;
let precond = AmgPreconditioner::from_csr(csr, amg_config);
let amg_time = amg_start.elapsed();
if config.verbosity > 0 {
let diag = precond.diagnostics();
println!(
" [FEM] AMG setup: {:.1}ms, {} levels, grid complexity {:.2}, operator complexity {:.2}",
amg_time.as_secs_f64() * 1000.0,
diag.num_levels,
diag.grid_complexity,
diag.operator_complexity
);
}
let result = gmres_preconditioned(csr, &precond, rhs, &config.gmres);
if config.verbosity > 0 {
log::info!(
"GMRES+AMG {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_pipelined(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using Pipelined GMRES solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let precond = IdentityPreconditioner;
let result = gmres_pipelined(csr, &precond, rhs, None, &config.gmres);
if config.verbosity > 0 {
log::info!(
"Pipelined GMRES {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_pipelined_ilu(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using Pipelined GMRES+ILU solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let ilu_start = Instant::now();
let precond = IluPreconditioner::from_csr(csr);
let ilu_time = ilu_start.elapsed();
if config.verbosity > 0 {
println!(
" [FEM] ILU factorization: {:.1}ms",
ilu_time.as_secs_f64() * 1000.0
);
}
let result = gmres_pipelined(csr, &precond, rhs, None, &config.gmres);
if config.verbosity > 0 {
log::info!(
"Pipelined GMRES+ILU {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_pipelined_amg(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if config.verbosity > 0 {
log::info!(
"Using Pipelined GMRES+AMG solver (restart={}, tol={})",
config.gmres.restart,
config.gmres.tolerance
);
}
let amg_start = Instant::now();
let mut amg_config = AmgConfig::for_parallel();
amg_config.smoother = math_audio_solvers::AmgSmoother::L1Jacobi;
let precond = AmgPreconditioner::from_csr(csr, amg_config);
let amg_time = amg_start.elapsed();
if config.verbosity > 0 {
let diag = precond.diagnostics();
println!(
" [FEM] AMG setup: {:.1}ms, {} levels, grid complexity {:.2}, operator complexity {:.2}",
amg_time.as_secs_f64() * 1000.0,
diag.num_levels,
diag.grid_complexity,
diag.operator_complexity
);
}
let result = gmres_pipelined(csr, &precond, rhs, None, &config.gmres);
if config.verbosity > 0 {
log::info!(
"Pipelined GMRES+AMG {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let precond = IdentityPreconditioner;
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_ilu_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let precond = IluPreconditioner::from_csr(csr);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_jacobi_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let precond = DiagonalPreconditioner::from_csr(csr);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_ilu_coloring_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let precond = IluColoringPreconditioner::from_csr(csr);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_ilu_fixedpoint_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
const FP_ITERATIONS: usize = 10;
let precond = IluFixedPointPreconditioner::from_csr(csr, FP_ITERATIONS);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_schwarz_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let precond = AdditiveSchwarzPreconditioner::from_csr(
csr,
config.schwarz_subdomains,
config.schwarz_overlap,
);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_amg_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let amg_config = AmgConfig::for_parallel();
let precond = AmgPreconditioner::from_csr(csr, amg_config);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_pipelined_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let precond = IdentityPreconditioner;
let result = gmres_pipelined(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_pipelined_ilu_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let precond = IluPreconditioner::from_csr(csr);
let result = gmres_pipelined(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_pipelined_amg_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let amg_config = AmgConfig::for_parallel();
let precond = AmgPreconditioner::from_csr(csr, amg_config);
let result = gmres_pipelined(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn build_shifted_laplacian(
stiffness: &StiffnessMatrix,
mass: &MassMatrix,
alpha: f64,
beta: f64,
) -> CsrMatrix<Complex64> {
use std::collections::HashMap;
let mut entries: HashMap<(usize, usize), Complex64> = HashMap::new();
let shift = Complex64::new(alpha, beta);
for i in 0..stiffness.nnz() {
let key = (stiffness.rows[i], stiffness.cols[i]);
let real_part = stiffness.values[i];
*entries.entry(key).or_insert(Complex64::new(0.0, 0.0)) += Complex64::new(real_part, 0.0);
}
for i in 0..mass.nnz() {
let key = (mass.rows[i], mass.cols[i]);
let real_part = mass.values[i];
*entries.entry(key).or_insert(Complex64::new(0.0, 0.0)) +=
shift * Complex64::new(real_part, 0.0);
}
let dim = stiffness.dim;
let mut rows = Vec::with_capacity(entries.len());
let mut cols = Vec::with_capacity(entries.len());
let mut values = Vec::with_capacity(entries.len());
for ((r, c), v) in entries {
if v.norm() > 1e-15 {
rows.push(r);
cols.push(c);
values.push(v);
}
}
CsrMatrix::from_triplets(
dim,
dim,
rows.into_iter()
.zip(cols)
.zip(values)
.map(|((r, c), v)| (r, c, v))
.collect(),
)
}
fn solve_gmres_shifted_laplacian(
problem: &HelmholtzProblem,
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let k = config.wavenumber.unwrap_or(1.0);
let default_config = ShiftedLaplacianConfig::for_wavenumber(k);
let sl_config = config.shifted_laplacian.as_ref().unwrap_or(&default_config);
if config.verbosity > 0 {
println!(
" [FEM] Shifted-Laplacian: α={:.4}, β={:.4}, AMG preconditioner",
sl_config.alpha, sl_config.beta
);
}
let sl_start = Instant::now();
let p_matrix = build_shifted_laplacian(
&problem.stiffness,
&problem.mass,
sl_config.alpha,
sl_config.beta,
);
let mut amg_config = AmgConfig::for_parallel();
amg_config.smoother = math_audio_solvers::AmgSmoother::L1Jacobi; amg_config.strong_threshold = 0.5;
let precond = AmgPreconditioner::from_csr(&p_matrix, amg_config);
let sl_time = sl_start.elapsed();
if config.verbosity > 0 {
let diag = precond.diagnostics();
println!(
" [FEM] SL-AMG setup: {:.1}ms, {} levels",
sl_time.as_secs_f64() * 1000.0,
diag.num_levels
);
}
let result = gmres_preconditioned(csr, &precond, rhs, &config.gmres);
if config.verbosity > 0 {
println!(
" [FEM] SL-GMRES {} in {} iterations (residual: {:.2e})",
if result.converged {
"converged"
} else {
"did not converge"
},
result.iterations,
result.residual
);
}
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
fn solve_gmres_shifted_laplacian_mg(
problem: &HelmholtzProblem,
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let k = config.wavenumber.unwrap_or(1.0);
let default_config = ShiftedLaplacianConfig::for_wavenumber(k);
let sl_config = config.shifted_laplacian.as_ref().unwrap_or(&default_config);
if config.verbosity > 0 {
println!(
" [FEM] Shifted-Laplacian-MG: α={:.4}, β={:.4}, {} V-cycles",
sl_config.alpha, sl_config.beta, sl_config.mg_cycles
);
}
let p_matrix = build_shifted_laplacian(
&problem.stiffness,
&problem.mass,
sl_config.alpha,
sl_config.beta,
);
let amg_config = AmgConfig::for_parallel();
let precond = AmgPreconditioner::from_csr(&p_matrix, amg_config);
let mut residual = rhs.clone();
let mut solution = Array1::zeros(rhs.len());
for _ in 0..sl_config.mg_cycles {
let result = gmres_preconditioned_with_guess(
&p_matrix,
&precond,
&residual,
Some(&solution),
&config.gmres,
);
if result.converged {
solution = result.x;
break;
}
solution = result.x;
let p_applied = p_matrix.matvec(&solution);
residual = &residual - &p_applied;
}
let final_residual = csr.matvec(&solution);
let residual_vec = rhs - &final_residual;
let residual_norm: f64 = residual_vec.iter().map(|v| v.norm()).sum::<f64>().sqrt();
Ok(Solution {
values: solution,
iterations: sl_config.mg_cycles,
residual: residual_norm,
converged: true,
})
}
#[allow(dead_code)]
fn solve_gmres_shifted_laplacian_with_guess(
problem: &HelmholtzProblem,
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let k = config.wavenumber.unwrap_or(1.0);
let default_config = ShiftedLaplacianConfig::for_wavenumber(k);
let sl_config = config.shifted_laplacian.as_ref().unwrap_or(&default_config);
if config.verbosity > 0 {
println!(
" [FEM] Shifted-Laplacian: α={:.4}, β={:.4}",
sl_config.alpha, sl_config.beta
);
}
let p_matrix = build_shifted_laplacian(
&problem.stiffness,
&problem.mass,
sl_config.alpha,
sl_config.beta,
);
let amg_config = AmgConfig::for_parallel();
let precond = AmgPreconditioner::from_csr(&p_matrix, amg_config);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
#[allow(dead_code)]
fn solve_gmres_shifted_laplacian_mg_with_guess(
problem: &HelmholtzProblem,
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
let k = config.wavenumber.unwrap_or(1.0);
let default_config = ShiftedLaplacianConfig::for_wavenumber(k);
let sl_config = config.shifted_laplacian.as_ref().unwrap_or(&default_config);
let p_matrix = build_shifted_laplacian(
&problem.stiffness,
&problem.mass,
sl_config.alpha,
sl_config.beta,
);
let amg_config = AmgConfig::for_parallel();
let precond = AmgPreconditioner::from_csr(&p_matrix, amg_config);
let result = gmres_preconditioned_with_guess(csr, &precond, rhs, x0, &config.gmres);
if !result.converged {
return Err(SolverError::ConvergenceFailure(
result.iterations,
result.residual,
));
}
Ok(Solution {
values: result.x,
iterations: result.iterations,
residual: result.residual,
converged: result.converged,
})
}
pub fn solve_csr(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
solve_csr_with_guess(csr, rhs, None, config)
}
pub fn solve_csr_with_guess(
csr: &CsrMatrix<Complex64>,
rhs: &Array1<Complex64>,
x0: Option<&Array1<Complex64>>,
config: &SolverConfig,
) -> Result<Solution, SolverError> {
if csr.num_rows != rhs.len() {
return Err(SolverError::DimensionMismatch {
expected: csr.num_rows,
actual: rhs.len(),
});
}
if let Some(guess) = x0
&& guess.len() != rhs.len()
{
return Err(SolverError::DimensionMismatch {
expected: rhs.len(),
actual: guess.len(),
});
}
match config.solver_type {
SolverType::Direct => solve_direct(csr, rhs, config),
SolverType::Gmres => solve_gmres_with_guess(csr, rhs, x0, config),
SolverType::GmresIlu => solve_gmres_ilu_with_guess(csr, rhs, x0, config),
SolverType::GmresJacobi => solve_gmres_jacobi_with_guess(csr, rhs, x0, config),
SolverType::GmresIluColoring => solve_gmres_ilu_coloring_with_guess(csr, rhs, x0, config),
SolverType::GmresIluFixedPoint => {
solve_gmres_ilu_fixedpoint_with_guess(csr, rhs, x0, config)
}
SolverType::GmresSchwarz => solve_gmres_schwarz_with_guess(csr, rhs, x0, config),
SolverType::GmresAmg => solve_gmres_amg_with_guess(csr, rhs, x0, config),
SolverType::GmresPipelined => solve_gmres_pipelined_with_guess(csr, rhs, x0, config),
SolverType::GmresPipelinedIlu => solve_gmres_pipelined_ilu_with_guess(csr, rhs, x0, config),
SolverType::GmresPipelinedAmg => solve_gmres_pipelined_amg_with_guess(csr, rhs, x0, config),
SolverType::GmresShiftedLaplacian => {
Err(SolverError::InvalidConfiguration(
"Shifted-Laplacian solver requires HelmholtzProblem, not CSR matrix. Use solve() instead.".into()
))
}
SolverType::GmresShiftedLaplacianMg => {
Err(SolverError::InvalidConfiguration(
"Shifted-Laplacian solver requires HelmholtzProblem, not CSR matrix. Use solve() instead.".into()
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assembly::HelmholtzProblem;
use crate::basis::PolynomialDegree;
use crate::mesh::unit_square_triangles;
#[test]
fn test_solve_helmholtz_direct() {
let mesh = unit_square_triangles(4);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let config = SolverConfig {
solver_type: SolverType::Direct,
..Default::default()
};
let solution = solve(&problem, &config).expect("Solver should succeed");
assert!(solution.converged);
assert_eq!(solution.values.len(), problem.num_dofs());
}
#[test]
fn test_solve_helmholtz_gmres() {
let mesh = unit_square_triangles(4);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let config = SolverConfig {
solver_type: SolverType::Gmres,
gmres: GmresConfigF64 {
max_iterations: 100,
restart: 20,
tolerance: 1e-8,
print_interval: 0,
},
..Default::default()
};
let solution = solve(&problem, &config).expect("Solver should succeed");
assert!(solution.converged);
}
#[test]
fn test_solve_helmholtz_gmres_ilu() {
let mesh = unit_square_triangles(4);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let config = SolverConfig {
solver_type: SolverType::GmresIlu,
gmres: GmresConfigF64 {
max_iterations: 100,
restart: 20,
tolerance: 1e-8,
print_interval: 0,
},
..Default::default()
};
let solution = solve(&problem, &config).expect("Solver should succeed");
assert!(solution.converged);
}
#[test]
fn test_csr_conversion() {
let mesh = unit_square_triangles(4);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let csr = problem.matrix.to_csr();
assert_eq!(csr.num_rows, problem.num_dofs());
assert_eq!(csr.num_cols, problem.num_dofs());
assert!(csr.nnz() > 0);
assert!(csr.nnz() <= problem.matrix.nnz());
}
#[test]
fn test_ilu_preconditioner_improves_convergence() {
let mesh = unit_square_triangles(8); let k = Complex64::new(2.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |x, y, _| {
Complex64::new(
(x * std::f64::consts::PI).sin() * (y * std::f64::consts::PI).sin(),
0.0,
)
});
let gmres_config = GmresConfigF64 {
max_iterations: 500,
restart: 30,
tolerance: 1e-8,
print_interval: 0,
};
let config_no_precond = SolverConfig {
solver_type: SolverType::Gmres,
gmres: gmres_config.clone(),
..Default::default()
};
let config_ilu = SolverConfig {
solver_type: SolverType::GmresIlu,
gmres: gmres_config,
..Default::default()
};
let sol_no_precond = solve(&problem, &config_no_precond).expect("Should converge");
let sol_ilu = solve(&problem, &config_ilu).expect("Should converge");
assert!(
sol_ilu.iterations <= sol_no_precond.iterations + 10,
"ILU should not significantly increase iterations: {} vs {}",
sol_ilu.iterations,
sol_no_precond.iterations
);
}
#[test]
fn test_solve_helmholtz_gmres_pipelined() {
let mesh = unit_square_triangles(4);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let config = SolverConfig {
solver_type: SolverType::GmresPipelined,
gmres: GmresConfigF64 {
max_iterations: 100,
restart: 20,
tolerance: 1e-8,
print_interval: 0,
},
..Default::default()
};
let solution = solve(&problem, &config).expect("Solver should succeed");
assert!(solution.converged);
}
#[test]
fn test_solve_helmholtz_gmres_pipelined_ilu() {
let mesh = unit_square_triangles(4);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let config = SolverConfig {
solver_type: SolverType::GmresPipelinedIlu,
gmres: GmresConfigF64 {
max_iterations: 100,
restart: 20,
tolerance: 1e-8,
print_interval: 0,
},
..Default::default()
};
let solution = solve(&problem, &config).expect("Solver should succeed");
assert!(solution.converged);
}
#[test]
fn test_solve_helmholtz_gmres_shifted_laplacian() {
let mesh = unit_square_triangles(4);
let k = Complex64::new(1.0, 0.0);
let problem = HelmholtzProblem::assemble(&mesh, PolynomialDegree::P1, k, |_, _, _| {
Complex64::new(1.0, 0.0)
});
let config = SolverConfig {
solver_type: SolverType::GmresShiftedLaplacian,
gmres: GmresConfigF64 {
max_iterations: 100,
restart: 20,
tolerance: 1e-8,
print_interval: 0,
},
wavenumber: Some(1.0),
..Default::default()
};
let solution = solve(&problem, &config).expect("Solver should succeed");
assert!(solution.converged);
assert_eq!(solution.values.len(), problem.num_dofs());
}
#[test]
fn test_shifted_laplacian_config_constructors() {
let config_default = ShiftedLaplacianConfig::default();
assert_eq!(config_default.alpha, 1.0);
assert_eq!(config_default.beta, 1.0);
let config_k = ShiftedLaplacianConfig::for_wavenumber(2.0);
assert_eq!(config_k.alpha, 2.0); assert_eq!(config_k.beta, 1.0);
let config_aggressive = ShiftedLaplacianConfig::aggressive(2.0);
assert_eq!(config_aggressive.alpha, 4.0); assert_eq!(config_aggressive.beta, 2.0);
let config_conservative = ShiftedLaplacianConfig::conservative(2.0);
assert_eq!(config_conservative.alpha, 1.0); assert_eq!(config_conservative.beta, 0.5); }
}