math_audio_bem/core/assembly/
mlfmm.rs

1//! Multi-Level Fast Multipole Method (MLFMM) assembly
2//!
3//! Direct port of NC_BuildSystemFMBEM (MLFMM mode) from NC_EquationSystem.cpp.
4//!
5//! The MLFMM extends SLFMM by using a hierarchical cluster tree:
6//! ```text
7//! Level 0:    Root cluster
8//!             |
9//! Level 1:    Child clusters
10//!             |
11//! Level 2:    Leaf clusters (elements)
12//! ```
13//!
14//! The matrix-vector product proceeds:
15//! 1. Upward pass: Aggregate sources from leaves to root using T-matrices
16//! 2. Translation: Translate expansions between far clusters using D-matrices
17//! 3. Downward pass: Disaggregate to leaves using S-matrices
18//! 4. Near-field: Direct interactions at leaf level
19
20use ndarray::{Array1, Array2};
21use num_complex::Complex64;
22
23use crate::core::integration::{regular_integration, singular_integration, unit_sphere_quadrature};
24use crate::core::types::{Cluster, ClusterLevel, Element, PhysicsParams};
25use math_audio_wave::special::spherical_hankel_first_kind;
26
27/// Result of MLFMM assembly
28pub struct MlfmmSystem {
29    /// Number of levels in the tree
30    pub num_levels: usize,
31    /// Near-field coefficient matrix at leaf level (sparse, stored as dense blocks)
32    pub near_matrix: Vec<NearFieldBlock>,
33    /// T-matrices at each level (child-to-parent aggregation)
34    /// Level 0 has no T-matrices (root)
35    pub t_matrices: Vec<LevelMatrices>,
36    /// D-matrices for far-field translation at each level
37    pub d_matrices: Vec<Vec<DMatrixEntry>>,
38    /// S-matrices at each level (parent-to-child disaggregation)
39    pub s_matrices: Vec<LevelMatrices>,
40    /// RHS vector
41    pub rhs: Array1<Complex64>,
42    /// Number of DOFs
43    pub num_dofs: usize,
44    /// Cluster tree (indexed by level)
45    cluster_levels: Vec<ClusterLevel>,
46    /// DOF mappings at leaf level: for each leaf cluster, the global DOF indices
47    pub leaf_dof_indices: Vec<Vec<usize>>,
48    /// Number of sphere points at each level
49    pub sphere_points_per_level: Vec<usize>,
50}
51
52/// Near-field block between two leaf clusters
53#[derive(Debug, Clone)]
54pub struct NearFieldBlock {
55    /// Source cluster index
56    pub source_cluster: usize,
57    /// Field cluster index
58    pub field_cluster: usize,
59    /// Dense coefficient matrix for this block
60    pub coefficients: Array2<Complex64>,
61}
62
63/// D-matrix entry for a far cluster pair
64///
65/// The D-matrix for FMM is diagonal (same multipole index in source and field),
66/// so we store only the diagonal entries to save memory.
67#[derive(Debug, Clone)]
68pub struct DMatrixEntry {
69    /// Source cluster index
70    pub source_cluster: usize,
71    /// Field cluster index
72    pub field_cluster: usize,
73    /// Level in tree
74    pub level: usize,
75    /// Diagonal translation coefficients (length = num_sphere_points)
76    pub diagonal: Array1<Complex64>,
77}
78
79/// Matrices at a single level of the tree
80#[derive(Debug, Clone)]
81pub struct LevelMatrices {
82    /// Level index
83    pub level: usize,
84    /// Number of clusters at this level
85    pub num_clusters: usize,
86    /// Matrices indexed by cluster
87    pub matrices: Vec<Array2<Complex64>>,
88}
89
90impl MlfmmSystem {
91    /// Create a new empty MLFMM system
92    pub fn new(num_dofs: usize, num_levels: usize, cluster_levels: Vec<ClusterLevel>) -> Self {
93        let sphere_points_per_level: Vec<usize> = cluster_levels
94            .iter()
95            .map(|level| level.theta_points * level.phi_points)
96            .collect();
97
98        Self {
99            num_levels,
100            near_matrix: Vec::new(),
101            t_matrices: Vec::with_capacity(num_levels),
102            d_matrices: Vec::with_capacity(num_levels),
103            s_matrices: Vec::with_capacity(num_levels),
104            rhs: Array1::zeros(num_dofs),
105            num_dofs,
106            cluster_levels,
107            leaf_dof_indices: Vec::new(),
108            sphere_points_per_level,
109        }
110    }
111
112    /// Apply the MLFMM operator: y = ([N] + [S][D][T]) * x
113    ///
114    /// Multi-level version with upward/downward passes.
115    ///
116    /// The algorithm proceeds as follows:
117    /// 1. **Near-field**: Direct interactions between nearby leaf clusters
118    /// 2. **Upward pass**: Aggregate source multipoles from leaves to root (M2M translations)
119    /// 3. **Translation**: Translate multipoles to locals between far clusters at each level (M2L)
120    /// 4. **Downward pass**: Disaggregate local expansions from root to leaves (L2L translations)
121    /// 5. **Evaluation**: Evaluate local expansions at leaf DOFs
122    ///
123    /// # Arguments
124    /// * `x` - Input vector (length = num_dofs)
125    ///
126    /// # Returns
127    /// * `y` - Output vector (length = num_dofs), y = A*x
128    pub fn matvec(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
129        let mut y = Array1::zeros(self.num_dofs);
130
131        if self.num_levels == 0 {
132            return y;
133        }
134
135        let leaf_level = self.num_levels - 1;
136
137        // === 1. Near-field contribution: y += [N] * x ===
138        // Note: block.coefficients is (n_source, n_field) where:
139        // - source = collocation points (rows, where we evaluate)
140        // - field = integration elements (columns, source of influence)
141        // For y = A*x: gather x from field DOFs, scatter y to source DOFs
142        for block in &self.near_matrix {
143            if block.source_cluster >= self.leaf_dof_indices.len()
144                || block.field_cluster >= self.leaf_dof_indices.len()
145            {
146                continue;
147            }
148
149            let src_dofs = &self.leaf_dof_indices[block.source_cluster];
150            let fld_dofs = &self.leaf_dof_indices[block.field_cluster];
151
152            // Gather x values from field cluster (columns of the matrix)
153            let x_local: Array1<Complex64> = Array1::from_iter(fld_dofs.iter().map(|&i| x[i]));
154
155            // Apply block matrix: y_local[i] = sum_j A[i,j] * x[j]
156            let y_local = block.coefficients.dot(&x_local);
157
158            // Scatter to source DOFs (rows of the matrix)
159            for (local_i, &global_i) in src_dofs.iter().enumerate() {
160                if local_i < y_local.len() {
161                    y[global_i] += y_local[local_i];
162                }
163            }
164
165            // Handle symmetric storage: if src != fld, also apply the (fld, src) block
166            // which is the transpose of this block
167            if block.source_cluster != block.field_cluster {
168                // Gather x from source cluster DOFs
169                let x_src: Array1<Complex64> = Array1::from_iter(src_dofs.iter().map(|&i| x[i]));
170                // Apply transpose: the (fld, src) block
171                let y_fld = block.coefficients.t().dot(&x_src);
172                // Scatter to field cluster DOFs
173                for (local_j, &global_j) in fld_dofs.iter().enumerate() {
174                    if local_j < y_fld.len() {
175                        y[global_j] += y_fld[local_j];
176                    }
177                }
178            }
179        }
180
181        // === 2. Far-field contribution using multi-level passes ===
182        if self.num_levels > 1 && !self.t_matrices.is_empty() {
183            // 2a. Upward pass: compute multipole expansions at each level
184            let multipoles = self.upward_pass(x);
185
186            // 2b. Translation at each level: M2L (multipole to local)
187            let mut locals = self.translate_all_levels(&multipoles);
188
189            // 2c. Downward pass: propagate locals from root to leaves
190            self.downward_pass(&mut locals);
191
192            // 2d. Evaluate locals at leaf DOFs
193            self.evaluate_locals(&locals, leaf_level, &mut y);
194        }
195
196        y
197    }
198
199    /// Upward pass: aggregate sources using T-matrices (M2M translations)
200    ///
201    /// At leaf level: T_leaf * x → multipole expansion
202    /// At non-leaf levels: Aggregate child multipoles → parent multipole
203    fn upward_pass(&self, x: &Array1<Complex64>) -> Vec<Vec<Array1<Complex64>>> {
204        let mut multipoles: Vec<Vec<Array1<Complex64>>> = Vec::with_capacity(self.num_levels);
205
206        // Initialize storage for each level
207        for level in 0..self.num_levels {
208            let num_clusters = if level < self.cluster_levels.len() {
209                self.cluster_levels[level].clusters.len()
210            } else {
211                0
212            };
213            let num_points = if level < self.sphere_points_per_level.len() {
214                self.sphere_points_per_level[level]
215            } else {
216                0
217            };
218            multipoles.push(
219                (0..num_clusters)
220                    .map(|_| Array1::zeros(num_points))
221                    .collect(),
222            );
223        }
224
225        let leaf_level = self.num_levels - 1;
226
227        // Step 1: Compute leaf-level multipoles from source DOFs
228        if leaf_level < self.t_matrices.len() {
229            let t_level = &self.t_matrices[leaf_level];
230            for cluster_idx in 0..t_level.num_clusters {
231                if cluster_idx >= self.leaf_dof_indices.len() {
232                    continue;
233                }
234                let cluster_dofs = &self.leaf_dof_indices[cluster_idx];
235                if cluster_dofs.is_empty() {
236                    continue;
237                }
238                let t_mat = &t_level.matrices[cluster_idx];
239                if t_mat.is_empty() {
240                    continue;
241                }
242                let x_local: Array1<Complex64> =
243                    Array1::from_iter(cluster_dofs.iter().map(|&i| x[i]));
244                if x_local.len() == t_mat.ncols() {
245                    multipoles[leaf_level][cluster_idx] = t_mat.dot(&x_local);
246                }
247            }
248        }
249
250        // Step 2: Propagate upward through tree (M2M translations)
251        // Traverse from leaf-1 to root (level 0)
252        for level in (0..leaf_level).rev() {
253            if level >= self.t_matrices.len() || level >= self.cluster_levels.len() {
254                continue;
255            }
256
257            let t_level = &self.t_matrices[level];
258            let clusters = &self.cluster_levels[level].clusters;
259
260            for (cluster_idx, cluster) in clusters.iter().enumerate() {
261                if cluster_idx >= t_level.matrices.len() {
262                    continue;
263                }
264
265                // Aggregate multipoles from all children
266                let t_mat = &t_level.matrices[cluster_idx];
267                if t_mat.is_empty() || cluster.sons.is_empty() {
268                    continue;
269                }
270
271                // Concatenate child multipoles and apply T-matrix
272                let child_level = level + 1;
273                if child_level >= multipoles.len() {
274                    continue;
275                }
276
277                let child_num_points = self
278                    .sphere_points_per_level
279                    .get(child_level)
280                    .copied()
281                    .unwrap_or(0);
282                let mut child_multipoles = Array1::zeros(cluster.sons.len() * child_num_points);
283
284                for (son_idx, &child_cluster_idx) in cluster.sons.iter().enumerate() {
285                    if child_cluster_idx < multipoles[child_level].len() {
286                        let child_mult = &multipoles[child_level][child_cluster_idx];
287                        let offset = son_idx * child_num_points;
288                        for (i, &val) in child_mult.iter().enumerate() {
289                            if offset + i < child_multipoles.len() {
290                                child_multipoles[offset + i] = val;
291                            }
292                        }
293                    }
294                }
295
296                if child_multipoles.len() == t_mat.ncols() {
297                    multipoles[level][cluster_idx] = t_mat.dot(&child_multipoles);
298                }
299            }
300        }
301
302        multipoles
303    }
304
305    /// Translate multipoles to locals at each level (M2L translations)
306    fn translate_all_levels(
307        &self,
308        multipoles: &[Vec<Array1<Complex64>>],
309    ) -> Vec<Vec<Array1<Complex64>>> {
310        let mut locals: Vec<Vec<Array1<Complex64>>> = Vec::with_capacity(self.num_levels);
311
312        // Initialize storage
313        for level in 0..self.num_levels {
314            let num_clusters = if level < self.cluster_levels.len() {
315                self.cluster_levels[level].clusters.len()
316            } else {
317                0
318            };
319            let num_points = self
320                .sphere_points_per_level
321                .get(level)
322                .copied()
323                .unwrap_or(0);
324            locals.push(
325                (0..num_clusters)
326                    .map(|_| Array1::zeros(num_points))
327                    .collect(),
328            );
329        }
330
331        // Apply D-matrices at each level
332        for (level, d_entries) in self.d_matrices.iter().enumerate() {
333            if level >= multipoles.len() || level >= locals.len() {
334                continue;
335            }
336
337            for d_entry in d_entries {
338                if d_entry.source_cluster >= multipoles[level].len()
339                    || d_entry.field_cluster >= locals[level].len()
340                {
341                    continue;
342                }
343
344                let src_mult = &multipoles[level][d_entry.source_cluster];
345                if src_mult.len() != d_entry.diagonal.len() {
346                    continue;
347                }
348
349                // D-matrix is diagonal, so D*x is element-wise multiplication
350                for (i, (&d, &s)) in d_entry.diagonal.iter().zip(src_mult.iter()).enumerate() {
351                    if i < locals[level][d_entry.field_cluster].len() {
352                        locals[level][d_entry.field_cluster][i] += d * s;
353                    }
354                }
355            }
356        }
357
358        locals
359    }
360
361    /// Downward pass: propagate locals from root to leaves (L2L translations)
362    fn downward_pass(&self, locals: &mut [Vec<Array1<Complex64>>]) {
363        // Traverse from root to leaves (level 0 to leaf_level)
364        for level in 0..self.num_levels.saturating_sub(1) {
365            if level >= self.s_matrices.len() || level >= self.cluster_levels.len() {
366                continue;
367            }
368
369            let s_level = &self.s_matrices[level];
370            let clusters = &self.cluster_levels[level].clusters;
371
372            for (cluster_idx, cluster) in clusters.iter().enumerate() {
373                if cluster_idx >= s_level.matrices.len() || cluster.sons.is_empty() {
374                    continue;
375                }
376
377                let s_mat = &s_level.matrices[cluster_idx];
378                if s_mat.is_empty() {
379                    continue;
380                }
381
382                // Propagate parent local to children
383                let parent_local = &locals[level][cluster_idx];
384                if parent_local.len() != s_mat.ncols() {
385                    continue;
386                }
387
388                let child_locals = s_mat.dot(parent_local);
389                let child_level = level + 1;
390                if child_level >= locals.len() {
391                    continue;
392                }
393
394                let child_num_points = self
395                    .sphere_points_per_level
396                    .get(child_level)
397                    .copied()
398                    .unwrap_or(0);
399
400                // Distribute to children
401                for (son_idx, &child_cluster_idx) in cluster.sons.iter().enumerate() {
402                    if child_cluster_idx >= locals[child_level].len() {
403                        continue;
404                    }
405
406                    let offset = son_idx * child_num_points;
407                    for i in 0..child_num_points {
408                        if offset + i < child_locals.len()
409                            && i < locals[child_level][child_cluster_idx].len()
410                        {
411                            locals[child_level][child_cluster_idx][i] += child_locals[offset + i];
412                        }
413                    }
414                }
415            }
416        }
417    }
418
419    /// Evaluate local expansions at leaf-level DOFs
420    fn evaluate_locals(
421        &self,
422        locals: &[Vec<Array1<Complex64>>],
423        leaf_level: usize,
424        y: &mut Array1<Complex64>,
425    ) {
426        if leaf_level >= self.s_matrices.len() || leaf_level >= locals.len() {
427            return;
428        }
429
430        let s_level = &self.s_matrices[leaf_level];
431
432        for cluster_idx in 0..s_level.num_clusters {
433            if cluster_idx >= self.leaf_dof_indices.len() || cluster_idx >= locals[leaf_level].len()
434            {
435                continue;
436            }
437
438            let cluster_dofs = &self.leaf_dof_indices[cluster_idx];
439            if cluster_dofs.is_empty() {
440                continue;
441            }
442
443            let s_mat = &s_level.matrices[cluster_idx];
444            if s_mat.is_empty() {
445                continue;
446            }
447
448            let local_exp = &locals[leaf_level][cluster_idx];
449            if local_exp.len() != s_mat.ncols() {
450                continue;
451            }
452
453            let y_local = s_mat.dot(local_exp);
454            for (local_j, &global_j) in cluster_dofs.iter().enumerate() {
455                if local_j < y_local.len() && global_j < y.len() {
456                    y[global_j] += y_local[local_j];
457                }
458            }
459        }
460    }
461
462    /// Get cluster at given level and index
463    pub fn get_cluster(&self, level: usize, index: usize) -> Option<&Cluster> {
464        self.cluster_levels.get(level)?.clusters.get(index)
465    }
466
467    /// Get number of clusters at a given level
468    pub fn num_clusters_at_level(&self, level: usize) -> usize {
469        self.cluster_levels
470            .get(level)
471            .map(|l| l.clusters.len())
472            .unwrap_or(0)
473    }
474}
475
476/// Build the MLFMM system matrices
477///
478/// # Arguments
479/// * `elements` - Vector of mesh elements
480/// * `nodes` - Node coordinates (num_nodes × 3)
481/// * `cluster_levels` - Hierarchical cluster tree
482/// * `physics` - Physics parameters
483pub fn build_mlfmm_system(
484    elements: &[Element],
485    nodes: &Array2<f64>,
486    cluster_levels: Vec<ClusterLevel>,
487    physics: &PhysicsParams,
488) -> MlfmmSystem {
489    let num_dofs = count_dofs(elements);
490    let num_levels = cluster_levels.len();
491
492    let mut system = MlfmmSystem::new(num_dofs, num_levels, cluster_levels.clone());
493
494    // Build leaf DOF mappings (needed for matvec)
495    if let Some(leaf_level) = cluster_levels.last() {
496        build_leaf_dof_mappings(&mut system, elements, &leaf_level.clusters);
497    }
498
499    // Build near-field matrix at leaf level
500    if let Some(leaf_level) = cluster_levels.last() {
501        build_near_field(&mut system, elements, nodes, &leaf_level.clusters, physics);
502    }
503
504    // Build T-matrices at each level (leaves to root)
505    for (level_idx, level) in cluster_levels.iter().enumerate().rev() {
506        let (sphere_coords, sphere_weights) =
507            unit_sphere_quadrature(level.theta_points, level.phi_points);
508
509        let t_matrices = build_t_matrices_level(
510            elements,
511            &level.clusters,
512            physics,
513            &sphere_coords,
514            &sphere_weights,
515            level_idx,
516            &cluster_levels,
517        );
518
519        system.t_matrices.push(t_matrices);
520    }
521    system.t_matrices.reverse(); // Order from root to leaves
522
523    // Build D-matrices at each level
524    for (level_idx, level) in cluster_levels.iter().enumerate() {
525        let num_sphere_points = level.theta_points * level.phi_points;
526
527        let d_matrices = build_d_matrices_level(
528            &level.clusters,
529            physics,
530            &[], // sphere_coords not needed for diagonal D-matrices
531            level.expansion_terms,
532            level_idx,
533            num_sphere_points,
534        );
535
536        system.d_matrices.push(d_matrices);
537    }
538
539    // Build S-matrices at each level (root to leaves)
540    for (level_idx, level) in cluster_levels.iter().enumerate() {
541        let (sphere_coords, sphere_weights) =
542            unit_sphere_quadrature(level.theta_points, level.phi_points);
543
544        let s_matrices = build_s_matrices_level(
545            elements,
546            &level.clusters,
547            physics,
548            &sphere_coords,
549            &sphere_weights,
550            level_idx,
551            &cluster_levels,
552        );
553
554        system.s_matrices.push(s_matrices);
555    }
556
557    system
558}
559
560/// Build the mapping from leaf clusters to global DOF indices
561fn build_leaf_dof_mappings(
562    system: &mut MlfmmSystem,
563    elements: &[Element],
564    leaf_clusters: &[Cluster],
565) {
566    for cluster in leaf_clusters {
567        let mut dof_indices = Vec::new();
568        for &elem_idx in &cluster.element_indices {
569            let elem = &elements[elem_idx];
570            if elem.property.is_evaluation() {
571                continue;
572            }
573            // Collect all DOF addresses for this element
574            dof_indices.extend(elem.dof_addresses.iter().copied());
575        }
576        system.leaf_dof_indices.push(dof_indices);
577    }
578}
579
580/// Count total number of DOFs
581fn count_dofs(elements: &[Element]) -> usize {
582    elements
583        .iter()
584        .filter(|e| !e.property.is_evaluation())
585        .map(|e| e.dof_addresses.len())
586        .sum()
587}
588
589/// Build near-field matrix blocks at leaf level
590fn build_near_field(
591    system: &mut MlfmmSystem,
592    elements: &[Element],
593    nodes: &Array2<f64>,
594    leaf_clusters: &[Cluster],
595    physics: &PhysicsParams,
596) {
597    let gamma = Complex64::new(physics.gamma(), 0.0);
598    let tau = Complex64::new(physics.tau, 0.0);
599    let beta = physics.burton_miller_beta();
600
601    // For each leaf cluster pair in near-field
602    for (i, cluster_i) in leaf_clusters.iter().enumerate() {
603        // Self-interaction
604        let block = compute_near_block(
605            elements,
606            nodes,
607            &cluster_i.element_indices,
608            &cluster_i.element_indices,
609            physics,
610            gamma,
611            tau,
612            beta,
613            true,
614        );
615        system.near_matrix.push(NearFieldBlock {
616            source_cluster: i,
617            field_cluster: i,
618            coefficients: block,
619        });
620
621        // Interaction with near clusters
622        for &j in &cluster_i.near_clusters {
623            if j > i {
624                let cluster_j = &leaf_clusters[j];
625                let block = compute_near_block(
626                    elements,
627                    nodes,
628                    &cluster_i.element_indices,
629                    &cluster_j.element_indices,
630                    physics,
631                    gamma,
632                    tau,
633                    beta,
634                    false,
635                );
636                system.near_matrix.push(NearFieldBlock {
637                    source_cluster: i,
638                    field_cluster: j,
639                    coefficients: block,
640                });
641            }
642        }
643    }
644}
645
646/// Compute a near-field block between two sets of elements
647fn compute_near_block(
648    elements: &[Element],
649    nodes: &Array2<f64>,
650    source_indices: &[usize],
651    field_indices: &[usize],
652    physics: &PhysicsParams,
653    gamma: Complex64,
654    tau: Complex64,
655    beta: Complex64,
656    is_self: bool,
657) -> Array2<Complex64> {
658    let n_source = source_indices.len();
659    let n_field = field_indices.len();
660    let mut block = Array2::zeros((n_source, n_field));
661
662    for (i, &src_idx) in source_indices.iter().enumerate() {
663        let source_elem = &elements[src_idx];
664        if source_elem.property.is_evaluation() {
665            continue;
666        }
667
668        for (j, &fld_idx) in field_indices.iter().enumerate() {
669            let field_elem = &elements[fld_idx];
670            if field_elem.property.is_evaluation() {
671                continue;
672            }
673
674            let element_coords = get_element_coords(field_elem, nodes);
675
676            let result = if is_self && src_idx == fld_idx {
677                // Singular integration
678                singular_integration(
679                    &source_elem.center,
680                    &source_elem.normal,
681                    &element_coords,
682                    field_elem.element_type,
683                    physics,
684                    None,
685                    0,
686                    false,
687                )
688            } else {
689                // Regular integration
690                regular_integration(
691                    &source_elem.center,
692                    &source_elem.normal,
693                    &element_coords,
694                    field_elem.element_type,
695                    field_elem.area,
696                    physics,
697                    None,
698                    0,
699                    false,
700                )
701            };
702
703            // Assemble using Burton-Miller formulation
704            let coeff = result.dg_dn_integral * gamma * tau + result.d2g_dnxdny_integral * beta;
705            block[[i, j]] = coeff;
706        }
707    }
708
709    block
710}
711
712/// Build T-matrices at a single level
713fn build_t_matrices_level(
714    elements: &[Element],
715    clusters: &[Cluster],
716    physics: &PhysicsParams,
717    sphere_coords: &[[f64; 3]],
718    sphere_weights: &[f64],
719    level_idx: usize,
720    cluster_levels: &[ClusterLevel],
721) -> LevelMatrices {
722    let k = physics.wave_number;
723    let num_sphere_points = sphere_coords.len();
724    let num_clusters = clusters.len();
725    let mut matrices = Vec::with_capacity(num_clusters);
726
727    let is_leaf_level = level_idx == cluster_levels.len() - 1;
728
729    for cluster in clusters {
730        if is_leaf_level {
731            // Leaf level: T-matrix maps element DOFs to multipole expansion
732            let num_elem = cluster.element_indices.len();
733            let mut t_matrix = Array2::zeros((num_sphere_points, num_elem));
734
735            for (j, &elem_idx) in cluster.element_indices.iter().enumerate() {
736                let elem = &elements[elem_idx];
737                if elem.property.is_evaluation() {
738                    continue;
739                }
740
741                for (p, coord) in sphere_coords.iter().enumerate() {
742                    let diff: Vec<f64> =
743                        (0..3).map(|d| elem.center[d] - cluster.center[d]).collect();
744                    let s_dot_diff: f64 = (0..3).map(|d| coord[d] * diff[d]).sum();
745
746                    let exp_factor =
747                        Complex64::new((k * s_dot_diff).cos(), -(k * s_dot_diff).sin());
748
749                    t_matrix[[p, j]] = exp_factor * sphere_weights[p];
750                }
751            }
752
753            matrices.push(t_matrix);
754        } else {
755            // Non-leaf: T-matrix aggregates child multipoles
756            let num_children = cluster.sons.len();
757            let child_num_points = if level_idx + 1 < cluster_levels.len() {
758                cluster_levels[level_idx + 1].theta_points
759                    * cluster_levels[level_idx + 1].phi_points
760            } else {
761                num_sphere_points
762            };
763
764            let mut t_matrix = Array2::zeros((num_sphere_points, num_children * child_num_points));
765
766            // Build aggregation operator for each child
767            for (child_idx, &child_cluster_idx) in cluster.sons.iter().enumerate() {
768                if level_idx + 1 < cluster_levels.len() {
769                    let child_cluster = &cluster_levels[level_idx + 1].clusters[child_cluster_idx];
770
771                    // Translation from child center to parent center
772                    for (p, coord) in sphere_coords.iter().enumerate() {
773                        let diff: Vec<f64> = (0..3)
774                            .map(|d| child_cluster.center[d] - cluster.center[d])
775                            .collect();
776                        let s_dot_diff: f64 = (0..3).map(|d| coord[d] * diff[d]).sum();
777
778                        let exp_factor =
779                            Complex64::new((k * s_dot_diff).cos(), -(k * s_dot_diff).sin());
780
781                        // Map child sphere points to parent
782                        for cp in 0..child_num_points {
783                            t_matrix[[p, child_idx * child_num_points + cp]] =
784                                exp_factor * sphere_weights[p] / child_num_points as f64;
785                        }
786                    }
787                }
788            }
789
790            matrices.push(t_matrix);
791        }
792    }
793
794    LevelMatrices {
795        level: level_idx,
796        num_clusters,
797        matrices,
798    }
799}
800
801/// Build D-matrices at a single level
802///
803/// The D-matrix is diagonal in the FMM, so we only store the diagonal entries.
804/// This reduces memory from O(P²) to O(P) per cluster pair.
805fn build_d_matrices_level(
806    clusters: &[Cluster],
807    physics: &PhysicsParams,
808    _sphere_coords: &[[f64; 3]],
809    n_terms: usize,
810    level_idx: usize,
811    num_sphere_points: usize,
812) -> Vec<DMatrixEntry> {
813    let k = physics.wave_number;
814    let mut d_entries = Vec::new();
815
816    for (i, cluster_i) in clusters.iter().enumerate() {
817        for &j in &cluster_i.far_clusters {
818            let cluster_j = &clusters[j];
819
820            // Distance vector between cluster centers
821            let diff: Vec<f64> = (0..3)
822                .map(|d| cluster_i.center[d] - cluster_j.center[d])
823                .collect();
824            let r = (diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2]).sqrt();
825
826            if r < 1e-15 {
827                continue; // Skip coincident clusters
828            }
829
830            let kr = k * r;
831
832            // Compute translation operator using multipole expansion
833            let h_funcs = spherical_hankel_first_kind(n_terms.max(2), kr, 1.0);
834
835            // D-matrix is diagonal: D[p,p] = h_0(kr) * ik
836            // Store only the diagonal (all entries are the same in this simplified model)
837            let d_value = h_funcs[0] * Complex64::new(0.0, k);
838            let diagonal = Array1::from_elem(num_sphere_points, d_value);
839
840            d_entries.push(DMatrixEntry {
841                source_cluster: i,
842                field_cluster: j,
843                level: level_idx,
844                diagonal,
845            });
846        }
847    }
848
849    d_entries
850}
851
852/// Build S-matrices at a single level
853fn build_s_matrices_level(
854    elements: &[Element],
855    clusters: &[Cluster],
856    physics: &PhysicsParams,
857    sphere_coords: &[[f64; 3]],
858    sphere_weights: &[f64],
859    level_idx: usize,
860    cluster_levels: &[ClusterLevel],
861) -> LevelMatrices {
862    let k = physics.wave_number;
863    let num_sphere_points = sphere_coords.len();
864    let num_clusters = clusters.len();
865    let mut matrices = Vec::with_capacity(num_clusters);
866
867    let is_leaf_level = level_idx == cluster_levels.len() - 1;
868
869    for cluster in clusters {
870        if is_leaf_level {
871            // Leaf level: S-matrix maps local expansion to element DOFs
872            let num_elem = cluster.element_indices.len();
873            let mut s_matrix = Array2::zeros((num_elem, num_sphere_points));
874
875            for (j, &elem_idx) in cluster.element_indices.iter().enumerate() {
876                let elem = &elements[elem_idx];
877                if elem.property.is_evaluation() {
878                    continue;
879                }
880
881                for (p, coord) in sphere_coords.iter().enumerate() {
882                    let diff: Vec<f64> =
883                        (0..3).map(|d| elem.center[d] - cluster.center[d]).collect();
884                    let s_dot_diff: f64 = (0..3).map(|d| coord[d] * diff[d]).sum();
885
886                    let exp_factor = Complex64::new((k * s_dot_diff).cos(), (k * s_dot_diff).sin());
887
888                    s_matrix[[j, p]] = exp_factor * sphere_weights[p];
889                }
890            }
891
892            matrices.push(s_matrix);
893        } else {
894            // Non-leaf: S-matrix disaggregates to children
895            let num_children = cluster.sons.len();
896            let child_num_points = if level_idx + 1 < cluster_levels.len() {
897                cluster_levels[level_idx + 1].theta_points
898                    * cluster_levels[level_idx + 1].phi_points
899            } else {
900                num_sphere_points
901            };
902
903            let mut s_matrix = Array2::zeros((num_children * child_num_points, num_sphere_points));
904
905            for (child_idx, &child_cluster_idx) in cluster.sons.iter().enumerate() {
906                if level_idx + 1 < cluster_levels.len() {
907                    let child_cluster = &cluster_levels[level_idx + 1].clusters[child_cluster_idx];
908
909                    for (p, coord) in sphere_coords.iter().enumerate() {
910                        let diff: Vec<f64> = (0..3)
911                            .map(|d| child_cluster.center[d] - cluster.center[d])
912                            .collect();
913                        let s_dot_diff: f64 = (0..3).map(|d| coord[d] * diff[d]).sum();
914
915                        let exp_factor =
916                            Complex64::new((k * s_dot_diff).cos(), (k * s_dot_diff).sin());
917
918                        for cp in 0..child_num_points {
919                            s_matrix[[child_idx * child_num_points + cp, p]] =
920                                exp_factor * sphere_weights[p] / child_num_points as f64;
921                        }
922                    }
923                }
924            }
925
926            matrices.push(s_matrix);
927        }
928    }
929
930    LevelMatrices {
931        level: level_idx,
932        num_clusters,
933        matrices,
934    }
935}
936
937/// Get element node coordinates as Array2
938fn get_element_coords(element: &Element, nodes: &Array2<f64>) -> Array2<f64> {
939    let num_nodes = element.connectivity.len();
940    let mut coords = Array2::zeros((num_nodes, 3));
941
942    for (i, &node_idx) in element.connectivity.iter().enumerate() {
943        for j in 0..3 {
944            coords[[i, j]] = nodes[[node_idx, j]];
945        }
946    }
947
948    coords
949}
950
951/// Estimate the optimal number of levels for the cluster tree
952///
953/// Based on NC_EstimateNumLevelsMLFMM from the C++ code.
954pub fn estimate_num_levels(
955    num_elements: usize,
956    elements_per_leaf: usize,
957    min_levels: usize,
958    max_levels: usize,
959) -> usize {
960    if num_elements == 0 {
961        return min_levels;
962    }
963
964    // Estimate: each level divides by ~8 (octree)
965    let mut levels = 1;
966    let mut n = num_elements;
967
968    while n > elements_per_leaf && levels < max_levels {
969        n /= 8;
970        levels += 1;
971    }
972
973    levels.max(min_levels).min(max_levels)
974}
975
976/// Build cluster tree from elements
977///
978/// Creates a hierarchical octree-like structure.
979pub fn build_cluster_tree(
980    elements: &[Element],
981    target_elements_per_leaf: usize,
982    physics: &PhysicsParams,
983) -> Vec<ClusterLevel> {
984    let num_elements = elements.len();
985    let num_levels = estimate_num_levels(num_elements, target_elements_per_leaf, 1, 8);
986
987    let mut levels = Vec::with_capacity(num_levels);
988
989    // Compute bounding box
990    let (min_coords, max_coords) = compute_bounding_box(elements);
991    let root_center = [
992        (min_coords[0] + max_coords[0]) / 2.0,
993        (min_coords[1] + max_coords[1]) / 2.0,
994        (min_coords[2] + max_coords[2]) / 2.0,
995    ];
996    let root_radius = ((max_coords[0] - min_coords[0]).powi(2)
997        + (max_coords[1] - min_coords[1]).powi(2)
998        + (max_coords[2] - min_coords[2]).powi(2))
999    .sqrt()
1000        / 2.0;
1001
1002    // Create root cluster
1003    let root_cluster = Cluster::new(Array1::from_vec(root_center.to_vec()));
1004    let all_indices: Vec<usize> = (0..num_elements).collect();
1005
1006    // Build tree recursively
1007    let mut root_level = ClusterLevel::new(1);
1008    let mut root = root_cluster;
1009    root.element_indices = all_indices;
1010    root.radius = root_radius;
1011    root.level = 0;
1012
1013    // Set expansion parameters based on wave number
1014    let kr = physics.wave_number * root_radius;
1015    root_level.expansion_terms = ((kr + 6.0 * kr.ln().max(1.0)) as usize).clamp(4, 30);
1016    root_level.theta_points = root_level.expansion_terms;
1017    root_level.phi_points = 2 * root_level.expansion_terms;
1018
1019    root_level.clusters.push(root);
1020    root_level.num_original = 1;
1021    root_level.max_radius = root_radius;
1022    root_level.avg_radius = root_radius;
1023    root_level.min_radius = root_radius;
1024
1025    levels.push(root_level);
1026
1027    // Subdivide recursively if needed
1028    if num_levels > 1 {
1029        subdivide_level(&mut levels, elements, 0, target_elements_per_leaf, physics);
1030    }
1031
1032    // Determine near/far interactions at each level
1033    for level in &mut levels {
1034        compute_near_far_lists(&mut level.clusters, physics.wave_number);
1035    }
1036
1037    levels
1038}
1039
1040/// Compute bounding box of all elements
1041fn compute_bounding_box(elements: &[Element]) -> ([f64; 3], [f64; 3]) {
1042    let mut min_coords = [f64::MAX, f64::MAX, f64::MAX];
1043    let mut max_coords = [f64::MIN, f64::MIN, f64::MIN];
1044
1045    for elem in elements {
1046        for d in 0..3 {
1047            min_coords[d] = min_coords[d].min(elem.center[d]);
1048            max_coords[d] = max_coords[d].max(elem.center[d]);
1049        }
1050    }
1051
1052    (min_coords, max_coords)
1053}
1054
1055/// Recursively subdivide clusters
1056fn subdivide_level(
1057    levels: &mut Vec<ClusterLevel>,
1058    elements: &[Element],
1059    parent_level: usize,
1060    target_elements_per_leaf: usize,
1061    physics: &PhysicsParams,
1062) {
1063    let parent_clusters = levels[parent_level].clusters.clone();
1064    let mut child_level = ClusterLevel::new(parent_clusters.len() * 8);
1065
1066    let mut max_radius = 0.0_f64;
1067    let mut min_radius = f64::MAX;
1068    let mut sum_radius = 0.0_f64;
1069
1070    for (parent_idx, parent) in parent_clusters.iter().enumerate() {
1071        if parent.element_indices.len() <= target_elements_per_leaf {
1072            // This cluster becomes a leaf - copy to child level
1073            let mut leaf = parent.clone();
1074            leaf.level = parent_level + 1;
1075            leaf.father = Some(parent_idx);
1076
1077            max_radius = max_radius.max(leaf.radius);
1078            min_radius = min_radius.min(leaf.radius);
1079            sum_radius += leaf.radius;
1080
1081            let child_idx = child_level.clusters.len();
1082            levels[parent_level].clusters[parent_idx]
1083                .sons
1084                .push(child_idx);
1085            child_level.clusters.push(leaf);
1086            continue;
1087        }
1088
1089        // Subdivide into 8 octants
1090        let half_size = parent.radius / 2.0;
1091        let offsets = [
1092            [-1.0, -1.0, -1.0],
1093            [-1.0, -1.0, 1.0],
1094            [-1.0, 1.0, -1.0],
1095            [-1.0, 1.0, 1.0],
1096            [1.0, -1.0, -1.0],
1097            [1.0, -1.0, 1.0],
1098            [1.0, 1.0, -1.0],
1099            [1.0, 1.0, 1.0],
1100        ];
1101
1102        for offset in &offsets {
1103            let child_center = [
1104                parent.center[0] + offset[0] * half_size * 0.5,
1105                parent.center[1] + offset[1] * half_size * 0.5,
1106                parent.center[2] + offset[2] * half_size * 0.5,
1107            ];
1108
1109            // Find elements in this octant
1110            let child_elements: Vec<usize> = parent
1111                .element_indices
1112                .iter()
1113                .filter(|&&idx| {
1114                    let elem = &elements[idx];
1115                    let dx = elem.center[0] - parent.center[0];
1116                    let dy = elem.center[1] - parent.center[1];
1117                    let dz = elem.center[2] - parent.center[2];
1118
1119                    (dx * offset[0] >= 0.0) && (dy * offset[1] >= 0.0) && (dz * offset[2] >= 0.0)
1120                })
1121                .copied()
1122                .collect();
1123
1124            if child_elements.is_empty() {
1125                continue;
1126            }
1127
1128            let mut child = Cluster::new(Array1::from_vec(child_center.to_vec()));
1129            child.element_indices = child_elements;
1130            child.radius = half_size;
1131            child.level = parent_level + 1;
1132            child.father = Some(parent_idx);
1133
1134            max_radius = max_radius.max(child.radius);
1135            min_radius = min_radius.min(child.radius);
1136            sum_radius += child.radius;
1137
1138            let child_idx = child_level.clusters.len();
1139            levels[parent_level].clusters[parent_idx]
1140                .sons
1141                .push(child_idx);
1142            child_level.clusters.push(child);
1143        }
1144    }
1145
1146    let num_clusters = child_level.clusters.len();
1147    child_level.num_original = num_clusters;
1148    child_level.max_radius = max_radius;
1149    child_level.min_radius = min_radius;
1150    child_level.avg_radius = if num_clusters > 0 {
1151        sum_radius / num_clusters as f64
1152    } else {
1153        0.0
1154    };
1155
1156    // Set expansion parameters
1157    let kr = physics.wave_number * child_level.avg_radius;
1158    child_level.expansion_terms = ((kr + 6.0 * kr.ln().max(1.0)) as usize).clamp(4, 30);
1159    child_level.theta_points = child_level.expansion_terms;
1160    child_level.phi_points = 2 * child_level.expansion_terms;
1161
1162    levels.push(child_level);
1163
1164    // Continue subdividing if needed
1165    let current_level = levels.len() - 1;
1166    let should_continue = levels[current_level]
1167        .clusters
1168        .iter()
1169        .any(|c| c.element_indices.len() > target_elements_per_leaf);
1170
1171    if should_continue && current_level < 7 {
1172        subdivide_level(
1173            levels,
1174            elements,
1175            current_level,
1176            target_elements_per_leaf,
1177            physics,
1178        );
1179    }
1180}
1181
1182/// Compute near/far cluster lists based on separation criterion
1183fn compute_near_far_lists(clusters: &mut [Cluster], wave_number: f64) {
1184    let n = clusters.len();
1185    let min_separation = 2.0; // Clusters farther than 2*radius are "far"
1186
1187    for i in 0..n {
1188        let center_i = clusters[i].center.clone();
1189        let radius_i = clusters[i].radius;
1190
1191        let mut near = Vec::new();
1192        let mut far = Vec::new();
1193
1194        for j in 0..n {
1195            if i == j {
1196                continue;
1197            }
1198
1199            let center_j = &clusters[j].center;
1200            let radius_j = clusters[j].radius;
1201
1202            let dist = ((center_i[0] - center_j[0]).powi(2)
1203                + (center_i[1] - center_j[1]).powi(2)
1204                + (center_i[2] - center_j[2]).powi(2))
1205            .sqrt();
1206
1207            let separation = dist / (radius_i + radius_j).max(1e-15);
1208
1209            // Also check if clusters are in the far-field acoustically
1210            let kr = wave_number * dist;
1211            let is_acoustic_far = kr > 2.0;
1212
1213            if separation > min_separation && is_acoustic_far {
1214                far.push(j);
1215            } else {
1216                near.push(j);
1217            }
1218        }
1219
1220        clusters[i].near_clusters = near;
1221        clusters[i].far_clusters = far;
1222    }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227    use super::*;
1228    use crate::core::types::{BoundaryCondition, ElementProperty, ElementType};
1229    use ndarray::array;
1230
1231    fn make_test_elements() -> (Vec<Element>, Array2<f64>) {
1232        let nodes = Array2::from_shape_vec(
1233            (4, 3),
1234            vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.5, 1.0, 0.0, 1.5, 1.0, 0.0],
1235        )
1236        .unwrap();
1237
1238        let elem0 = Element {
1239            connectivity: vec![0, 1, 2],
1240            element_type: ElementType::Tri3,
1241            property: ElementProperty::Surface,
1242            normal: array![0.0, 0.0, 1.0],
1243            node_normals: Array2::zeros((3, 3)),
1244            center: array![0.5, 1.0 / 3.0, 0.0],
1245            area: 0.5,
1246            boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(1.0, 0.0)]),
1247            group: 0,
1248            dof_addresses: vec![0],
1249        };
1250
1251        let elem1 = Element {
1252            connectivity: vec![1, 3, 2],
1253            element_type: ElementType::Tri3,
1254            property: ElementProperty::Surface,
1255            normal: array![0.0, 0.0, 1.0],
1256            node_normals: Array2::zeros((3, 3)),
1257            center: array![1.0, 2.0 / 3.0, 0.0],
1258            area: 0.5,
1259            boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)]),
1260            group: 0,
1261            dof_addresses: vec![1],
1262        };
1263
1264        (vec![elem0, elem1], nodes)
1265    }
1266
1267    #[test]
1268    fn test_estimate_num_levels() {
1269        assert_eq!(estimate_num_levels(10, 10, 1, 8), 1);
1270        // 100 elements, ~12 per octant after first split -> still need to subdivide
1271        assert_eq!(estimate_num_levels(100, 10, 1, 8), 3);
1272        // 1000 elements -> need multiple levels
1273        assert!(estimate_num_levels(1000, 10, 1, 8) >= 3);
1274    }
1275
1276    #[test]
1277    fn test_build_cluster_tree() {
1278        let (elements, _nodes) = make_test_elements();
1279        let physics = PhysicsParams::new(100.0, 343.0, 1.21, false);
1280
1281        let tree = build_cluster_tree(&elements, 10, &physics);
1282
1283        assert!(!tree.is_empty());
1284        assert!(!tree[0].clusters.is_empty());
1285        assert_eq!(tree[0].clusters[0].element_indices.len(), elements.len());
1286    }
1287
1288    #[test]
1289    fn test_build_mlfmm_system() {
1290        let (elements, nodes) = make_test_elements();
1291        let physics = PhysicsParams::new(100.0, 343.0, 1.21, false);
1292
1293        let cluster_tree = build_cluster_tree(&elements, 10, &physics);
1294        let system = build_mlfmm_system(&elements, &nodes, cluster_tree, &physics);
1295
1296        assert_eq!(system.num_dofs, 2);
1297        assert!(system.num_levels >= 1);
1298    }
1299
1300    #[test]
1301    fn test_mlfmm_matvec() {
1302        let (elements, nodes) = make_test_elements();
1303        let physics = PhysicsParams::new(100.0, 343.0, 1.21, false);
1304
1305        let cluster_tree = build_cluster_tree(&elements, 10, &physics);
1306        let system = build_mlfmm_system(&elements, &nodes, cluster_tree, &physics);
1307
1308        let x = Array1::from_vec(vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)]);
1309        let y = system.matvec(&x);
1310
1311        assert_eq!(y.len(), 2);
1312    }
1313}