bem/core/integration/
regular.rs

1//! Regular (non-singular) element integration
2//!
3//! Adaptive integration for elements when source and field elements are different.
4//! Direct port of NC_RegularIntegration from NC_EquationSystem.cpp.
5
6use ndarray::{Array1, Array2};
7use num_complex::Complex64;
8use std::f64::consts::PI;
9
10use crate::core::integration::gauss::{quad_quadrature, triangle_quadrature};
11use crate::core::integration::singular::generate_subelements;
12use crate::core::mesh::element::{cross_product, normalize};
13use crate::core::types::{ElementType, IntegrationResult, PhysicsParams};
14
15/// Perform regular integration over a field element
16///
17/// This implements NC_RegularIntegration from the C++ code.
18/// Uses adaptive subelement subdivision based on distance from source point.
19///
20/// # Arguments
21/// * `source_point` - Source point (collocation point)
22/// * `source_normal` - Unit normal at source point
23/// * `element_coords` - Node coordinates of the field element (num_nodes × 3)
24/// * `element_type` - Triangle or quad element
25/// * `element_area` - Area of the field element
26/// * `physics` - Physics parameters (wave number, etc.)
27/// * `bc_values` - Boundary condition values at element nodes (for RHS)
28/// * `bc_type` - Boundary condition type (0=velocity, 1=pressure)
29/// * `compute_rhs` - Whether to compute RHS contribution
30///
31/// # Returns
32/// IntegrationResult with G, H, H^T, E integrals and RHS contribution
33pub fn regular_integration(
34    source_point: &Array1<f64>,
35    source_normal: &Array1<f64>,
36    element_coords: &Array2<f64>,
37    element_type: ElementType,
38    element_area: f64,
39    physics: &PhysicsParams,
40    bc_values: Option<&[Complex64]>,
41    bc_type: i32,
42    compute_rhs: bool,
43) -> IntegrationResult {
44    let wavruim = physics.harmonic_factor * physics.wave_number;
45    let k2 = physics.wave_number * physics.wave_number;
46
47    let mut result = IntegrationResult::default();
48
49    // Generate adaptive subelements
50    let subelements = generate_subelements(source_point, element_coords, element_type, element_area);
51
52    // Local coordinates of element vertices (standard vertex mapping)
53    // Triangle: N0 at (0,0), N1 at (1,0), N2 at (0,1)
54    // Quad: bilinear on [-1,1]²
55    let (_csi_nodes, _eta_nodes): (Vec<f64>, Vec<f64>) = match element_type {
56        ElementType::Tri3 => (vec![0.0, 1.0, 0.0], vec![0.0, 0.0, 1.0]),
57        ElementType::Quad4 => (vec![1.0, -1.0, -1.0, 1.0], vec![1.0, 1.0, -1.0, -1.0]),
58    };
59
60    // Process each subelement
61    for subel in &subelements {
62        let xice = subel.xi_center;
63        let etce = subel.eta_center;
64        let fase = subel.factor;
65        let fase2 = fase * fase;
66        let iforie = (fase.abs() - 1.0).abs() < 1e-10; // Not subdivided
67
68        // Get quadrature points for this subelement
69        let quad_points = get_quadrature_points(element_type, subel.gauss_order);
70
71        for (csi_gau, eta_gau, wei_gau) in quad_points {
72            // Transform quadrature point to original element coordinates
73            let (xio, eto, weih2) = if iforie {
74                (csi_gau, eta_gau, wei_gau)
75            } else if let Some(tri_verts) = &subel.tri_vertices {
76                // For triangular subelements: use affine transformation with shape functions
77                // Reference triangle point (ξ, η) maps to:
78                // xio = v0.0 * (1-ξ-η) + v1.0 * ξ + v2.0 * η
79                // eto = v0.1 * (1-ξ-η) + v1.1 * ξ + v2.1 * η
80                let l0 = 1.0 - csi_gau - eta_gau; // barycentric coord for v0
81                let xio = tri_verts[0].0 * l0 + tri_verts[1].0 * csi_gau + tri_verts[2].0 * eta_gau;
82                let eto = tri_verts[0].1 * l0 + tri_verts[1].1 * csi_gau + tri_verts[2].1 * eta_gau;
83
84                // Jacobian of the affine map = 2 * area of subtriangle
85                // Area = 0.5 * |det([v1-v0, v2-v0])|
86                let dx1 = tri_verts[1].0 - tri_verts[0].0;
87                let dy1 = tri_verts[1].1 - tri_verts[0].1;
88                let dx2 = tri_verts[2].0 - tri_verts[0].0;
89                let dy2 = tri_verts[2].1 - tri_verts[0].1;
90                let det = (dx1 * dy2 - dx2 * dy1).abs();
91                // det = 2 * area of subtriangle in local coords
92                // The reference triangle has area 0.5, so Jacobian = det
93                let weih2 = wei_gau * det;
94
95                (xio, eto, weih2)
96            } else {
97                // For quad subelements: use center + scale transformation
98                (
99                    xice + csi_gau * fase,
100                    etce + eta_gau * fase,
101                    wei_gau * fase2,
102                )
103            };
104
105            // Compute shape functions, Jacobian and position at quadrature point
106            let (shape_fn, jacobian, el_norm, crd_poi) =
107                compute_parameters(element_coords, element_type, xio, eto);
108
109            let wga = weih2 * jacobian;
110
111            // Compute distance vector and kernels
112            let mut diff_fsp = Array1::zeros(3);
113            for i in 0..3 {
114                diff_fsp[i] = crd_poi[i] - source_point[i];
115            }
116
117            let (unit_r, dis_fsp) = normalize(&diff_fsp);
118
119            if dis_fsp < 1e-15 {
120                continue;
121            }
122
123            // G kernel: exp(ikr)/(4πr)
124            let re1 = wavruim * dis_fsp;
125            let re2 = wga / (4.0 * PI * dis_fsp);
126            let zg = Complex64::new(re1.cos() * re2, re1.sin() * re2);
127
128            // H kernel (double layer): ∂G/∂n_y
129            let z1 = Complex64::new(-1.0 / dis_fsp, wavruim);
130            let zhh_base = zg * z1;
131            let re1_h = unit_r.dot(&el_norm); // (y-x)·n_y / r
132            let zhh = zhh_base * re1_h;
133
134            // H^T kernel (adjoint double layer): ∂G/∂n_x
135            let re2_h = -unit_r.dot(source_normal); // -(y-x)·n_x / r
136            let zht = zhh_base * re2_h;
137
138            // E kernel (hypersingular): ∂²G/(∂n_x ∂n_y)
139            let rq = re1_h * re2_h;
140            let nx_dot_ny = source_normal.dot(&el_norm);
141            let dq = dis_fsp * dis_fsp;
142
143            let ze_factor = Complex64::new(
144                (3.0 / dq - k2) * rq + nx_dot_ny / dq,
145                -wavruim / dis_fsp * (3.0 * rq + nx_dot_ny),
146            );
147            let ze = zg * ze_factor;
148
149            // Accumulate integrals
150            result.g_integral += zg;
151            result.dg_dn_integral += zhh;
152            result.dg_dnx_integral += zht;
153            result.d2g_dnxdny_integral += ze;
154
155            // RHS contribution
156            if compute_rhs {
157                if let Some(bc) = bc_values {
158                    // Interpolate BC at quadrature point
159                    let mut zbgao = Complex64::new(0.0, 0.0);
160                    for (i, &n) in shape_fn.iter().enumerate() {
161                        if i < bc.len() {
162                            zbgao += bc[i] * n;
163                        }
164                    }
165
166                    let gamma = physics.gamma();
167                    let tau = physics.tau;
168                    let beta = physics.burton_miller_beta();
169
170                    if bc_type == 0 {
171                        // Velocity BC
172                        result.rhs_contribution += (zg * gamma * tau + zht * beta) * zbgao;
173                    } else if bc_type == 1 {
174                        // Pressure BC
175                        result.rhs_contribution -= (zhh * gamma * tau + ze * beta) * zbgao;
176                    }
177                }
178            }
179        }
180    }
181
182    result
183}
184
185/// Get quadrature points for element type and order
186fn get_quadrature_points(element_type: ElementType, order: usize) -> Vec<(f64, f64, f64)> {
187    match element_type {
188        ElementType::Tri3 => triangle_quadrature(order),
189        ElementType::Quad4 => quad_quadrature(order),
190    }
191}
192
193/// Compute shape functions, Jacobian, normal and position at local coordinates
194fn compute_parameters(
195    element_coords: &Array2<f64>,
196    element_type: ElementType,
197    s: f64,
198    t: f64,
199) -> (Vec<f64>, f64, Array1<f64>, Array1<f64>) {
200    let num_nodes = element_type.num_nodes();
201
202    let (shape_fn, shape_ds, shape_dt) = match element_type {
203        ElementType::Tri3 => {
204            // Triangle area coordinates: standard vertex mapping
205            // N0 = 1-s-t (vertex at (0,0))
206            // N1 = s (vertex at (1,0))
207            // N2 = t (vertex at (0,1))
208            let shape_fn = vec![1.0 - s - t, s, t];
209            let shape_ds = vec![-1.0, 1.0, 0.0];
210            let shape_dt = vec![-1.0, 0.0, 1.0];
211            (shape_fn, shape_ds, shape_dt)
212        }
213        ElementType::Quad4 => {
214            // Bilinear quad on [-1,1]²
215            let s1 = 0.25 * (s + 1.0);
216            let s2 = 0.25 * (s - 1.0);
217            let t1 = t + 1.0;
218            let t2 = t - 1.0;
219
220            let shape_fn = vec![s1 * t1, -s2 * t1, s2 * t2, -s1 * t2];
221            let shape_ds = vec![
222                0.25 * (t + 1.0),
223                -0.25 * (t + 1.0),
224                0.25 * (t - 1.0),
225                -0.25 * (t - 1.0),
226            ];
227            let shape_dt = vec![
228                0.25 * (s + 1.0),
229                0.25 * (1.0 - s),
230                0.25 * (s - 1.0),
231                -0.25 * (s + 1.0),
232            ];
233            (shape_fn, shape_ds, shape_dt)
234        }
235    };
236
237    // Compute global position and tangent vectors
238    let mut crd_poi = Array1::zeros(3);
239    let mut dx_ds = Array1::zeros(3);
240    let mut dx_dt = Array1::zeros(3);
241
242    for i in 0..num_nodes {
243        for j in 0..3 {
244            crd_poi[j] += shape_fn[i] * element_coords[[i, j]];
245            dx_ds[j] += shape_ds[i] * element_coords[[i, j]];
246            dx_dt[j] += shape_dt[i] * element_coords[[i, j]];
247        }
248    }
249
250    // Normal = dx_ds × dx_dt
251    let normal = cross_product(&dx_ds, &dx_dt);
252    let jacobian = normal.dot(&normal).sqrt();
253
254    let el_norm = if jacobian > 1e-15 {
255        normal / jacobian
256    } else {
257        Array1::zeros(3)
258    };
259
260    (shape_fn, jacobian, el_norm, crd_poi)
261}
262
263/// Threshold for quasi-singular integration (distance/element_size ratio)
264/// NumCalc uses approximately 3.0
265pub const QUASI_SINGULAR_THRESHOLD: f64 = 3.0;
266
267/// High-accuracy threshold requiring 13-point quadrature
268pub const HIGH_ACCURACY_THRESHOLD: f64 = 2.0;
269
270/// Determine the optimal quadrature order based on distance to element
271///
272/// Returns the recommended Gauss order for triangle quadrature.
273/// - ratio < 2.0: 13-point (very near-singular)
274/// - ratio < 3.0: 7-point (quasi-singular)
275/// - ratio >= 3.0: 4-point (regular)
276pub fn optimal_quadrature_order(distance: f64, element_size: f64) -> usize {
277    let ratio = distance / element_size;
278    if ratio < HIGH_ACCURACY_THRESHOLD {
279        4 // Maps to 13-point in triangle_quadrature
280    } else if ratio < QUASI_SINGULAR_THRESHOLD {
281        3 // Maps to 7-point in triangle_quadrature
282    } else {
283        2 // Maps to 4-point in triangle_quadrature
284    }
285}
286
287/// Perform quasi-singular integration with adaptive quadrature order
288///
289/// This function determines the appropriate quadrature order based on the
290/// distance between source point and element, using higher-order rules
291/// for near-singular cases.
292///
293/// # Arguments
294/// * `source_point` - Source point (collocation point)
295/// * `source_normal` - Unit normal at source point
296/// * `element_coords` - Node coordinates of the field element (num_nodes × 3)
297/// * `element_type` - Triangle or quad element
298/// * `element_center` - Center of the element
299/// * `element_area` - Area of the field element
300/// * `physics` - Physics parameters (wave number, etc.)
301///
302/// # Returns
303/// IntegrationResult with G, H, H^T, E integrals
304pub fn quasi_singular_integration(
305    source_point: &Array1<f64>,
306    source_normal: &Array1<f64>,
307    element_coords: &Array2<f64>,
308    element_type: ElementType,
309    element_center: &Array1<f64>,
310    element_area: f64,
311    physics: &PhysicsParams,
312) -> IntegrationResult {
313    // Compute distance from source to element center
314    let mut dist_sq = 0.0;
315    for i in 0..3 {
316        let d = element_center[i] - source_point[i];
317        dist_sq += d * d;
318    }
319    let distance = dist_sq.sqrt();
320    let element_size = element_area.sqrt();
321    let ratio = distance / element_size;
322
323    // Choose integration strategy based on distance ratio
324    if ratio < HIGH_ACCURACY_THRESHOLD {
325        // Very close: use adaptive subdivision with high-order quadrature
326        regular_integration(
327            source_point,
328            source_normal,
329            element_coords,
330            element_type,
331            element_area,
332            physics,
333            None,
334            0,
335            false,
336        )
337    } else if ratio < QUASI_SINGULAR_THRESHOLD {
338        // Quasi-singular: use high-order quadrature without subdivision
339        let order = optimal_quadrature_order(distance, element_size);
340        regular_integration_fixed_order(
341            source_point,
342            source_normal,
343            element_coords,
344            element_type,
345            physics,
346            order,
347        )
348    } else {
349        // Regular far-field: use standard 4-point quadrature
350        regular_integration_fixed_order(
351            source_point,
352            source_normal,
353            element_coords,
354            element_type,
355            physics,
356            2, // 4-point quadrature
357        )
358    }
359}
360
361/// Compute element integration with fixed quadrature order (no adaptation)
362///
363/// This is a simpler version for far-field elements where adaptation is not needed.
364pub fn regular_integration_fixed_order(
365    source_point: &Array1<f64>,
366    source_normal: &Array1<f64>,
367    element_coords: &Array2<f64>,
368    element_type: ElementType,
369    physics: &PhysicsParams,
370    gauss_order: usize,
371) -> IntegrationResult {
372    let wavruim = physics.harmonic_factor * physics.wave_number;
373    let k2 = physics.wave_number * physics.wave_number;
374
375    let mut result = IntegrationResult::default();
376
377    // Get quadrature points
378    let quad_points = get_quadrature_points(element_type, gauss_order);
379
380    for (xi, eta, weight) in quad_points {
381        // Compute shape functions, Jacobian and position
382        let (_, jacobian, el_norm, crd_poi) =
383            compute_parameters(element_coords, element_type, xi, eta);
384
385        let wga = weight * jacobian;
386
387        // Distance vector
388        let mut diff_fsp = Array1::zeros(3);
389        for i in 0..3 {
390            diff_fsp[i] = crd_poi[i] - source_point[i];
391        }
392
393        let (unit_r, dis_fsp) = normalize(&diff_fsp);
394
395        if dis_fsp < 1e-15 {
396            continue;
397        }
398
399        // G kernel
400        let kr = wavruim * dis_fsp;
401        let g_scaled = wga / (4.0 * PI * dis_fsp);
402        let zg = Complex64::new(kr.cos() * g_scaled, kr.sin() * g_scaled);
403
404        // H and H^T kernels
405        let z1 = Complex64::new(-1.0 / dis_fsp, wavruim);
406        let zhh_base = zg * z1;
407        let r_dot_ny = unit_r.dot(&el_norm);
408        let r_dot_nx = -unit_r.dot(source_normal);
409        let zhh = zhh_base * r_dot_ny;
410        let zht = zhh_base * r_dot_nx;
411
412        // E kernel
413        let rq = r_dot_ny * r_dot_nx;
414        let nx_dot_ny = source_normal.dot(&el_norm);
415        let dq = dis_fsp * dis_fsp;
416
417        let ze_factor = Complex64::new(
418            (3.0 / dq - k2) * rq + nx_dot_ny / dq,
419            -wavruim / dis_fsp * (3.0 * rq + nx_dot_ny),
420        );
421        let ze = zg * ze_factor;
422
423        // Accumulate
424        result.g_integral += zg;
425        result.dg_dn_integral += zhh;
426        result.dg_dnx_integral += zht;
427        result.d2g_dnxdny_integral += ze;
428    }
429
430    result
431}
432
433/// Compute G kernel only (single layer potential) for efficiency
434///
435/// Used when only G is needed (e.g., for FMM far-field approximation).
436pub fn integrate_g_only(
437    source_point: &Array1<f64>,
438    element_coords: &Array2<f64>,
439    element_type: ElementType,
440    physics: &PhysicsParams,
441    gauss_order: usize,
442) -> Complex64 {
443    let wavruim = physics.harmonic_factor * physics.wave_number;
444    let mut result = Complex64::new(0.0, 0.0);
445
446    let quad_points = get_quadrature_points(element_type, gauss_order);
447
448    for (xi, eta, weight) in quad_points {
449        let (_, jacobian, _, crd_poi) =
450            compute_parameters(element_coords, element_type, xi, eta);
451
452        let wga = weight * jacobian;
453
454        let mut diff = Array1::zeros(3);
455        for i in 0..3 {
456            diff[i] = crd_poi[i] - source_point[i];
457        }
458        let r = diff.dot(&diff).sqrt();
459
460        if r < 1e-15 {
461            continue;
462        }
463
464        let kr = wavruim * r;
465        let g_scaled = wga / (4.0 * PI * r);
466        result += Complex64::new(kr.cos() * g_scaled, kr.sin() * g_scaled);
467    }
468
469    result
470}
471
472/// Compute H kernel only (double layer potential)
473///
474/// Used when only the double layer is needed.
475pub fn integrate_h_only(
476    source_point: &Array1<f64>,
477    element_coords: &Array2<f64>,
478    element_type: ElementType,
479    physics: &PhysicsParams,
480    gauss_order: usize,
481) -> Complex64 {
482    let wavruim = physics.harmonic_factor * physics.wave_number;
483    let mut result = Complex64::new(0.0, 0.0);
484
485    let quad_points = get_quadrature_points(element_type, gauss_order);
486
487    for (xi, eta, weight) in quad_points {
488        let (_, jacobian, el_norm, crd_poi) =
489            compute_parameters(element_coords, element_type, xi, eta);
490
491        let wga = weight * jacobian;
492
493        let mut diff = Array1::zeros(3);
494        for i in 0..3 {
495            diff[i] = crd_poi[i] - source_point[i];
496        }
497        let r = diff.dot(&diff).sqrt();
498
499        if r < 1e-15 {
500            continue;
501        }
502
503        let kr = wavruim * r;
504        let g_scaled = wga / (4.0 * PI * r);
505        let zg = Complex64::new(kr.cos() * g_scaled, kr.sin() * g_scaled);
506
507        let z1 = Complex64::new(-1.0 / r, wavruim);
508        let r_dot_n: f64 = diff.iter().zip(el_norm.iter()).map(|(d, n)| d * n).sum::<f64>() / r;
509
510        result += zg * z1 * r_dot_n;
511    }
512
513    result
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use ndarray::{array, Array2};
520
521    fn make_test_triangle() -> Array2<f64> {
522        Array2::from_shape_vec((3, 3), vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]).unwrap()
523    }
524
525    fn make_test_quad() -> Array2<f64> {
526        Array2::from_shape_vec(
527            (4, 3),
528            vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0],
529        )
530        .unwrap()
531    }
532
533    #[test]
534    fn test_regular_integration_far_field() {
535        let coords = make_test_triangle();
536        let source = array![10.0, 0.0, 0.0]; // Far from element
537        let normal = array![0.0, 0.0, 1.0];
538
539        let physics = PhysicsParams::new(1000.0, 343.0, 1.21, false);
540
541        let result = regular_integration(
542            &source,
543            &normal,
544            &coords,
545            ElementType::Tri3,
546            0.5,
547            &physics,
548            None,
549            0,
550            false,
551        );
552
553        // G integral should be finite and small (far field)
554        assert!(result.g_integral.norm().is_finite());
555        assert!(result.g_integral.norm() < 0.1);
556    }
557
558    #[test]
559    fn test_regular_integration_fixed_order() {
560        let coords = make_test_triangle();
561        let source = array![5.0, 5.0, 0.0];
562        let normal = array![0.0, 0.0, 1.0];
563
564        let physics = PhysicsParams::new(1000.0, 343.0, 1.21, false);
565
566        let result = regular_integration_fixed_order(
567            &source,
568            &normal,
569            &coords,
570            ElementType::Tri3,
571            &physics,
572            4,
573        );
574
575        assert!(result.g_integral.norm().is_finite());
576    }
577
578    #[test]
579    fn test_integrate_g_only() {
580        let coords = make_test_triangle();
581        let source = array![2.0, 2.0, 1.0];
582
583        let physics = PhysicsParams::new(1000.0, 343.0, 1.21, false);
584
585        let g = integrate_g_only(&source, &coords, ElementType::Tri3, &physics, 4);
586
587        assert!(g.norm().is_finite());
588        assert!(g.norm() > 0.0);
589    }
590
591    #[test]
592    fn test_quad_element_integration() {
593        let coords = make_test_quad();
594        let source = array![5.0, 0.5, 0.0];
595        let normal = array![0.0, 0.0, 1.0];
596
597        let physics = PhysicsParams::new(1000.0, 343.0, 1.21, false);
598
599        let result = regular_integration_fixed_order(
600            &source,
601            &normal,
602            &coords,
603            ElementType::Quad4,
604            &physics,
605            4,
606        );
607
608        assert!(result.g_integral.norm().is_finite());
609    }
610
611    #[test]
612    fn test_integration_symmetry() {
613        // For a symmetric setup, H^T at point A should equal H at symmetric point
614        let coords = make_test_triangle();
615        let source1 = array![0.5, 0.5, 1.0];
616        let normal1 = array![0.0, 0.0, 1.0];
617        let source2 = array![0.5, 0.5, -1.0];
618        let normal2 = array![0.0, 0.0, -1.0];
619
620        let physics = PhysicsParams::new(1000.0, 343.0, 1.21, false);
621
622        let result1 = regular_integration_fixed_order(
623            &source1,
624            &normal1,
625            &coords,
626            ElementType::Tri3,
627            &physics,
628            4,
629        );
630
631        let result2 = regular_integration_fixed_order(
632            &source2,
633            &normal2,
634            &coords,
635            ElementType::Tri3,
636            &physics,
637            4,
638        );
639
640        // G should be equal (symmetric sources)
641        assert!((result1.g_integral.norm() - result2.g_integral.norm()).abs() < 1e-10);
642    }
643
644    #[test]
645    fn test_compute_parameters_triangle() {
646        let coords = make_test_triangle();
647
648        let (shape, jac, normal, pos) = compute_parameters(&coords, ElementType::Tri3, 0.5, 0.25);
649
650        // Shape functions should sum to 1
651        let sum: f64 = shape.iter().sum();
652        assert!((sum - 1.0).abs() < 1e-10);
653
654        // Jacobian should be 2 * area = 1
655        assert!((jac - 1.0).abs() < 1e-10);
656
657        // Normal should be (0, 0, 1)
658        assert!(normal[2].abs() > 0.99);
659
660        // Position check
661        assert!((pos[0] - 0.5).abs() < 1e-10);
662        assert!((pos[1] - 0.25).abs() < 1e-10);
663    }
664
665    #[test]
666    fn test_compute_parameters_quad() {
667        let coords = make_test_quad();
668
669        let (shape, jac, normal, _pos) = compute_parameters(&coords, ElementType::Quad4, 0.0, 0.0);
670
671        // Shape functions should sum to 1
672        let sum: f64 = shape.iter().sum();
673        assert!((sum - 1.0).abs() < 1e-10);
674
675        // Normal should be (0, 0, 1)
676        assert!(normal[2].abs() > 0.99);
677    }
678}