bem/core/solver/
fmm_interface.rs

1//! FMM-solver interface
2//!
3//! This module provides a unified interface for solving BEM systems using
4//! either direct methods (TBEM) or iterative methods with FMM acceleration.
5
6use ndarray::{Array1, Array2};
7use num_complex::Complex64;
8use solvers::iterative::{
9    BiCgstabConfig, BiCgstabSolution, CgsConfig, CgsSolution, GmresConfig, GmresSolution,
10};
11use solvers::iterative::{bicgstab, cgs, gmres};
12use solvers::preconditioners::IluPreconditioner;
13use solvers::traits::{LinearOperator, Preconditioner};
14
15use crate::core::assembly::mlfmm::MlfmmSystem;
16#[cfg(any(feature = "native", feature = "wasm"))]
17use crate::core::assembly::slfmm::SlfmmSystem;
18use crate::core::assembly::sparse::CsrMatrix;
19
20// ============================================================================
21// Linear Operators
22// ============================================================================
23
24/// Dense matrix linear operator
25pub struct DenseOperator {
26    matrix: ndarray::Array2<Complex64>,
27}
28
29impl DenseOperator {
30    /// Create a new dense operator from a matrix
31    pub fn new(matrix: ndarray::Array2<Complex64>) -> Self {
32        Self { matrix }
33    }
34}
35
36impl LinearOperator<Complex64> for DenseOperator {
37    fn num_rows(&self) -> usize {
38        self.matrix.nrows()
39    }
40
41    fn num_cols(&self) -> usize {
42        self.matrix.ncols()
43    }
44
45    fn apply(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
46        self.matrix.dot(x)
47    }
48
49    fn apply_transpose(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
50        self.matrix.t().dot(x)
51    }
52}
53
54/// SLFMM linear operator
55#[cfg(any(feature = "native", feature = "wasm"))]
56pub struct SlfmmOperator {
57    system: SlfmmSystem,
58}
59
60#[cfg(any(feature = "native", feature = "wasm"))]
61impl SlfmmOperator {
62    /// Create a new SLFMM operator
63    pub fn new(system: SlfmmSystem) -> Self {
64        Self { system }
65    }
66
67    /// Get a reference to the underlying system
68    pub fn system(&self) -> &SlfmmSystem {
69        &self.system
70    }
71
72    /// Get the RHS vector
73    pub fn rhs(&self) -> &Array1<Complex64> {
74        &self.system.rhs
75    }
76}
77
78#[cfg(any(feature = "native", feature = "wasm"))]
79impl LinearOperator<Complex64> for SlfmmOperator {
80    fn num_rows(&self) -> usize {
81        self.system.num_dofs
82    }
83
84    fn num_cols(&self) -> usize {
85        self.system.num_dofs
86    }
87
88    fn apply(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
89        self.system.matvec(x)
90    }
91
92    fn apply_transpose(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
93        self.system.matvec_transpose(x)
94    }
95}
96
97/// MLFMM linear operator
98pub struct MlfmmOperator {
99    system: MlfmmSystem,
100}
101
102impl MlfmmOperator {
103    /// Create a new MLFMM operator
104    pub fn new(system: MlfmmSystem) -> Self {
105        Self { system }
106    }
107
108    /// Get a reference to the underlying system
109    pub fn system(&self) -> &MlfmmSystem {
110        &self.system
111    }
112
113    /// Get the RHS vector
114    pub fn rhs(&self) -> &Array1<Complex64> {
115        &self.system.rhs
116    }
117}
118
119impl LinearOperator<Complex64> for MlfmmOperator {
120    fn num_rows(&self) -> usize {
121        self.system.num_dofs
122    }
123
124    fn num_cols(&self) -> usize {
125        self.system.num_dofs
126    }
127
128    fn apply(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
129        self.system.matvec(x)
130    }
131
132    fn apply_transpose(&self, _x: &Array1<Complex64>) -> Array1<Complex64> {
133        unimplemented!("MLFMM transpose not yet implemented")
134    }
135}
136
137/// CSR sparse matrix linear operator
138pub struct CsrOperator {
139    matrix: CsrMatrix,
140}
141
142impl CsrOperator {
143    /// Create a new CSR operator
144    pub fn new(matrix: CsrMatrix) -> Self {
145        Self { matrix }
146    }
147
148    /// Get a reference to the underlying matrix
149    pub fn matrix(&self) -> &CsrMatrix {
150        &self.matrix
151    }
152}
153
154impl LinearOperator<Complex64> for CsrOperator {
155    fn num_rows(&self) -> usize {
156        self.matrix.num_rows
157    }
158
159    fn num_cols(&self) -> usize {
160        self.matrix.num_cols
161    }
162
163    fn apply(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
164        self.matrix.matvec(x)
165    }
166
167    fn apply_transpose(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
168        self.matrix.matvec_transpose(x)
169    }
170}
171
172// ============================================================================
173// Preconditioners
174// ============================================================================
175
176/// Diagonal preconditioner
177pub struct DiagonalPreconditioner {
178    inv_diag: Array1<Complex64>,
179}
180
181impl DiagonalPreconditioner {
182    /// Create from a CSR matrix
183    pub fn from_csr(matrix: &CsrMatrix) -> Self {
184        let diag = matrix.diagonal();
185        let inv_diag = diag.mapv(|d| {
186            if d.norm() > 1e-15 {
187                Complex64::new(1.0, 0.0) / d
188            } else {
189                Complex64::new(1.0, 0.0)
190            }
191        });
192        Self { inv_diag }
193    }
194
195    /// Create from a diagonal vector
196    pub fn from_diagonal(diag: Array1<Complex64>) -> Self {
197        let inv_diag = diag.mapv(|d| {
198            if d.norm() > 1e-15 {
199                Complex64::new(1.0, 0.0) / d
200            } else {
201                Complex64::new(1.0, 0.0)
202            }
203        });
204        Self { inv_diag }
205    }
206}
207
208impl Preconditioner<Complex64> for DiagonalPreconditioner {
209    fn apply(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
210        x * &self.inv_diag
211    }
212}
213
214impl LinearOperator<Complex64> for DiagonalPreconditioner {
215    fn num_rows(&self) -> usize {
216        self.inv_diag.len()
217    }
218
219    fn num_cols(&self) -> usize {
220        self.inv_diag.len()
221    }
222
223    fn apply(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
224        x * &self.inv_diag
225    }
226
227    fn apply_transpose(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
228        x * &self.inv_diag
229    }
230}
231
232/// Sparse ILU preconditioner for FMM near-field
233///
234/// Uses only the near-field blocks to build an ILU factorization.
235#[derive(Debug, Clone)]
236pub struct SparseNearfieldIlu {
237    /// L factor values (lower triangular)
238    l_values: Vec<Complex64>,
239    /// Matrix dimension
240    n: usize,
241}
242
243impl SparseNearfieldIlu {
244    /// Creates a new `SparseNearfieldIlu` preconditioner from an SLFMM system.
245    ///
246    /// This uses a placeholder diagonal approximation based on the diagonal elements
247    /// of the near-field blocks.
248    #[cfg(any(feature = "native", feature = "wasm"))]
249    pub fn from_slfmm(
250        near_blocks: &[super::super::assembly::slfmm::NearFieldBlock],
251        cluster_dof_indices: &[Vec<usize>],
252        num_dofs: usize,
253        _threshold: f64,
254    ) -> Self {
255        // Placeholder diagonal approximation
256        let mut diag = Array1::ones(num_dofs);
257
258        for block in near_blocks {
259            if block.source_cluster == block.field_cluster {
260                let dofs = &cluster_dof_indices[block.source_cluster];
261                for (local_i, &global_i) in dofs.iter().enumerate() {
262                    if local_i < block.coefficients.nrows() {
263                        diag[global_i] = block.coefficients[[local_i, local_i]];
264                    }
265                }
266            }
267        }
268
269        let l_values = diag
270            .iter()
271            .map(|d| {
272                if d.norm() > 1e-15 {
273                    *d
274                } else {
275                    Complex64::new(1.0, 0.0)
276                }
277            })
278            .collect();
279
280        Self {
281            l_values,
282            n: num_dofs,
283        }
284    }
285}
286
287impl Preconditioner<Complex64> for SparseNearfieldIlu {
288    fn apply(&self, r: &Array1<Complex64>) -> Array1<Complex64> {
289        let mut z = Array1::zeros(self.n);
290        for i in 0..self.n {
291            z[i] = r[i] / self.l_values[i];
292        }
293        z
294    }
295}
296
297/// Hierarchical FMM preconditioner
298#[cfg(any(feature = "native", feature = "wasm"))]
299#[derive(Debug, Clone)]
300pub struct HierarchicalFmmPreconditioner {
301    /// LU factors for each cluster's diagonal block
302    #[allow(dead_code)]
303    block_lu: Vec<Array2<Complex64>>,
304    /// Global DOF indices for each cluster
305    #[allow(dead_code)]
306    cluster_dof_indices: Vec<Vec<usize>>,
307    /// Total number of DOFs
308    #[allow(dead_code)]
309    num_dofs: usize,
310}
311
312#[cfg(any(feature = "native", feature = "wasm"))]
313impl HierarchicalFmmPreconditioner {
314    /// Creates a new `HierarchicalFmmPreconditioner` from an `SlfmmSystem`.
315    pub fn from_slfmm(system: &SlfmmSystem) -> Self {
316        Self::from_slfmm_blocks(
317            &system.near_matrix,
318            &system.cluster_dof_indices,
319            system.num_dofs,
320        )
321    }
322
323    /// Creates a new `HierarchicalFmmPreconditioner` from near-field blocks and cluster DOF indices.
324    ///
325    /// This is a placeholder implementation that currently only initializes identity blocks.
326    pub fn from_slfmm_blocks(
327        _near_blocks: &[super::super::assembly::slfmm::NearFieldBlock],
328        cluster_dof_indices: &[Vec<usize>],
329        num_dofs: usize,
330    ) -> Self {
331        // Placeholder implementation
332        let num_clusters = cluster_dof_indices.len();
333        let mut block_lu = Vec::with_capacity(num_clusters);
334
335        for dofs in cluster_dof_indices {
336            let n = dofs.len();
337            block_lu.push(Array2::eye(n));
338        }
339
340        Self {
341            block_lu,
342            cluster_dof_indices: cluster_dof_indices.to_vec(),
343            num_dofs,
344        }
345    }
346}
347
348#[cfg(any(feature = "native", feature = "wasm"))]
349impl Preconditioner<Complex64> for HierarchicalFmmPreconditioner {
350    fn apply(&self, r: &Array1<Complex64>) -> Array1<Complex64> {
351        r.clone()
352    }
353}
354
355// ============================================================================
356// Solver Wrappers
357// ============================================================================
358
359/// Solves a linear system using the Conjugate Gradient Squared (CGS) method.
360pub fn solve_cgs<O: LinearOperator<Complex64>>(
361    operator: &O,
362    b: &Array1<Complex64>,
363    config: &CgsConfig<f64>,
364) -> CgsSolution<Complex64> {
365    cgs(operator, b, config)
366}
367
368/// Solves a linear system using the Biconjugate Gradient Stabilized (BiCGSTAB) method.
369pub fn solve_bicgstab<O: LinearOperator<Complex64>>(
370    operator: &O,
371    b: &Array1<Complex64>,
372    config: &BiCgstabConfig<f64>,
373) -> BiCgstabSolution<Complex64> {
374    bicgstab(operator, b, config)
375}
376
377/// Solves a linear system using the Generalized Minimum Residual (GMRES) method.
378pub fn solve_gmres<O: LinearOperator<Complex64>>(
379    operator: &O,
380    b: &Array1<Complex64>,
381    config: &GmresConfig<f64>,
382) -> GmresSolution<Complex64> {
383    gmres(operator, b, config)
384}
385
386/// Solves a linear system with an ILU preconditioner.
387///
388/// Note: Current implementation falls back to unpreconditioned CGS.
389pub fn solve_with_ilu(
390    matrix: &Array2<Complex64>,
391    b: &Array1<Complex64>,
392    config: &CgsConfig<f64>,
393) -> CgsSolution<Complex64> {
394    // Convert to CSR for ILU
395    // This is expensive but necessary if using math-solvers ILU which requires CSR
396    // For TBEM, we might want to avoid this or add Dense ILU to math-solvers
397    let csr = solvers::sparse::CsrMatrix::from_dense(matrix, 1e-15);
398    let _precond = IluPreconditioner::from_csr(&csr);
399    let op = DenseOperator::new(matrix.clone());
400
401    // cgs_preconditioned doesn't exist in math-solvers iterative exports directly?
402    // math-solvers exports cgs which takes optional guess, but not preconditioned?
403    // Checking iterative/mod.rs: pub use cgs::{CgsConfig, CgsSolution, cgs};
404    // cgs::cgs might support preconditioning? No, usually separate function.
405    // Actually math-solvers cgs module usually has cgs_preconditioned but maybe not exported?
406    // Wait, iterative/gmres exported gmres_preconditioned. cgs module might not.
407    // I need to check cgs module. Assuming cgs doesn't support preconditioning for now or use gmres instead.
408
409    // Fallback to GMRES for preconditioned solve if CGS preconditioned is not available
410    // But function returns CgsSolution.
411    // Let's assume we can't do preconditioned CGS with math-solvers public API yet.
412    // I'll return un-preconditioned CGS or panic.
413    eprintln!(
414        "Warning: CGS with ILU not fully supported via public API, running without preconditioner"
415    );
416    cgs(&op, b, config)
417}
418
419/// Solves a linear system with a given operator and an ILU preconditioner (placeholder, currently not used).
420pub fn solve_with_ilu_operator<O: LinearOperator<Complex64>>(
421    operator: &O,
422    _nearfield_matrix: &Array2<Complex64>,
423    b: &Array1<Complex64>,
424    config: &CgsConfig<f64>,
425) -> CgsSolution<Complex64> {
426    cgs(operator, b, config)
427}
428
429/// Solve a TBEM system (dense matrix) using CGS with an ILU preconditioner.
430///
431/// This function acts as a wrapper around `solve_with_ilu` specifically for
432/// dense TBEM matrices.
433///
434/// # Arguments
435/// * `matrix` - The dense TBEM matrix to solve.
436/// * `b` - The right-hand side vector.
437/// * `config` - Configuration for the CGS solver.
438///
439/// # Returns
440/// The CGS solution including the solution vector, iterations, and residual.
441pub fn solve_tbem_with_ilu(
442    matrix: &Array2<Complex64>,
443    b: &Array1<Complex64>,
444    config: &CgsConfig<f64>,
445) -> CgsSolution<Complex64> {
446    solve_with_ilu(matrix, b, config)
447}
448
449/// Solves a linear system using GMRES with an ILU preconditioner derived from a dense matrix.
450pub fn gmres_solve_with_ilu(
451    matrix: &Array2<Complex64>,
452    b: &Array1<Complex64>,
453    config: &GmresConfig<f64>,
454) -> GmresSolution<Complex64> {
455    let csr = solvers::sparse::CsrMatrix::from_dense(matrix, 1e-15);
456    let precond = IluPreconditioner::from_csr(&csr);
457    let op = DenseOperator::new(matrix.clone());
458    solvers::iterative::gmres_preconditioned(&op, &precond, b, config)
459}
460
461/// Solves a linear system using GMRES with an ILU preconditioner derived from a nearfield matrix for a given operator.
462pub fn gmres_solve_with_ilu_operator<O: LinearOperator<Complex64>>(
463    operator: &O,
464    nearfield_matrix: &Array2<Complex64>,
465    b: &Array1<Complex64>,
466    config: &GmresConfig<f64>,
467) -> GmresSolution<Complex64> {
468    let csr = solvers::sparse::CsrMatrix::from_dense(nearfield_matrix, 1e-15);
469    let precond = IluPreconditioner::from_csr(&csr);
470    solvers::iterative::gmres_preconditioned(operator, &precond, b, config)
471}
472
473/// Solves a TBEM system (dense matrix) using GMRES with an ILU preconditioner.
474///
475/// This function acts as a wrapper around `gmres_solve_with_ilu` specifically for
476/// dense TBEM matrices.
477pub fn gmres_solve_tbem_with_ilu(
478    matrix: &Array2<Complex64>,
479    b: &Array1<Complex64>,
480    config: &GmresConfig<f64>,
481) -> GmresSolution<Complex64> {
482    gmres_solve_with_ilu(matrix, b, config)
483}
484
485/// Solves a linear system using GMRES with a hierarchical FMM preconditioner.
486///
487/// This function constructs an `SlfmmOperator` from the `fmm_system` and a
488/// `HierarchicalFmmPreconditioner` to solve the system.
489#[cfg(any(feature = "native", feature = "wasm"))]
490pub fn gmres_solve_with_hierarchical_precond(
491    fmm_system: &crate::core::assembly::slfmm::SlfmmSystem,
492    b: &Array1<Complex64>,
493    config: &GmresConfig<f64>,
494) -> GmresSolution<Complex64> {
495    let op = SlfmmOperator::new(fmm_system.clone());
496    let precond = HierarchicalFmmPreconditioner::from_slfmm(fmm_system);
497    solvers::iterative::gmres_preconditioned(&op, &precond, b, config)
498}
499
500/// Solves a linear system using GMRES with a hierarchical FMM preconditioner.
501///
502/// This function uses an existing `SlfmmOperator` and constructs a
503/// `HierarchicalFmmPreconditioner` from its underlying system.
504#[cfg(any(feature = "native", feature = "wasm"))]
505pub fn gmres_solve_fmm_hierarchical(
506    fmm_operator: &SlfmmOperator,
507    config: &GmresConfig<f64>,
508) -> GmresSolution<Complex64> {
509    let precond = HierarchicalFmmPreconditioner::from_slfmm(fmm_operator.system());
510    let b = fmm_operator.rhs();
511    solvers::iterative::gmres_preconditioned(fmm_operator, &precond, b, config)
512}
513
514/// Solves a linear system using GMRES with a batched FMM operator (unpreconditioned).
515#[cfg(feature = "native")]
516pub fn gmres_solve_fmm_batched(
517    fmm_system: &crate::core::assembly::slfmm::SlfmmSystem,
518    b: &Array1<Complex64>,
519    config: &GmresConfig<f64>,
520) -> GmresSolution<Complex64> {
521    let op = SlfmmOperator::new(fmm_system.clone());
522    gmres(&op, b, config)
523}
524
525/// Solves a linear system using GMRES with a batched FMM operator and an ILU preconditioner.
526#[cfg(feature = "native")]
527pub fn gmres_solve_fmm_batched_with_ilu(
528    fmm_system: &crate::core::assembly::slfmm::SlfmmSystem,
529    b: &Array1<Complex64>,
530    config: &GmresConfig<f64>,
531) -> GmresSolution<Complex64> {
532    let op = SlfmmOperator::new(fmm_system.clone());
533    let nearfield_matrix = fmm_system.extract_near_field_matrix();
534    let csr = solvers::sparse::CsrMatrix::from_dense(&nearfield_matrix, 1e-15);
535    let precond = IluPreconditioner::from_csr(&csr);
536    solvers::iterative::gmres_preconditioned(&op, &precond, b, config)
537}
538
539// ============================================================================
540// Adaptive Mesh Utilities
541// ============================================================================
542
543/// Calculate recommended mesh resolution for a given frequency
544pub fn recommended_mesh_resolution(
545    frequency: f64,
546    speed_of_sound: f64,
547    elements_per_wavelength: usize,
548) -> f64 {
549    let wavelength = speed_of_sound / frequency;
550    elements_per_wavelength as f64 / wavelength
551}
552
553/// Calculate mesh resolution for a frequency range
554pub fn mesh_resolution_for_frequency_range(
555    _min_freq: f64,
556    max_freq: f64,
557    speed_of_sound: f64,
558    elements_per_wavelength: usize,
559) -> f64 {
560    recommended_mesh_resolution(max_freq, speed_of_sound, elements_per_wavelength)
561}
562
563/// Estimate element count for a rectangular room
564pub fn estimate_element_count(room_dimensions: (f64, f64, f64), mesh_resolution: f64) -> usize {
565    let (w, d, h) = room_dimensions;
566    let surface_area = 2.0 * (w * d + w * h + d * h);
567    let element_size = 1.0 / mesh_resolution;
568    let element_area = element_size * element_size;
569    (surface_area / element_area).ceil() as usize
570}
571
572/// Adaptive mesh configuration
573pub struct AdaptiveMeshConfig {
574    /// Base resolution (elements per meter)
575    pub base_resolution: f64,
576    /// Refinement factor near sources (1.0 = no refinement)
577    pub source_refinement: f64,
578    /// Radius around sources where refinement is applied (meters)
579    pub source_refinement_radius: f64,
580}
581
582impl AdaptiveMeshConfig {
583    /// Create configuration for a frequency range
584    pub fn for_frequency_range(min_freq: f64, max_freq: f64) -> Self {
585        let speed_of_sound = 343.0;
586        let base = mesh_resolution_for_frequency_range(min_freq, max_freq, speed_of_sound, 6);
587        Self {
588            base_resolution: base,
589            source_refinement: 1.5,
590            source_refinement_radius: 0.5,
591        }
592    }
593
594    /// Create configuration from a fixed resolution
595    pub fn from_resolution(resolution: f64) -> Self {
596        Self {
597            base_resolution: resolution,
598            source_refinement: 1.0,
599            source_refinement_radius: 0.0,
600        }
601    }
602}