bem/core/
bem_solver.rs

1//! High-level BEM Solver API
2//!
3//! This module provides a unified, high-level interface for acoustic BEM simulations.
4//! It integrates mesh generation, system assembly, linear solving, and post-processing.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use bem::core::{BemSolver, BemProblem, IncidentField};
10//!
11//! // Create a rigid sphere scattering problem
12//! let problem = BemProblem::rigid_sphere_scattering(
13//!     0.1,        // radius
14//!     1000.0,     // frequency
15//!     343.0,      // speed of sound
16//!     1.21,       // density
17//! );
18//!
19//! // Configure solver
20//! let solver = BemSolver::new()
21//!     .with_mesh_refinement(3)
22//!     .with_solver_method(SolverMethod::Direct);
23//!
24//! // Solve
25//! let solution = solver.solve(&problem)?;
26//!
27//! // Evaluate field at a point
28//! let pressure = solution.evaluate_pressure(&[0.0, 0.0, 0.2]);
29//! ```
30
31use ndarray::{Array1, Array2};
32use num_complex::Complex64;
33use std::f64::consts::PI;
34
35use crate::core::assembly::tbem::build_tbem_system_with_beta;
36use crate::core::incident::IncidentField;
37use crate::core::mesh::generators::{generate_icosphere_mesh, generate_sphere_mesh};
38use crate::core::postprocess::pressure::{compute_total_field, FieldPoint};
39use crate::core::solver::direct::direct_solve;
40use crate::core::types::{BoundaryCondition, Element, Mesh, PhysicsParams};
41
42/// Solver method for the linear system
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum SolverMethod {
45    /// Direct LU factorization (best for small problems)
46    Direct,
47    /// Conjugate Gradient Squared (iterative)
48    Cgs,
49    /// BiCGSTAB (iterative, more stable than CGS)
50    BiCgStab,
51}
52
53impl Default for SolverMethod {
54    fn default() -> Self {
55        SolverMethod::Direct
56    }
57}
58
59/// Assembly method for the BEM matrix
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum AssemblyMethod {
62    /// Traditional BEM with O(N^2) dense matrix
63    Tbem,
64    /// Single-Level Fast Multipole Method
65    Slfmm,
66    /// Multi-Level Fast Multipole Method
67    Mlfmm,
68}
69
70impl Default for AssemblyMethod {
71    fn default() -> Self {
72        AssemblyMethod::Tbem
73    }
74}
75
76/// Boundary condition type for the problem
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum BoundaryConditionType {
79    /// Rigid surface (zero normal velocity)
80    Rigid,
81    /// Soft surface (zero pressure)
82    Soft,
83    /// Impedance boundary condition
84    Impedance,
85}
86
87/// Definition of a BEM problem
88#[derive(Debug, Clone)]
89pub struct BemProblem {
90    /// Problem geometry mesh
91    pub mesh: Mesh,
92    /// Physical parameters
93    pub physics: PhysicsParams,
94    /// Incident field
95    pub incident_field: IncidentField,
96    /// Boundary condition type
97    pub bc_type: BoundaryConditionType,
98    /// Use Burton-Miller formulation (recommended for exterior problems)
99    pub use_burton_miller: bool,
100}
101
102impl BemProblem {
103    /// Create a rigid sphere scattering problem with plane wave incidence
104    ///
105    /// # Arguments
106    /// * `radius` - Sphere radius (m)
107    /// * `frequency` - Excitation frequency (Hz)
108    /// * `speed_of_sound` - Speed of sound (m/s)
109    /// * `density` - Medium density (kg/m�)
110    pub fn rigid_sphere_scattering(
111        radius: f64,
112        frequency: f64,
113        speed_of_sound: f64,
114        density: f64,
115    ) -> Self {
116        // Determine mesh resolution based on ka
117        let k = 2.0 * PI * frequency / speed_of_sound;
118        let ka = k * radius;
119
120        // Rule of thumb: ~10 elements per wavelength
121        // For a sphere, this translates to subdivision level
122        let subdivisions = if ka < 1.0 {
123            2 // Low frequency: coarse mesh ok
124        } else if ka < 5.0 {
125            3 // Medium frequency
126        } else {
127            4 // High frequency: need finer mesh
128        };
129
130        let mesh = generate_icosphere_mesh(radius, subdivisions);
131        let physics = PhysicsParams::new(frequency, speed_of_sound, density, false);
132        let incident_field = IncidentField::plane_wave_z();
133
134        Self {
135            mesh,
136            physics,
137            incident_field,
138            bc_type: BoundaryConditionType::Rigid,
139            use_burton_miller: true,
140        }
141    }
142
143    /// Create a rigid sphere scattering problem with custom mesh resolution
144    pub fn rigid_sphere_scattering_custom(
145        radius: f64,
146        frequency: f64,
147        speed_of_sound: f64,
148        density: f64,
149        n_theta: usize,
150        n_phi: usize,
151    ) -> Self {
152        let mesh = generate_sphere_mesh(radius, n_theta, n_phi);
153        let physics = PhysicsParams::new(frequency, speed_of_sound, density, false);
154        let incident_field = IncidentField::plane_wave_z();
155
156        Self {
157            mesh,
158            physics,
159            incident_field,
160            bc_type: BoundaryConditionType::Rigid,
161            use_burton_miller: true,
162        }
163    }
164
165    /// Set the incident field
166    pub fn with_incident_field(mut self, field: IncidentField) -> Self {
167        self.incident_field = field;
168        self
169    }
170
171    /// Set the boundary condition type
172    pub fn with_boundary_condition(mut self, bc_type: BoundaryConditionType) -> Self {
173        self.bc_type = bc_type;
174        self
175    }
176
177    /// Enable/disable Burton-Miller formulation
178    pub fn with_burton_miller(mut self, use_bm: bool) -> Self {
179        self.use_burton_miller = use_bm;
180        self
181    }
182
183    /// Get the wave number times radius (ka)
184    pub fn ka(&self) -> f64 {
185        self.physics.wave_number * self.mesh_radius()
186    }
187
188    /// Estimate mesh radius from bounding box
189    fn mesh_radius(&self) -> f64 {
190        // Estimate radius from mesh nodes
191        let mut max_r = 0.0f64;
192        for i in 0..self.mesh.nodes.nrows() {
193            let r = (self.mesh.nodes[[i, 0]].powi(2)
194                + self.mesh.nodes[[i, 1]].powi(2)
195                + self.mesh.nodes[[i, 2]].powi(2))
196            .sqrt();
197            max_r = max_r.max(r);
198        }
199        max_r
200    }
201}
202
203/// BEM solver configuration
204#[derive(Debug, Clone)]
205pub struct BemSolver {
206    /// Linear solver method
207    pub solver_method: SolverMethod,
208    /// Matrix assembly method
209    pub assembly_method: AssemblyMethod,
210    /// Maximum iterations for iterative solvers
211    pub max_iterations: usize,
212    /// Tolerance for iterative solvers
213    pub tolerance: f64,
214    /// Verbose output
215    pub verbose: bool,
216    /// Burton-Miller β scale factor (default: 4.0 for best accuracy at ka ~ 1)
217    pub beta_scale: f64,
218}
219
220impl Default for BemSolver {
221    fn default() -> Self {
222        Self {
223            solver_method: SolverMethod::Direct,
224            assembly_method: AssemblyMethod::Tbem,
225            max_iterations: 1000,
226            tolerance: 1e-8,
227            verbose: false,
228            beta_scale: 4.0, // Empirically optimal for ka ~ 1
229        }
230    }
231}
232
233impl BemSolver {
234    /// Create a new solver with default settings
235    pub fn new() -> Self {
236        Self::default()
237    }
238
239    /// Set the linear solver method
240    pub fn with_solver_method(mut self, method: SolverMethod) -> Self {
241        self.solver_method = method;
242        self
243    }
244
245    /// Set the assembly method
246    pub fn with_assembly_method(mut self, method: AssemblyMethod) -> Self {
247        self.assembly_method = method;
248        self
249    }
250
251    /// Set maximum iterations for iterative solvers
252    pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
253        self.max_iterations = max_iter;
254        self
255    }
256
257    /// Set tolerance for iterative solvers
258    pub fn with_tolerance(mut self, tol: f64) -> Self {
259        self.tolerance = tol;
260        self
261    }
262
263    /// Enable verbose output
264    pub fn with_verbose(mut self, verbose: bool) -> Self {
265        self.verbose = verbose;
266        self
267    }
268
269    /// Solve a BEM problem
270    ///
271    /// # Arguments
272    /// * `problem` - The BEM problem definition
273    ///
274    /// # Returns
275    /// Solution containing surface pressures and methods to evaluate fields
276    pub fn solve(&self, problem: &BemProblem) -> Result<BemSolution, BemError> {
277        if self.verbose {
278            log::info!(
279                "Solving BEM problem: {} elements, ka = {:.3}",
280                problem.mesh.elements.len(),
281                problem.ka()
282            );
283        }
284
285        // Step 1: Prepare elements with boundary conditions
286        let elements = self.prepare_elements(problem);
287
288        // Step 2: Assemble system matrix and RHS
289        let (matrix, rhs) = self.assemble_system(&elements, &problem.mesh.nodes, &problem.physics)?;
290
291        // Step 3: Add incident field contribution to RHS
292        let rhs = self.add_incident_field_rhs(
293            rhs,
294            &elements,
295            &problem.incident_field,
296            &problem.physics,
297            problem.use_burton_miller,
298        );
299
300        // Step 4: Solve linear system
301        let surface_pressure = self.solve_linear_system(&matrix, &rhs)?;
302
303        if self.verbose {
304            log::info!("Solution complete. Max surface pressure: {:.6}",
305                surface_pressure.iter().map(|p| p.norm()).fold(0.0f64, f64::max));
306        }
307
308        Ok(BemSolution {
309            surface_pressure,
310            elements,
311            nodes: problem.mesh.nodes.clone(),
312            incident_field: problem.incident_field.clone(),
313            physics: problem.physics.clone(),
314        })
315    }
316
317    /// Prepare elements with boundary conditions
318    fn prepare_elements(&self, problem: &BemProblem) -> Vec<Element> {
319        let mut elements = problem.mesh.elements.clone();
320
321        // Set boundary conditions based on problem type
322        let bc = match problem.bc_type {
323            BoundaryConditionType::Rigid => {
324                // Zero normal velocity (Neumann BC)
325                BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)])
326            }
327            BoundaryConditionType::Soft => {
328                // Zero pressure (Dirichlet BC)
329                BoundaryCondition::Pressure(vec![Complex64::new(0.0, 0.0)])
330            }
331            BoundaryConditionType::Impedance => {
332                // Default impedance (plane wave)
333                let z0 = problem.physics.density * problem.physics.speed_of_sound;
334                BoundaryCondition::VelocityWithAdmittance {
335                    velocity: vec![Complex64::new(0.0, 0.0)],
336                    admittance: Complex64::new(1.0 / z0, 0.0),
337                }
338            }
339        };
340
341        // Assign BC and DOF addresses to each element
342        for (i, elem) in elements.iter_mut().enumerate() {
343            elem.boundary_condition = bc.clone();
344            elem.dof_addresses = vec![i];
345        }
346
347        elements
348    }
349
350    /// Assemble the BEM system matrix
351    fn assemble_system(
352        &self,
353        elements: &[Element],
354        nodes: &Array2<f64>,
355        physics: &PhysicsParams,
356    ) -> Result<(Array2<Complex64>, Array1<Complex64>), BemError> {
357        match self.assembly_method {
358            AssemblyMethod::Tbem => {
359                // Use scaled Burton-Miller β for better accuracy
360                let beta = physics.burton_miller_beta_scaled(self.beta_scale);
361                let system = build_tbem_system_with_beta(elements, nodes, physics, beta);
362                Ok((system.matrix, system.rhs))
363            }
364            AssemblyMethod::Slfmm | AssemblyMethod::Mlfmm => {
365                // FMM methods not yet integrated into high-level API
366                Err(BemError::NotImplemented(
367                    "FMM assembly not yet available in high-level API".to_string(),
368                ))
369            }
370        }
371    }
372
373    /// Add incident field contribution to RHS
374    fn add_incident_field_rhs(
375        &self,
376        mut rhs: Array1<Complex64>,
377        elements: &[Element],
378        incident_field: &IncidentField,
379        physics: &PhysicsParams,
380        use_burton_miller: bool,
381    ) -> Array1<Complex64> {
382        // Collect element centers and normals
383        let n = elements.len();
384        let mut centers = Array2::zeros((n, 3));
385        let mut normals = Array2::zeros((n, 3));
386
387        for (i, elem) in elements.iter().enumerate() {
388            for j in 0..3 {
389                centers[[i, j]] = elem.center[j];
390                normals[[i, j]] = elem.normal[j];
391            }
392        }
393
394        // Compute incident field RHS contribution with scaled β
395        let incident_rhs = if use_burton_miller {
396            let beta = physics.burton_miller_beta_scaled(self.beta_scale);
397            incident_field.compute_rhs_with_beta(&centers, &normals, physics, beta)
398        } else {
399            incident_field.compute_rhs(&centers, &normals, physics, false)
400        };
401
402        // Add to system RHS
403        rhs = rhs + incident_rhs;
404
405        rhs
406    }
407
408    /// Solve the linear system
409    fn solve_linear_system(
410        &self,
411        matrix: &Array2<Complex64>,
412        rhs: &Array1<Complex64>,
413    ) -> Result<Array1<Complex64>, BemError> {
414        match self.solver_method {
415            SolverMethod::Direct => {
416                let solution = direct_solve(matrix, rhs);
417                if solution.success {
418                    Ok(solution.x)
419                } else {
420                    Err(BemError::SolverFailed("LU decomposition failed".to_string()))
421                }
422            }
423            SolverMethod::Cgs | SolverMethod::BiCgStab => {
424                // Iterative solvers not yet integrated
425                Err(BemError::NotImplemented(
426                    "Iterative solvers not yet available in high-level API".to_string(),
427                ))
428            }
429        }
430    }
431}
432
433/// Solution of a BEM problem
434#[derive(Debug, Clone)]
435pub struct BemSolution {
436    /// Surface pressure at each element
437    pub surface_pressure: Array1<Complex64>,
438    /// Elements used in the solution
439    pub elements: Vec<Element>,
440    /// Node coordinates
441    pub nodes: Array2<f64>,
442    /// Incident field used
443    pub incident_field: IncidentField,
444    /// Physics parameters
445    pub physics: PhysicsParams,
446}
447
448impl BemSolution {
449    /// Evaluate total pressure at a single point
450    pub fn evaluate_pressure(&self, point: &[f64; 3]) -> Complex64 {
451        let eval_points = Array2::from_shape_vec((1, 3), vec![point[0], point[1], point[2]]).unwrap();
452
453        let field_points = compute_total_field(
454            &eval_points,
455            &self.elements,
456            &self.nodes,
457            &self.surface_pressure,
458            None,
459            &self.incident_field,
460            &self.physics,
461        );
462
463        field_points[0].p_total
464    }
465
466    /// Evaluate total pressure at multiple points
467    pub fn evaluate_pressure_field(&self, points: &Array2<f64>) -> Vec<FieldPoint> {
468        compute_total_field(
469            points,
470            &self.elements,
471            &self.nodes,
472            &self.surface_pressure,
473            None,
474            &self.incident_field,
475            &self.physics,
476        )
477    }
478
479    /// Get max surface pressure magnitude
480    pub fn max_surface_pressure(&self) -> f64 {
481        self.surface_pressure
482            .iter()
483            .map(|p| p.norm())
484            .fold(0.0f64, f64::max)
485    }
486
487    /// Get mean surface pressure magnitude
488    pub fn mean_surface_pressure(&self) -> f64 {
489        let sum: f64 = self.surface_pressure.iter().map(|p| p.norm()).sum();
490        sum / self.surface_pressure.len() as f64
491    }
492
493    /// Number of DOFs in the solution
494    pub fn num_dofs(&self) -> usize {
495        self.surface_pressure.len()
496    }
497}
498
499/// BEM solver errors
500#[derive(Debug, Clone)]
501pub enum BemError {
502    /// Feature not yet implemented
503    NotImplemented(String),
504    /// Linear solver failed
505    SolverFailed(String),
506    /// Invalid mesh
507    InvalidMesh(String),
508    /// Invalid parameters
509    InvalidParameters(String),
510}
511
512impl std::fmt::Display for BemError {
513    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514        match self {
515            BemError::NotImplemented(msg) => write!(f, "Not implemented: {}", msg),
516            BemError::SolverFailed(msg) => write!(f, "Solver failed: {}", msg),
517            BemError::InvalidMesh(msg) => write!(f, "Invalid mesh: {}", msg),
518            BemError::InvalidParameters(msg) => write!(f, "Invalid parameters: {}", msg),
519        }
520    }
521}
522
523impl std::error::Error for BemError {}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_bem_problem_creation() {
531        let problem = BemProblem::rigid_sphere_scattering(0.1, 1000.0, 343.0, 1.21);
532
533        assert!(problem.mesh.elements.len() > 0);
534        assert!(problem.mesh.nodes.nrows() > 0);
535        assert!(problem.ka() > 0.0);
536    }
537
538    #[test]
539    fn test_bem_solver_creation() {
540        let solver = BemSolver::new()
541            .with_solver_method(SolverMethod::Direct)
542            .with_assembly_method(AssemblyMethod::Tbem)
543            .with_verbose(false);
544
545        assert_eq!(solver.solver_method, SolverMethod::Direct);
546        assert_eq!(solver.assembly_method, AssemblyMethod::Tbem);
547    }
548
549    #[test]
550    fn test_bem_solver_small_problem() {
551        // Very small problem for quick test
552        let problem = BemProblem::rigid_sphere_scattering_custom(
553            0.1,   // radius
554            100.0, // very low frequency for quick test
555            343.0,
556            1.21,
557            4, // coarse mesh
558            8,
559        );
560
561        let solver = BemSolver::new();
562        let result = solver.solve(&problem);
563
564        assert!(result.is_ok());
565        let solution = result.unwrap();
566        assert!(solution.num_dofs() > 0);
567        assert!(solution.max_surface_pressure() > 0.0);
568    }
569
570    #[test]
571    fn test_field_evaluation() {
572        // Very small problem
573        let problem = BemProblem::rigid_sphere_scattering_custom(
574            0.1,
575            100.0,
576            343.0,
577            1.21,
578            4,
579            8,
580        );
581
582        let solver = BemSolver::new();
583        let solution = solver.solve(&problem).unwrap();
584
585        // Evaluate at a point outside the sphere
586        let p = solution.evaluate_pressure(&[0.0, 0.0, 0.2]);
587
588        // Should have some pressure (incident + scattered)
589        assert!(p.norm() > 0.0);
590    }
591}