1use ndarray::{Array1, Array2};
16use num_complex::Complex64;
17use rayon::prelude::*;
18
19use crate::core::greens::spherical::spherical_hankel_first_kind;
20use crate::core::integration::{regular_integration, singular_integration, unit_sphere_quadrature};
21use crate::core::types::{BoundaryCondition, Cluster, Element, PhysicsParams};
22
23pub struct SlfmmSystem {
25 pub near_matrix: Vec<NearFieldBlock>,
27 pub t_matrices: Vec<Array2<Complex64>>,
29 pub t_vector: Array1<Complex64>,
31 pub d_matrices: Vec<DMatrixEntry>,
33 pub s_matrices: Vec<Array2<Complex64>>,
35 pub rhs: Array1<Complex64>,
37 pub num_dofs: usize,
39 pub num_sphere_points: usize,
41 pub num_expansion_terms: usize,
43 pub num_clusters: usize,
45 pub cluster_dof_indices: Vec<Vec<usize>>,
47}
48
49#[derive(Debug, Clone)]
51pub struct NearFieldBlock {
52 pub source_cluster: usize,
54 pub field_cluster: usize,
56 pub coefficients: Array2<Complex64>,
58}
59
60#[derive(Debug, Clone)]
62pub struct DMatrixEntry {
63 pub source_cluster: usize,
65 pub field_cluster: usize,
67 pub coefficients: Array2<Complex64>,
69}
70
71impl SlfmmSystem {
72 pub fn new(
74 num_dofs: usize,
75 num_clusters: usize,
76 num_sphere_points: usize,
77 num_expansion_terms: usize,
78 ) -> Self {
79 Self {
80 near_matrix: Vec::new(),
81 t_matrices: Vec::with_capacity(num_clusters),
82 t_vector: Array1::zeros(num_sphere_points * num_clusters),
83 d_matrices: Vec::new(),
84 s_matrices: Vec::with_capacity(num_clusters),
85 rhs: Array1::zeros(num_dofs),
86 num_dofs,
87 num_sphere_points,
88 num_expansion_terms,
89 num_clusters,
90 cluster_dof_indices: Vec::with_capacity(num_clusters),
91 }
92 }
93
94 pub fn extract_near_field_matrix(&self) -> Array2<Complex64> {
99 let mut matrix = Array2::zeros((self.num_dofs, self.num_dofs));
100
101 for block in &self.near_matrix {
102 let src_dofs = &self.cluster_dof_indices[block.source_cluster];
103 let fld_dofs = &self.cluster_dof_indices[block.field_cluster];
104
105 for (local_i, &global_i) in src_dofs.iter().enumerate() {
107 for (local_j, &global_j) in fld_dofs.iter().enumerate() {
108 matrix[[global_i, global_j]] += block.coefficients[[local_i, local_j]];
109 }
110 }
111
112 if block.source_cluster != block.field_cluster {
114 for (local_i, &global_i) in src_dofs.iter().enumerate() {
115 for (local_j, &global_j) in fld_dofs.iter().enumerate() {
116 matrix[[global_j, global_i]] += block.coefficients[[local_i, local_j]];
117 }
118 }
119 }
120 }
121
122 matrix
123 }
124
125 pub fn matvec(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
139 let near_contributions: Vec<Vec<(usize, Complex64)>> = self
142 .near_matrix
143 .par_iter()
144 .flat_map(|block| {
145 let src_dofs = &self.cluster_dof_indices[block.source_cluster];
146 let fld_dofs = &self.cluster_dof_indices[block.field_cluster];
147
148 let mut contributions = Vec::new();
149
150 let x_local: Array1<Complex64> =
152 Array1::from_iter(fld_dofs.iter().map(|&i| x[i]));
153
154 let y_local = block.coefficients.dot(&x_local);
156
157 for (local_i, &global_i) in src_dofs.iter().enumerate() {
159 contributions.push((global_i, y_local[local_i]));
160 }
161
162 if block.source_cluster != block.field_cluster {
164 let x_src: Array1<Complex64> =
165 Array1::from_iter(src_dofs.iter().map(|&i| x[i]));
166 let y_fld = block.coefficients.t().dot(&x_src);
167 for (local_j, &global_j) in fld_dofs.iter().enumerate() {
168 contributions.push((global_j, y_fld[local_j]));
169 }
170 }
171
172 vec![contributions]
173 })
174 .collect();
175
176 let mut y = Array1::zeros(self.num_dofs);
178 for contributions in near_contributions {
179 for (idx, val) in contributions {
180 y[idx] += val;
181 }
182 }
183
184 let multipoles: Vec<Array1<Complex64>> = self
188 .t_matrices
189 .par_iter()
190 .enumerate()
191 .map(|(cluster_idx, t_mat)| {
192 let cluster_dofs = &self.cluster_dof_indices[cluster_idx];
193 if cluster_dofs.is_empty() || t_mat.is_empty() {
194 Array1::zeros(self.num_sphere_points)
195 } else {
196 let x_local: Array1<Complex64> =
197 Array1::from_iter(cluster_dofs.iter().map(|&i| x[i]));
198 t_mat.dot(&x_local)
199 }
200 })
201 .collect();
202
203 let d_contributions: Vec<(usize, Array1<Complex64>)> = self
206 .d_matrices
207 .par_iter()
208 .map(|d_entry| {
209 let src_mult = &multipoles[d_entry.source_cluster];
210 let translated = d_entry.coefficients.dot(src_mult);
211 (d_entry.field_cluster, translated)
212 })
213 .collect();
214
215 let mut locals: Vec<Array1<Complex64>> = (0..self.num_clusters)
217 .map(|_| Array1::zeros(self.num_sphere_points))
218 .collect();
219
220 for (field_cluster, translated) in d_contributions {
221 for i in 0..self.num_sphere_points {
222 locals[field_cluster][i] += translated[i];
223 }
224 }
225
226 let far_contributions: Vec<Vec<(usize, Complex64)>> = self
228 .s_matrices
229 .par_iter()
230 .enumerate()
231 .filter_map(|(cluster_idx, s_mat)| {
232 let cluster_dofs = &self.cluster_dof_indices[cluster_idx];
233 if cluster_dofs.is_empty() || s_mat.is_empty() {
234 return None;
235 }
236 let y_local = s_mat.dot(&locals[cluster_idx]);
237 let contributions: Vec<(usize, Complex64)> = cluster_dofs
238 .iter()
239 .enumerate()
240 .map(|(local_j, &global_j)| (global_j, y_local[local_j]))
241 .collect();
242 Some(contributions)
243 })
244 .collect();
245
246 for contributions in far_contributions {
248 for (idx, val) in contributions {
249 y[idx] += val;
250 }
251 }
252
253 y
254 }
255
256 pub fn matvec_transpose(&self, x: &Array1<Complex64>) -> Array1<Complex64> {
260 let near_contributions: Vec<Vec<(usize, Complex64)>> = self
262 .near_matrix
263 .par_iter()
264 .flat_map(|block| {
265 let src_dofs = &self.cluster_dof_indices[block.source_cluster];
266 let fld_dofs = &self.cluster_dof_indices[block.field_cluster];
267
268 let mut contributions = Vec::new();
269
270 let x_local: Array1<Complex64> =
272 Array1::from_iter(src_dofs.iter().map(|&i| x[i]));
273 let y_local = block.coefficients.t().dot(&x_local);
274 for (local_j, &global_j) in fld_dofs.iter().enumerate() {
275 contributions.push((global_j, y_local[local_j]));
276 }
277
278 if block.source_cluster != block.field_cluster {
280 let x_fld: Array1<Complex64> =
281 Array1::from_iter(fld_dofs.iter().map(|&i| x[i]));
282 let y_src = block.coefficients.dot(&x_fld);
283 for (local_i, &global_i) in src_dofs.iter().enumerate() {
284 contributions.push((global_i, y_src[local_i]));
285 }
286 }
287
288 vec![contributions]
289 })
290 .collect();
291
292 let mut y = Array1::zeros(self.num_dofs);
294 for contributions in near_contributions {
295 for (idx, val) in contributions {
296 y[idx] += val;
297 }
298 }
299
300 let locals: Vec<Array1<Complex64>> = self
304 .s_matrices
305 .par_iter()
306 .enumerate()
307 .map(|(cluster_idx, s_mat)| {
308 let cluster_dofs = &self.cluster_dof_indices[cluster_idx];
309 if cluster_dofs.is_empty() || s_mat.is_empty() {
310 Array1::zeros(self.num_sphere_points)
311 } else {
312 let x_local: Array1<Complex64> =
313 Array1::from_iter(cluster_dofs.iter().map(|&i| x[i]));
314 s_mat.t().dot(&x_local)
315 }
316 })
317 .collect();
318
319 let d_contributions: Vec<(usize, Array1<Complex64>)> = self
321 .d_matrices
322 .par_iter()
323 .map(|d_entry| {
324 let fld_local = &locals[d_entry.field_cluster];
325 let translated = d_entry.coefficients.t().dot(fld_local);
326 (d_entry.source_cluster, translated)
327 })
328 .collect();
329
330 let mut multipoles: Vec<Array1<Complex64>> = (0..self.num_clusters)
332 .map(|_| Array1::zeros(self.num_sphere_points))
333 .collect();
334
335 for (source_cluster, translated) in d_contributions {
336 for i in 0..self.num_sphere_points {
337 multipoles[source_cluster][i] += translated[i];
338 }
339 }
340
341 let far_contributions: Vec<Vec<(usize, Complex64)>> = self
343 .t_matrices
344 .par_iter()
345 .enumerate()
346 .filter_map(|(cluster_idx, t_mat)| {
347 let cluster_dofs = &self.cluster_dof_indices[cluster_idx];
348 if cluster_dofs.is_empty() || t_mat.is_empty() {
349 return None;
350 }
351 let y_local = t_mat.t().dot(&multipoles[cluster_idx]);
352 let contributions: Vec<(usize, Complex64)> = cluster_dofs
353 .iter()
354 .enumerate()
355 .map(|(local_i, &global_i)| (global_i, y_local[local_i]))
356 .collect();
357 Some(contributions)
358 })
359 .collect();
360
361 for contributions in far_contributions {
363 for (idx, val) in contributions {
364 y[idx] += val;
365 }
366 }
367
368 y
369 }
370}
371
372pub fn build_slfmm_system(
383 elements: &[Element],
384 nodes: &Array2<f64>,
385 clusters: &[Cluster],
386 physics: &PhysicsParams,
387 n_theta: usize,
388 n_phi: usize,
389 n_terms: usize,
390) -> SlfmmSystem {
391 let num_dofs = count_dofs(elements);
392 let num_clusters = clusters.len();
393 let num_sphere_points = n_theta * n_phi;
394
395 let mut system = SlfmmSystem::new(num_dofs, num_clusters, num_sphere_points, n_terms);
396
397 build_cluster_dof_mappings(&mut system, elements, clusters);
400
401 let (sphere_coords, sphere_weights) = unit_sphere_quadrature(n_theta, n_phi);
403
404 build_near_field(&mut system, elements, nodes, clusters, physics);
406
407 build_t_matrices(
409 &mut system,
410 elements,
411 clusters,
412 physics,
413 &sphere_coords,
414 &sphere_weights,
415 );
416
417 build_d_matrices(&mut system, clusters, physics, &sphere_coords, n_terms);
419
420 build_s_matrices(
422 &mut system,
423 elements,
424 clusters,
425 physics,
426 &sphere_coords,
427 &sphere_weights,
428 );
429
430 system
431}
432
433fn build_cluster_dof_mappings(
435 system: &mut SlfmmSystem,
436 elements: &[Element],
437 clusters: &[Cluster],
438) {
439 for cluster in clusters {
440 let mut dof_indices = Vec::new();
441 for &elem_idx in &cluster.element_indices {
442 let elem = &elements[elem_idx];
443 if elem.property.is_evaluation() {
444 continue;
445 }
446 dof_indices.extend(elem.dof_addresses.iter().copied());
448 }
449 system.cluster_dof_indices.push(dof_indices);
450 }
451}
452
453fn count_dofs(elements: &[Element]) -> usize {
455 elements
456 .iter()
457 .filter(|e| !e.property.is_evaluation())
458 .map(|e| e.dof_addresses.len())
459 .sum()
460}
461
462fn build_near_field(
464 system: &mut SlfmmSystem,
465 elements: &[Element],
466 nodes: &Array2<f64>,
467 clusters: &[Cluster],
468 physics: &PhysicsParams,
469) {
470 let gamma = Complex64::new(physics.gamma(), 0.0);
471 let tau = Complex64::new(physics.tau, 0.0);
472 let beta = physics.burton_miller_beta();
473
474 let mut cluster_pairs: Vec<(usize, usize, bool)> = Vec::new();
476
477 for (i, cluster_i) in clusters.iter().enumerate() {
478 cluster_pairs.push((i, i, true));
480
481 for &j in &cluster_i.near_clusters {
483 if j > i {
484 cluster_pairs.push((i, j, false));
485 }
486 }
487 }
488
489 let near_blocks: Vec<NearFieldBlock> = cluster_pairs
491 .par_iter()
492 .map(|&(i, j, is_self)| {
493 let cluster_i = &clusters[i];
494 let cluster_j = &clusters[j];
495
496 let mut block = compute_near_block(
497 elements,
498 nodes,
499 &cluster_i.element_indices,
500 &cluster_j.element_indices,
501 physics,
502 gamma,
503 tau,
504 beta,
505 is_self,
506 );
507
508 if is_self {
510 for local_idx in 0..cluster_i.element_indices.len() {
511 let elem_idx = cluster_i.element_indices[local_idx];
512 let elem = &elements[elem_idx];
513 if elem.property.is_evaluation() {
514 continue;
515 }
516 match &elem.boundary_condition {
517 BoundaryCondition::Velocity(_)
518 | BoundaryCondition::VelocityWithAdmittance { .. } => {
519 block[[local_idx, local_idx]] += gamma * 0.5;
520 }
521 BoundaryCondition::Pressure(_) => {
522 block[[local_idx, local_idx]] += beta * tau * 0.5;
523 }
524 _ => {}
525 }
526 }
527 }
528
529 NearFieldBlock {
530 source_cluster: i,
531 field_cluster: j,
532 coefficients: block,
533 }
534 })
535 .collect();
536
537 system.near_matrix = near_blocks;
538}
539
540fn compute_near_block(
542 elements: &[Element],
543 nodes: &Array2<f64>,
544 source_indices: &[usize],
545 field_indices: &[usize],
546 physics: &PhysicsParams,
547 gamma: Complex64,
548 tau: Complex64,
549 beta: Complex64,
550 is_self: bool,
551) -> Array2<Complex64> {
552 let n_source = source_indices.len();
553 let n_field = field_indices.len();
554 let mut block = Array2::zeros((n_source, n_field));
555
556 for (i, &src_idx) in source_indices.iter().enumerate() {
557 let source_elem = &elements[src_idx];
558 if source_elem.property.is_evaluation() {
559 continue;
560 }
561
562 for (j, &fld_idx) in field_indices.iter().enumerate() {
563 let field_elem = &elements[fld_idx];
564 if field_elem.property.is_evaluation() {
565 continue;
566 }
567
568 let element_coords = get_element_coords(field_elem, nodes);
569
570 let result = if is_self && src_idx == fld_idx {
571 singular_integration(
573 &source_elem.center,
574 &source_elem.normal,
575 &element_coords,
576 field_elem.element_type,
577 physics,
578 None,
579 0,
580 false,
581 )
582 } else {
583 regular_integration(
585 &source_elem.center,
586 &source_elem.normal,
587 &element_coords,
588 field_elem.element_type,
589 field_elem.area,
590 physics,
591 None,
592 0,
593 false,
594 )
595 };
596
597 let coeff =
599 result.dg_dn_integral * gamma * tau + result.d2g_dnxdny_integral * beta;
600 block[[i, j]] = coeff;
601 }
602 }
603
604 block
605}
606
607fn build_t_matrices(
609 system: &mut SlfmmSystem,
610 elements: &[Element],
611 clusters: &[Cluster],
612 physics: &PhysicsParams,
613 sphere_coords: &[[f64; 3]],
614 sphere_weights: &[f64],
615) {
616 let k = physics.wave_number;
617 let num_sphere_points = sphere_coords.len();
618
619 let t_matrices: Vec<Array2<Complex64>> = clusters
621 .par_iter()
622 .map(|cluster| {
623 let num_elem = cluster.element_indices.len();
624 let mut t_matrix = Array2::zeros((num_sphere_points, num_elem));
625
626 for (j, &elem_idx) in cluster.element_indices.iter().enumerate() {
627 let elem = &elements[elem_idx];
628 if elem.property.is_evaluation() {
629 continue;
630 }
631
632 let diff: [f64; 3] = [
634 elem.center[0] - cluster.center[0],
635 elem.center[1] - cluster.center[1],
636 elem.center[2] - cluster.center[2],
637 ];
638
639 for (p, coord) in sphere_coords.iter().enumerate() {
641 let s_dot_diff = coord[0] * diff[0] + coord[1] * diff[1] + coord[2] * diff[2];
642 let exp_factor =
643 Complex64::new((k * s_dot_diff).cos(), -(k * s_dot_diff).sin());
644 t_matrix[[p, j]] = exp_factor * sphere_weights[p];
645 }
646 }
647
648 t_matrix
649 })
650 .collect();
651
652 system.t_matrices = t_matrices;
653}
654
655fn build_d_matrices(
657 system: &mut SlfmmSystem,
658 clusters: &[Cluster],
659 physics: &PhysicsParams,
660 sphere_coords: &[[f64; 3]],
661 n_terms: usize,
662) {
663 let k = physics.wave_number;
664 let num_sphere_points = sphere_coords.len();
665
666 let mut far_pairs: Vec<(usize, usize)> = Vec::new();
668 for (i, cluster_i) in clusters.iter().enumerate() {
669 for &j in &cluster_i.far_clusters {
670 far_pairs.push((i, j));
671 }
672 }
673
674 let d_matrices: Vec<DMatrixEntry> = far_pairs
676 .par_iter()
677 .map(|&(i, j)| {
678 let cluster_i = &clusters[i];
679 let cluster_j = &clusters[j];
680
681 let diff = [
683 cluster_i.center[0] - cluster_j.center[0],
684 cluster_i.center[1] - cluster_j.center[1],
685 cluster_i.center[2] - cluster_j.center[2],
686 ];
687 let r = (diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2]).sqrt();
688 let kr = k * r;
689
690 let mut d_matrix = Array2::zeros((num_sphere_points, num_sphere_points));
692
693 let h_funcs = spherical_hankel_first_kind(n_terms.max(2), kr, 1.0);
694
695 for p in 0..num_sphere_points {
696 d_matrix[[p, p]] = h_funcs[0] * Complex64::new(0.0, k);
697 }
698
699 DMatrixEntry {
700 source_cluster: i,
701 field_cluster: j,
702 coefficients: d_matrix,
703 }
704 })
705 .collect();
706
707 system.d_matrices = d_matrices;
708}
709
710fn build_s_matrices(
712 system: &mut SlfmmSystem,
713 elements: &[Element],
714 clusters: &[Cluster],
715 physics: &PhysicsParams,
716 sphere_coords: &[[f64; 3]],
717 sphere_weights: &[f64],
718) {
719 let k = physics.wave_number;
720 let num_sphere_points = sphere_coords.len();
721
722 let s_matrices: Vec<Array2<Complex64>> = clusters
724 .par_iter()
725 .map(|cluster| {
726 let num_elem = cluster.element_indices.len();
727 let mut s_matrix = Array2::zeros((num_elem, num_sphere_points));
728
729 for (j, &elem_idx) in cluster.element_indices.iter().enumerate() {
730 let elem = &elements[elem_idx];
731 if elem.property.is_evaluation() {
732 continue;
733 }
734
735 let diff: [f64; 3] = [
737 elem.center[0] - cluster.center[0],
738 elem.center[1] - cluster.center[1],
739 elem.center[2] - cluster.center[2],
740 ];
741
742 for (p, coord) in sphere_coords.iter().enumerate() {
744 let s_dot_diff = coord[0] * diff[0] + coord[1] * diff[1] + coord[2] * diff[2];
745 let exp_factor =
746 Complex64::new((k * s_dot_diff).cos(), (k * s_dot_diff).sin());
747 s_matrix[[j, p]] = exp_factor * sphere_weights[p];
748 }
749 }
750
751 s_matrix
752 })
753 .collect();
754
755 system.s_matrices = s_matrices;
756}
757
758fn get_element_coords(element: &Element, nodes: &Array2<f64>) -> Array2<f64> {
760 let num_nodes = element.connectivity.len();
761 let mut coords = Array2::zeros((num_nodes, 3));
762
763 for (i, &node_idx) in element.connectivity.iter().enumerate() {
764 for j in 0..3 {
765 coords[[i, j]] = nodes[[node_idx, j]];
766 }
767 }
768
769 coords
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775 use crate::core::types::{BoundaryCondition, ElementProperty, ElementType};
776 use ndarray::array;
777
778 fn make_test_cluster() -> Cluster {
779 let mut cluster = Cluster::new(array![0.5, 0.5, 0.0]);
780 cluster.element_indices = vec![0, 1];
781 cluster.near_clusters = vec![];
782 cluster.far_clusters = vec![];
783 cluster
784 }
785
786 fn make_test_elements() -> (Vec<Element>, Array2<f64>) {
787 let nodes = Array2::from_shape_vec(
788 (4, 3),
789 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],
790 )
791 .unwrap();
792
793 let elem0 = Element {
794 connectivity: vec![0, 1, 2],
795 element_type: ElementType::Tri3,
796 property: ElementProperty::Surface,
797 normal: array![0.0, 0.0, 1.0],
798 node_normals: Array2::zeros((3, 3)),
799 center: array![0.5, 1.0 / 3.0, 0.0],
800 area: 0.5,
801 boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(1.0, 0.0)]),
802 group: 0,
803 dof_addresses: vec![0],
804 };
805
806 let elem1 = Element {
807 connectivity: vec![1, 3, 2],
808 element_type: ElementType::Tri3,
809 property: ElementProperty::Surface,
810 normal: array![0.0, 0.0, 1.0],
811 node_normals: Array2::zeros((3, 3)),
812 center: array![1.0, 2.0 / 3.0, 0.0],
813 area: 0.5,
814 boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)]),
815 group: 0,
816 dof_addresses: vec![1],
817 };
818
819 (vec![elem0, elem1], nodes)
820 }
821
822 #[test]
823 fn test_slfmm_system_creation() {
824 let system = SlfmmSystem::new(10, 2, 32, 5);
825 assert_eq!(system.num_dofs, 10);
826 assert_eq!(system.num_sphere_points, 32);
827 assert_eq!(system.num_expansion_terms, 5);
828 }
829
830 #[test]
831 fn test_build_slfmm_system() {
832 let (elements, nodes) = make_test_elements();
833 let cluster = make_test_cluster();
834 let clusters = vec![cluster];
835 let physics = PhysicsParams::new(100.0, 343.0, 1.21, false);
836
837 let system = build_slfmm_system(&elements, &nodes, &clusters, &physics, 4, 8, 5);
838
839 assert_eq!(system.num_dofs, 2);
840 assert_eq!(system.t_matrices.len(), 1);
841 assert_eq!(system.s_matrices.len(), 1);
842 }
843
844 #[test]
845 fn test_near_field_block() {
846 let (elements, nodes) = make_test_elements();
847 let physics = PhysicsParams::new(100.0, 343.0, 1.21, false);
848 let gamma = Complex64::new(physics.gamma(), 0.0);
849 let tau = Complex64::new(physics.tau, 0.0);
850 let beta = physics.burton_miller_beta();
851
852 let block = compute_near_block(
853 &elements,
854 &nodes,
855 &[0, 1],
856 &[0, 1],
857 &physics,
858 gamma,
859 tau,
860 beta,
861 true,
862 );
863
864 assert_eq!(block.shape(), &[2, 2]);
865 assert!(block[[0, 0]].norm() > 0.0);
867 assert!(block[[1, 1]].norm() > 0.0);
868 }
869}