Skip to main content

math_audio_bem/core/integration/
singular.rs

1//! Singular integration handling
2//!
3//! Adaptive subdivision for nearly-singular and singular element integrals.
4//! Direct port of NC_SingularIntegration from NC_EquationSystem.cpp.
5
6use ndarray::{Array1, Array2, array};
7use num_complex::Complex64;
8use std::f64::consts::PI;
9
10use crate::core::integration::gauss::gauss_legendre;
11use crate::core::mesh::element::{cross_product, normalize};
12use crate::core::types::{ElementType, IntegrationResult, PhysicsParams};
13
14/// Maximum number of subelements for adaptive integration
15pub const MAX_SUBELEMENTS: usize = 110;
16
17/// Quadrature parameters for singular integration
18///
19/// Controls the accuracy of numerical integration. Higher values give
20/// better accuracy at higher computational cost.
21#[derive(Debug, Clone, Copy)]
22pub struct QuadratureParams {
23    /// Number of Gauss points for 1D edge integration (default: 3)
24    pub edge_gauss_order: usize,
25    /// Number of Gauss points per dimension for 2D subelement integration (default: 4)
26    pub subelement_gauss_order: usize,
27    /// Number of edge sections for hypersingular integration (default: 4)
28    pub edge_sections: usize,
29    /// Number of subtriangles per edge section (default: 2)
30    pub subtriangles_per_section: usize,
31}
32
33impl Default for QuadratureParams {
34    fn default() -> Self {
35        Self {
36            edge_gauss_order: 3,
37            subelement_gauss_order: 4,
38            edge_sections: 4,
39            subtriangles_per_section: 2,
40        }
41    }
42}
43
44impl QuadratureParams {
45    /// Create quadrature parameters optimized for the given ka (wave number × element size)
46    ///
47    /// Higher ka values require more integration points for accuracy.
48    pub fn for_ka(ka: f64) -> Self {
49        if ka < 0.3 {
50            // Very low frequency: coarse quadrature is fine
51            Self {
52                edge_gauss_order: 3,
53                subelement_gauss_order: 4,
54                edge_sections: 4,
55                subtriangles_per_section: 2,
56            }
57        } else if ka < 1.0 {
58            // Low-medium frequency: increase quadrature
59            Self {
60                edge_gauss_order: 4,
61                subelement_gauss_order: 5,
62                edge_sections: 6,
63                subtriangles_per_section: 2,
64            }
65        } else if ka < 2.0 {
66            // Medium frequency: further increase
67            Self {
68                edge_gauss_order: 5,
69                subelement_gauss_order: 6,
70                edge_sections: 8,
71                subtriangles_per_section: 3,
72            }
73        } else {
74            // High frequency: maximum quadrature
75            Self {
76                edge_gauss_order: 6,
77                subelement_gauss_order: 7,
78                edge_sections: 10,
79                subtriangles_per_section: 4,
80            }
81        }
82    }
83
84    /// High-accuracy quadrature for validation/debugging
85    pub fn high_accuracy() -> Self {
86        Self {
87            edge_gauss_order: 8,
88            subelement_gauss_order: 8,
89            edge_sections: 12,
90            subtriangles_per_section: 4,
91        }
92    }
93}
94
95/// Local coordinates for 6-point triangle subdivision (vertices + midpoints)
96/// Standard vertex mapping: v0 at (0,0), v1 at (1,0), v2 at (0,1)
97/// Midpoints: m3 = mid(v0,v1) at (0.5,0), m4 = mid(v1,v2) at (0.5,0.5), m5 = mid(v2,v0) at (0,0.5)
98const CSI6: [f64; 6] = [0.0, 1.0, 0.0, 0.5, 0.5, 0.0];
99const ETA6: [f64; 6] = [0.0, 0.0, 1.0, 0.0, 0.5, 0.5];
100
101/// Local coordinates for 8-point quad subdivision (vertices + midpoints)
102const CSI8: [f64; 8] = [1.0, -1.0, -1.0, 1.0, 0.0, -1.0, 0.0, 1.0];
103const ETA8: [f64; 8] = [1.0, 1.0, -1.0, -1.0, 1.0, 0.0, -1.0, 0.0];
104
105/// Perform singular integration for self-element
106///
107/// This implements NC_SingularIntegration from the C++ code.
108/// Uses edge-based integration for the hypersingular kernel and
109/// subelement triangulation for accurate integration near singularity.
110///
111/// # Arguments
112/// * `source_point` - Source point (collocation point)
113/// * `source_normal` - Unit normal at source point
114/// * `element_coords` - Node coordinates of the field element (num_nodes × 3)
115/// * `element_type` - Triangle or quad element
116/// * `physics` - Physics parameters (wave number, etc.)
117/// * `bc_values` - Boundary condition values at element nodes (for RHS)
118/// * `bc_type` - Boundary condition type (0=velocity, 1=pressure)
119/// * `compute_rhs` - Whether to compute RHS contribution
120///
121/// # Returns
122/// IntegrationResult with G, H, H^T, E integrals and RHS contribution
123pub fn singular_integration(
124    source_point: &Array1<f64>,
125    source_normal: &Array1<f64>,
126    element_coords: &Array2<f64>,
127    element_type: ElementType,
128    physics: &PhysicsParams,
129    bc_values: Option<&[Complex64]>,
130    bc_type: i32,
131    compute_rhs: bool,
132) -> IntegrationResult {
133    // Estimate element size for frequency-dependent quadrature
134    let element_size = estimate_element_size(element_coords, element_type);
135    let ka = physics.wave_number * element_size;
136    let quadrature = QuadratureParams::for_ka(ka);
137
138    singular_integration_with_params(
139        source_point,
140        source_normal,
141        element_coords,
142        element_type,
143        physics,
144        bc_values,
145        bc_type,
146        compute_rhs,
147        &quadrature,
148    )
149}
150
151/// Perform singular integration with explicit quadrature parameters
152///
153/// This version allows fine-grained control over integration accuracy.
154pub fn singular_integration_with_params(
155    source_point: &Array1<f64>,
156    source_normal: &Array1<f64>,
157    element_coords: &Array2<f64>,
158    element_type: ElementType,
159    physics: &PhysicsParams,
160    bc_values: Option<&[Complex64]>,
161    bc_type: i32,
162    compute_rhs: bool,
163    quadrature: &QuadratureParams,
164) -> IntegrationResult {
165    let num_nodes = element_type.num_nodes();
166    let wavruim = physics.harmonic_factor * physics.wave_number;
167    let k2 = physics.wave_number * physics.wave_number;
168
169    let mut result = IntegrationResult::default();
170
171    // Gauss-Legendre for edge integration
172    let ngpo1 = quadrature.edge_gauss_order;
173    let (coord_gau, weit_gau) = gauss_legendre(ngpo1);
174    let nsec1 = quadrature.edge_sections;
175    let nsec2 = quadrature.subtriangles_per_section;
176
177    // Loop over edges of the element
178    for ieg in 0..num_nodes {
179        let ig1 = (ieg + 1) % num_nodes;
180        let ig2 = ieg + num_nodes;
181
182        // Compute edge length and direction
183        let mut diff_poi = Array1::zeros(3);
184        let mut leneg = 0.0;
185        for i in 0..3 {
186            diff_poi[i] = element_coords[[ig1, i]] - element_coords[[ieg, i]];
187            leneg += diff_poi[i] * diff_poi[i];
188        }
189        leneg = leneg.sqrt();
190
191        // Unit vector along edge
192        let diff_poo = &diff_poi / leneg;
193        let leneg_scaled = leneg / (2.0 * nsec1 as f64);
194
195        // Edge integration for hypersingular kernel (E integral)
196        let mut zre = Complex64::new(0.0, 0.0);
197
198        // Each edge is divided into nsec1 sections
199        let delsec = 2.0 / nsec1 as f64;
200        let mut secmid = -1.0 - delsec / 2.0;
201
202        for _isec in 0..nsec1 {
203            secmid += delsec;
204
205            // Loop over Gauss points
206            for ig in 0..ngpo1 {
207                let sga = secmid + coord_gau[ig] / nsec1 as f64;
208                let wga = weit_gau[ig] * leneg_scaled;
209
210                // Gauss point coordinates
211                let mut crd_gp = Array1::zeros(3);
212                let mut diff_fsp = Array1::zeros(3);
213                for i in 0..3 {
214                    crd_gp[i] = element_coords[[ieg, i]] + diff_poi[i] * (sga + 1.0) / 2.0;
215                    diff_fsp[i] = crd_gp[i] - source_point[i];
216                }
217
218                // Normalize and get distance
219                let (unit_r, dis_fsp) = normalize(&diff_fsp);
220
221                if dis_fsp < 1e-15 {
222                    continue;
223                }
224
225                // Green's function and gradient factor
226                let re1 = wavruim * dis_fsp;
227                let re2 = 4.0 * PI * dis_fsp;
228                let zg = Complex64::new(re1.cos() / re2, re1.sin() / re2);
229
230                // Factor (ik - 1/r)
231                let z1 = Complex64::new(-1.0 / dis_fsp, wavruim);
232                let zg_factor = zg * z1;
233
234                // Compute ∇G × edge_direction
235                let mut zdg_dy = Array1::<Complex64>::zeros(3);
236                for i in 0..3 {
237                    zdg_dy[i] = zg_factor * unit_r[i];
238                }
239
240                // Cross product: (∇G) × (edge direction)
241                let zwk = array![
242                    zdg_dy[1] * diff_poo[2] - zdg_dy[2] * diff_poo[1],
243                    zdg_dy[2] * diff_poo[0] - zdg_dy[0] * diff_poo[2],
244                    zdg_dy[0] * diff_poo[1] - zdg_dy[1] * diff_poo[0]
245                ];
246
247                // Dot with source normal
248                zre += (zwk[0] * source_normal[0]
249                    + zwk[1] * source_normal[1]
250                    + zwk[2] * source_normal[2])
251                    * wga;
252            }
253        }
254        result.d2g_dnxdny_integral += zre;
255
256        // Subelement integration for G, H, H^T kernels
257        for isec in 0..nsec2 {
258            // Compute subtriangle vertices in local coordinates
259            let (ssub, tsub, aresub) = match element_type {
260                ElementType::Tri3 => {
261                    let aresub = 1.0 / 24.0 / nsec2 as f64;
262                    let mut ssub = [0.0; 3];
263                    let mut tsub = [0.0; 3];
264
265                    ssub[0] = 1.0 / 3.0;
266                    tsub[0] = 1.0 / 3.0;
267
268                    if isec == 0 {
269                        ssub[1] = CSI6[ieg];
270                        ssub[2] = CSI6[ig2];
271                        tsub[1] = ETA6[ieg];
272                        tsub[2] = ETA6[ig2];
273                    } else {
274                        ssub[1] = CSI6[ig2];
275                        ssub[2] = CSI6[ig1];
276                        tsub[1] = ETA6[ig2];
277                        tsub[2] = ETA6[ig1];
278                    }
279                    (ssub, tsub, aresub)
280                }
281                ElementType::Quad4 => {
282                    let aresub = 0.25 / nsec2 as f64;
283                    let mut ssub = [0.0; 3];
284                    let mut tsub = [0.0; 3];
285
286                    ssub[0] = 0.0;
287                    tsub[0] = 0.0;
288
289                    if isec == 0 {
290                        ssub[1] = CSI8[ieg];
291                        ssub[2] = CSI8[ig2];
292                        tsub[1] = ETA8[ieg];
293                        tsub[2] = ETA8[ig2];
294                    } else {
295                        ssub[1] = CSI8[ig2];
296                        ssub[2] = CSI8[ig1];
297                        tsub[1] = ETA8[ig2];
298                        tsub[2] = ETA8[ig1];
299                    }
300                    (ssub, tsub, aresub)
301                }
302            };
303
304            // Get quadrature points for subtriangle integration
305            // The mapping uses a tensor-product domain [-1,1]² with Duffy-type transformation
306            let ngausin = quadrature.subelement_gauss_order;
307            let quad_points = gauss_legendre(ngausin);
308            let (gau_coords, gau_weights) = quad_points;
309
310            for (i, &sga) in gau_coords.iter().enumerate() {
311                for (j, &tga) in gau_coords.iter().enumerate() {
312                    let wei_gau = gau_weights[i] * gau_weights[j];
313
314                    // Map from subtriangle to original element coordinates
315                    let sgg = 0.5 * (1.0 - sga) * ssub[0]
316                        + 0.25 * (1.0 + sga) * ((1.0 - tga) * ssub[1] + (1.0 + tga) * ssub[2]);
317                    let tgg = 0.5 * (1.0 - sga) * tsub[0]
318                        + 0.25 * (1.0 + sga) * ((1.0 - tga) * tsub[1] + (1.0 + tga) * tsub[2]);
319
320                    // Compute shape functions and Jacobian
321                    let (shape_fn, _shape_ds, _shape_dt, jacobian, el_norm, crd_poi) =
322                        compute_shape_and_jacobian(element_coords, element_type, sgg, tgg);
323
324                    let wga = wei_gau * (1.0 + sga) * aresub * jacobian;
325
326                    // Compute kernel functions
327                    let mut diff_fsp = Array1::zeros(3);
328                    for i in 0..3 {
329                        diff_fsp[i] = crd_poi[i] - source_point[i];
330                    }
331
332                    let (unit_r, dis_fsp) = normalize(&diff_fsp);
333
334                    if dis_fsp < 1e-15 {
335                        continue;
336                    }
337
338                    // G kernel
339                    let re1 = wavruim * dis_fsp;
340                    let re2 = wga / (4.0 * PI * dis_fsp);
341                    let zg = Complex64::new(re1.cos() * re2, re1.sin() * re2);
342
343                    // H and H^T kernels
344                    let z1 = Complex64::new(-1.0 / dis_fsp, wavruim);
345                    let zhh_base = zg * z1;
346                    let re1_h = unit_r.dot(&el_norm); // (y-x)·n_y / r
347                    let re2_h = -unit_r.dot(source_normal); // -(y-x)·n_x / r
348                    let zhh = zhh_base * re1_h;
349                    let zht = zhh_base * re2_h;
350
351                    // Accumulate integrals
352                    result.g_integral += zg;
353                    result.dg_dn_integral += zhh;
354                    result.dg_dnx_integral += zht;
355
356                    // E kernel contribution
357                    result.d2g_dnxdny_integral += zg * k2 * source_normal.dot(&el_norm);
358
359                    // RHS contribution if needed
360                    if compute_rhs && bc_type == 0 {
361                        // Velocity BC
362                        if let Some(bc) = bc_values {
363                            let mut zbgao = Complex64::new(0.0, 0.0);
364                            for (ii, &n) in shape_fn.iter().enumerate() {
365                                if ii < bc.len() {
366                                    zbgao += bc[ii] * n;
367                                }
368                            }
369                            let gamma = physics.gamma();
370                            let tau = physics.tau;
371                            let beta = physics.burton_miller_beta();
372                            result.rhs_contribution += (zg * gamma * tau + zht * beta) * zbgao;
373                        }
374                    }
375                }
376            }
377        }
378    }
379
380    // RHS for pressure BC
381    if compute_rhs
382        && bc_type == 1
383        && let Some(bc) = bc_values
384    {
385        let zbgao = bc.iter().sum::<Complex64>() / bc.len() as f64;
386        let gamma = physics.gamma();
387        let tau = physics.tau;
388        let beta = physics.burton_miller_beta();
389        result.rhs_contribution =
390            -(result.dg_dn_integral * gamma * tau + result.d2g_dnxdny_integral * beta) * zbgao;
391    }
392
393    result
394}
395
396/// Compute shape functions, derivatives, Jacobian and position at local coordinates
397#[allow(clippy::type_complexity)]
398fn compute_shape_and_jacobian(
399    element_coords: &Array2<f64>,
400    element_type: ElementType,
401    s: f64,
402    t: f64,
403) -> (Vec<f64>, Vec<f64>, Vec<f64>, f64, Array1<f64>, Array1<f64>) {
404    let num_nodes = element_type.num_nodes();
405
406    let (shape_fn, shape_ds, shape_dt) = match element_type {
407        ElementType::Tri3 => {
408            // Triangle area coordinates:
409            // N0 = 1-s-t (vertex at (0,0))
410            // N1 = s (vertex at (1,0))
411            // N2 = t (vertex at (0,1))
412            let shape_fn = vec![1.0 - s - t, s, t];
413            let shape_ds = vec![-1.0, 1.0, 0.0];
414            let shape_dt = vec![-1.0, 0.0, 1.0];
415            (shape_fn, shape_ds, shape_dt)
416        }
417        ElementType::Quad4 => {
418            // Bilinear quad
419            let s1 = 0.25 * (s + 1.0);
420            let s2 = 0.25 * (s - 1.0);
421            let t1 = t + 1.0;
422            let t2 = t - 1.0;
423
424            let shape_fn = vec![s1 * t1, -s2 * t1, s2 * t2, -s1 * t2];
425            let shape_ds = vec![
426                0.25 * (t + 1.0),
427                -0.25 * (t + 1.0),
428                0.25 * (t - 1.0),
429                -0.25 * (t - 1.0),
430            ];
431            let shape_dt = vec![
432                0.25 * (s + 1.0),
433                0.25 * (1.0 - s),
434                0.25 * (s - 1.0),
435                -0.25 * (s + 1.0),
436            ];
437            (shape_fn, shape_ds, shape_dt)
438        }
439    };
440
441    // Compute global position and tangent vectors
442    let mut crd_poi = Array1::zeros(3);
443    let mut dx_ds = Array1::zeros(3);
444    let mut dx_dt = Array1::zeros(3);
445
446    for i in 0..num_nodes {
447        for j in 0..3 {
448            crd_poi[j] += shape_fn[i] * element_coords[[i, j]];
449            dx_ds[j] += shape_ds[i] * element_coords[[i, j]];
450            dx_dt[j] += shape_dt[i] * element_coords[[i, j]];
451        }
452    }
453
454    // Normal = dx_ds × dx_dt
455    let normal = cross_product(&dx_ds, &dx_dt);
456    let jacobian = normal.dot(&normal).sqrt();
457
458    let el_norm = if jacobian > 1e-15 {
459        normal / jacobian
460    } else {
461        Array1::zeros(3)
462    };
463
464    (shape_fn, shape_ds, shape_dt, jacobian, el_norm, crd_poi)
465}
466
467/// Subelement data for adaptive integration
468#[derive(Debug, Clone)]
469pub struct Subelement {
470    /// Center local coordinate (xi) - used for quads
471    pub xi_center: f64,
472    /// Center local coordinate (eta) - used for quads
473    pub eta_center: f64,
474    /// Dimension factor (2^-j for subdivision level j) - used for quads
475    pub factor: f64,
476    /// Gauss order for this subelement
477    pub gauss_order: usize,
478    /// Vertex local coordinates for triangular subelements (for affine mapping)
479    /// Format: [(xi0, eta0), (xi1, eta1), (xi2, eta2)]
480    pub tri_vertices: Option<[(f64, f64); 3]>,
481}
482
483/// Generate adaptive subelements based on source point proximity
484///
485/// This implements NC_GenerateSubelements from the C++ code.
486/// Elements are recursively subdivided until the distance from the
487/// source point to subelement center exceeds a tolerance.
488///
489/// # Arguments
490/// * `source_point` - Source point coordinates
491/// * `element_coords` - Node coordinates of the element
492/// * `element_type` - Triangle or quad element
493/// * `element_area` - Area of the element
494///
495/// # Returns
496/// Vector of Subelements with their centers, scale factors, and Gauss orders
497pub fn generate_subelements(
498    source_point: &Array1<f64>,
499    element_coords: &Array2<f64>,
500    element_type: ElementType,
501    element_area: f64,
502) -> Vec<Subelement> {
503    const MAX_NSE: usize = 60;
504    const NSE: usize = 4; // Subdivision factor
505    // NumCalc uses ~3.0 for quasi-singular threshold
506    // Increased from 1.5 to improve near-singular integration accuracy
507    const TOL_F: f64 = 3.0; // Distance tolerance factor (was 1.5)
508    const GAU_MAX: usize = 7; // Maximum Gauss order
509    const GAU_MIN: usize = 4; // Minimum Gauss order (was 3, increased for better accuracy)
510    const GAU_ACCU: f64 = 0.0005; // Accuracy tolerance
511
512    let num_vertices = element_type.num_nodes();
513    let mut result = Vec::new();
514
515    // Local coordinates of vertices
516    let (csi_nodes, eta_nodes): (Vec<f64>, Vec<f64>) = match element_type {
517        ElementType::Tri3 => (CSI6[0..3].to_vec(), ETA6[0..3].to_vec()),
518        ElementType::Quad4 => (CSI8[0..4].to_vec(), ETA8[0..4].to_vec()),
519    };
520
521    // Working arrays for subelement vertices
522    let mut xi_sfp: Vec<Vec<f64>> = vec![vec![0.0; num_vertices]; MAX_NSE];
523    let mut et_sfp: Vec<Vec<f64>> = vec![vec![0.0; num_vertices]; MAX_NSE];
524
525    // Initialize with original element
526    xi_sfp[0][..num_vertices].copy_from_slice(&csi_nodes[..num_vertices]);
527    et_sfp[0][..num_vertices].copy_from_slice(&eta_nodes[..num_vertices]);
528
529    let mut nsfl = 1; // Number of subelements at current level
530    let mut faclin = 2.0; // Relative edge length factor
531
532    loop {
533        let mut ndie = 0; // Number of elements to subdivide
534        faclin *= 0.5;
535        let arels = element_area * faclin * faclin;
536        let nsel = nsfl;
537
538        // Copy current level
539        let xi_sep: Vec<Vec<f64>> = xi_sfp[..nsel].to_vec();
540        let et_sep: Vec<Vec<f64>> = et_sfp[..nsel].to_vec();
541
542        for idi in 0..nsel {
543            // Compute center of current subelement
544            let scent: f64 =
545                xi_sep[idi].iter().take(num_vertices).sum::<f64>() / num_vertices as f64;
546            let tcent: f64 =
547                et_sep[idi].iter().take(num_vertices).sum::<f64>() / num_vertices as f64;
548
549            // Global coordinates of center
550            let crd_poip = local_to_global(element_coords, element_type, scent, tcent);
551
552            // Distance ratio
553            let dist = distance(&crd_poip, source_point);
554            let ratdis = dist / arels.sqrt();
555
556            if ratdis < TOL_F {
557                // Need to subdivide
558                ndie += 1;
559                if ndie > 15 {
560                    // Too many subdivisions, stop
561                    break;
562                }
563
564                nsfl = ndie * NSE;
565                let nsf0 = nsfl - NSE;
566
567                // Generate midside points
568                let mut xisp = vec![0.0; num_vertices * 2];
569                let mut etsp = vec![0.0; num_vertices * 2];
570
571                for j in 0..num_vertices {
572                    let j1 = (j + 1) % num_vertices;
573                    xisp[j] = xi_sep[idi][j];
574                    xisp[j + num_vertices] = (xi_sep[idi][j] + xi_sep[idi][j1]) / 2.0;
575                    etsp[j] = et_sep[idi][j];
576                    etsp[j + num_vertices] = (et_sep[idi][j] + et_sep[idi][j1]) / 2.0;
577                }
578
579                // Generate 4 (or 3 for triangle) subelements
580                for j in 0..num_vertices {
581                    let nsu = nsf0 + j;
582                    let j1 = j + num_vertices;
583                    // For the second midpoint index:
584                    // - For vertex j, we need the midpoint between vertex j and vertex (j+1)%num_vertices
585                    // - This is stored at index j + num_vertices in the midpoint arrays
586                    // - The "previous" midpoint (between vertex (j-1)%num_vertices and j) is at
587                    //   index ((j + num_vertices - 1) % num_vertices) + num_vertices
588                    let j2 = if j == 0 {
589                        num_vertices + num_vertices - 1 // Last midpoint (between last vertex and vertex 0)
590                    } else {
591                        j - 1 + num_vertices // Previous midpoint
592                    };
593
594                    match element_type {
595                        ElementType::Quad4 => {
596                            xi_sfp[nsu] = vec![xisp[j], xisp[j1], scent, xisp[j2]];
597                            et_sfp[nsu] = vec![etsp[j], etsp[j1], tcent, etsp[j2]];
598                        }
599                        ElementType::Tri3 => {
600                            xi_sfp[nsu] = vec![xisp[j], xisp[j1], xisp[j2]];
601                            et_sfp[nsu] = vec![etsp[j], etsp[j1], etsp[j2]];
602
603                            // Central triangle for Tri3
604                            if j == num_vertices - 1 {
605                                let nsu_center = nsf0 + NSE - 1;
606                                xi_sfp[nsu_center] = vec![
607                                    xisp[num_vertices],
608                                    xisp[num_vertices + 1],
609                                    xisp[num_vertices + 2],
610                                ];
611                                et_sfp[nsu_center] = vec![
612                                    etsp[num_vertices],
613                                    etsp[num_vertices + 1],
614                                    etsp[num_vertices + 2],
615                                ];
616                            }
617                        }
618                    }
619                }
620            } else {
621                // Store subelement result
622                let (xi_center, eta_center, scale, tri_verts) = match element_type {
623                    ElementType::Quad4 => {
624                        let xc = xi_sep[idi].iter().sum::<f64>() / 4.0;
625                        let ec = et_sep[idi].iter().sum::<f64>() / 4.0;
626                        (xc, ec, faclin, None)
627                    }
628                    ElementType::Tri3 => {
629                        // Store the actual triangle vertices for proper affine mapping
630                        let xc = (xi_sep[idi][0] + xi_sep[idi][1] + xi_sep[idi][2]) / 3.0;
631                        let ec = (et_sep[idi][0] + et_sep[idi][1] + et_sep[idi][2]) / 3.0;
632                        let vertices = [
633                            (xi_sep[idi][0], et_sep[idi][0]),
634                            (xi_sep[idi][1], et_sep[idi][1]),
635                            (xi_sep[idi][2], et_sep[idi][2]),
636                        ];
637                        (xc, ec, faclin, Some(vertices))
638                    }
639                };
640
641                // Compute Gauss order based on distance
642                let disfac = 0.5 / ratdis;
643                let gauss_order = compute_gauss_order(disfac, GAU_MIN, GAU_MAX, GAU_ACCU);
644
645                result.push(Subelement {
646                    xi_center,
647                    eta_center,
648                    factor: scale,
649                    gauss_order,
650                    tri_vertices: tri_verts,
651                });
652
653                if result.len() >= MAX_SUBELEMENTS {
654                    return result;
655                }
656            }
657        }
658
659        if ndie == 0 {
660            break;
661        }
662    }
663
664    result
665}
666
667/// Compute optimal Gauss order for given distance factor
668fn compute_gauss_order(disfac: f64, gau_min: usize, gau_max: usize, accuracy: f64) -> usize {
669    for order in gau_min..=gau_max {
670        let err_g = estimate_error_g(order, disfac);
671        let err_h = estimate_error_h(order, disfac);
672        let err_e = estimate_error_e(order, disfac);
673
674        if err_g < accuracy && err_h < accuracy && err_e < accuracy {
675            return order;
676        }
677    }
678    gau_max
679}
680
681/// Error estimate for G kernel (1/r) integration
682fn estimate_error_g(order: usize, disfac: f64) -> f64 {
683    // Empirical error formula from NC_IntegrationConstants.h
684    let n = order as f64;
685    (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 1)
686}
687
688/// Error estimate for H kernel (1/r²) integration
689fn estimate_error_h(order: usize, disfac: f64) -> f64 {
690    let n = order as f64;
691    (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 2)
692}
693
694/// Error estimate for E kernel (1/r³) integration
695fn estimate_error_e(order: usize, disfac: f64) -> f64 {
696    let n = order as f64;
697    (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 3)
698}
699
700/// Convert local coordinates to global coordinates
701fn local_to_global(
702    element_coords: &Array2<f64>,
703    element_type: ElementType,
704    s: f64,
705    t: f64,
706) -> Array1<f64> {
707    let shape_fn = match element_type {
708        ElementType::Tri3 => vec![1.0 - s - t, s, t],
709        ElementType::Quad4 => {
710            let s1 = 0.25 * (s + 1.0);
711            let s2 = 0.25 * (s - 1.0);
712            let t1 = t + 1.0;
713            let t2 = t - 1.0;
714            vec![s1 * t1, -s2 * t1, s2 * t2, -s1 * t2]
715        }
716    };
717
718    let num_nodes = element_type.num_nodes();
719    let mut result = Array1::zeros(3);
720    for i in 0..num_nodes {
721        for j in 0..3 {
722            result[j] += shape_fn[i] * element_coords[[i, j]];
723        }
724    }
725    result
726}
727
728/// Euclidean distance between two points
729fn distance(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
730    let diff = a - b;
731    diff.dot(&diff).sqrt()
732}
733
734/// Estimate characteristic element size (square root of area for 2D elements)
735///
736/// For accuracy in quasi-singular integration, we use sqrt(area) as it
737/// provides a consistent measure regardless of element shape.
738fn estimate_element_size(element_coords: &Array2<f64>, element_type: ElementType) -> f64 {
739    let _num_nodes = element_type.num_nodes();
740
741    // Compute element area using cross product
742    match element_type {
743        ElementType::Tri3 => {
744            // Triangle area = 0.5 * |v1 × v2|
745            let v1: [f64; 3] = [
746                element_coords[[1, 0]] - element_coords[[0, 0]],
747                element_coords[[1, 1]] - element_coords[[0, 1]],
748                element_coords[[1, 2]] - element_coords[[0, 2]],
749            ];
750            let v2: [f64; 3] = [
751                element_coords[[2, 0]] - element_coords[[0, 0]],
752                element_coords[[2, 1]] - element_coords[[0, 1]],
753                element_coords[[2, 2]] - element_coords[[0, 2]],
754            ];
755            let cross = [
756                v1[1] * v2[2] - v1[2] * v2[1],
757                v1[2] * v2[0] - v1[0] * v2[2],
758                v1[0] * v2[1] - v1[1] * v2[0],
759            ];
760            let area =
761                0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
762            area.sqrt().max(1e-10)
763        }
764        ElementType::Quad4 => {
765            // Quad area via two triangles (split along diagonal 0-2)
766            let v1: [f64; 3] = [
767                element_coords[[1, 0]] - element_coords[[0, 0]],
768                element_coords[[1, 1]] - element_coords[[0, 1]],
769                element_coords[[1, 2]] - element_coords[[0, 2]],
770            ];
771            let v2: [f64; 3] = [
772                element_coords[[2, 0]] - element_coords[[0, 0]],
773                element_coords[[2, 1]] - element_coords[[0, 1]],
774                element_coords[[2, 2]] - element_coords[[0, 2]],
775            ];
776            let cross1 = [
777                v1[1] * v2[2] - v1[2] * v2[1],
778                v1[2] * v2[0] - v1[0] * v2[2],
779                v1[0] * v2[1] - v1[1] * v2[0],
780            ];
781
782            let v3: [f64; 3] = [
783                element_coords[[3, 0]] - element_coords[[0, 0]],
784                element_coords[[3, 1]] - element_coords[[0, 1]],
785                element_coords[[3, 2]] - element_coords[[0, 2]],
786            ];
787            let cross2 = [
788                v2[1] * v3[2] - v2[2] * v3[1],
789                v2[2] * v3[0] - v2[0] * v3[2],
790                v2[0] * v3[1] - v2[1] * v3[0],
791            ];
792
793            let area1 = 0.5
794                * (cross1[0] * cross1[0] + cross1[1] * cross1[1] + cross1[2] * cross1[2]).sqrt();
795            let area2 = 0.5
796                * (cross2[0] * cross2[0] + cross2[1] * cross2[1] + cross2[2] * cross2[2]).sqrt();
797            (area1 + area2).sqrt().max(1e-10)
798        }
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use ndarray::Array2;
806
807    fn make_test_triangle() -> Array2<f64> {
808        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()
809    }
810
811    #[test]
812    fn test_local_to_global_triangle() {
813        let coords = make_test_triangle();
814
815        // At center (1/3, 1/3)
816        let center = local_to_global(&coords, ElementType::Tri3, 1.0 / 3.0, 1.0 / 3.0);
817        assert!((center[0] - 1.0 / 3.0).abs() < 1e-10);
818        assert!((center[1] - 1.0 / 3.0).abs() < 1e-10);
819        assert!(center[2].abs() < 1e-10);
820    }
821
822    #[test]
823    fn test_generate_subelements_far_point() {
824        let coords = make_test_triangle();
825        let source = array![10.0, 10.0, 0.0]; // Far from element
826
827        let subelements = generate_subelements(&source, &coords, ElementType::Tri3, 0.5);
828
829        // Far point should not require many subdivisions
830        assert!(subelements.len() <= 4);
831    }
832
833    #[test]
834    fn test_singular_integration_basic() {
835        let coords = make_test_triangle();
836        let source = array![1.0 / 3.0, 1.0 / 3.0, 0.0]; // At center
837        let normal = array![0.0, 0.0, 1.0];
838
839        // Use low frequency so kr << 1 and cos(kr) ≈ 1 (positive real part)
840        let physics = PhysicsParams::new(10.0, 343.0, 1.21, false);
841
842        let result = singular_integration(
843            &source,
844            &normal,
845            &coords,
846            ElementType::Tri3,
847            &physics,
848            None,
849            0,
850            false,
851        );
852
853        // G integral should be finite and non-zero
854        assert!(result.g_integral.norm().is_finite());
855        assert!(result.g_integral.norm() > 0.0, "G integral was zero");
856
857        // At low frequency (small kr), the real part should be positive
858        assert!(
859            result.g_integral.re > 0.0,
860            "G integral real part was: {} (expected positive at low freq)",
861            result.g_integral.re
862        );
863
864        // H integral should be zero (source normal perpendicular to element)
865        assert!(
866            result.dg_dn_integral.norm() < 1e-10,
867            "H integral should be zero, was: {:?}",
868            result.dg_dn_integral
869        );
870    }
871
872    #[test]
873    fn test_compute_shape_and_jacobian() {
874        let coords = make_test_triangle();
875
876        let (shape, _, _, jac, normal, _pos) =
877            compute_shape_and_jacobian(&coords, ElementType::Tri3, 0.5, 0.25);
878
879        // Shape functions should sum to 1
880        let sum: f64 = shape.iter().sum();
881        assert!((sum - 1.0).abs() < 1e-10);
882
883        // Jacobian should be 2 * area = 1 for this triangle
884        assert!((jac - 1.0).abs() < 1e-10);
885
886        // Normal should be (0, 0, 1)
887        assert!(normal[2].abs() > 0.99);
888    }
889}