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::{array, Array1, Array2};
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 +=
358                    zg * k2 * source_normal.dot(&el_norm);
359
360                // RHS contribution if needed
361                if compute_rhs && bc_type == 0 {
362                    // Velocity BC
363                    if let Some(bc) = bc_values {
364                        let mut zbgao = Complex64::new(0.0, 0.0);
365                        for (ii, &n) in shape_fn.iter().enumerate() {
366                            if ii < bc.len() {
367                                zbgao += bc[ii] * n;
368                            }
369                        }
370                        let gamma = physics.gamma();
371                        let tau = physics.tau;
372                        let beta = physics.burton_miller_beta();
373                        result.rhs_contribution +=
374                            (zg * gamma * tau + zht * beta) * zbgao;
375                    }
376                }
377                }
378            }
379        }
380    }
381
382    // RHS for pressure BC
383    if compute_rhs && bc_type == 1 {
384        if let Some(bc) = bc_values {
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 = -(result.dg_dn_integral * gamma * tau
390                + result.d2g_dnxdny_integral * beta)
391                * zbgao;
392        }
393    }
394
395    result
396}
397
398/// Compute shape functions, derivatives, Jacobian and position at local coordinates
399fn compute_shape_and_jacobian(
400    element_coords: &Array2<f64>,
401    element_type: ElementType,
402    s: f64,
403    t: f64,
404) -> (Vec<f64>, Vec<f64>, Vec<f64>, f64, Array1<f64>, Array1<f64>) {
405    let num_nodes = element_type.num_nodes();
406
407    let (shape_fn, shape_ds, shape_dt) = match element_type {
408        ElementType::Tri3 => {
409            // Triangle area coordinates:
410            // N0 = 1-s-t (vertex at (0,0))
411            // N1 = s (vertex at (1,0))
412            // N2 = t (vertex at (0,1))
413            let shape_fn = vec![1.0 - s - t, s, t];
414            let shape_ds = vec![-1.0, 1.0, 0.0];
415            let shape_dt = vec![-1.0, 0.0, 1.0];
416            (shape_fn, shape_ds, shape_dt)
417        }
418        ElementType::Quad4 => {
419            // Bilinear quad
420            let s1 = 0.25 * (s + 1.0);
421            let s2 = 0.25 * (s - 1.0);
422            let t1 = t + 1.0;
423            let t2 = t - 1.0;
424
425            let shape_fn = vec![s1 * t1, -s2 * t1, s2 * t2, -s1 * t2];
426            let shape_ds = vec![
427                0.25 * (t + 1.0),
428                -0.25 * (t + 1.0),
429                0.25 * (t - 1.0),
430                -0.25 * (t - 1.0),
431            ];
432            let shape_dt = vec![
433                0.25 * (s + 1.0),
434                0.25 * (1.0 - s),
435                0.25 * (s - 1.0),
436                -0.25 * (s + 1.0),
437            ];
438            (shape_fn, shape_ds, shape_dt)
439        }
440    };
441
442    // Compute global position and tangent vectors
443    let mut crd_poi = Array1::zeros(3);
444    let mut dx_ds = Array1::zeros(3);
445    let mut dx_dt = Array1::zeros(3);
446
447    for i in 0..num_nodes {
448        for j in 0..3 {
449            crd_poi[j] += shape_fn[i] * element_coords[[i, j]];
450            dx_ds[j] += shape_ds[i] * element_coords[[i, j]];
451            dx_dt[j] += shape_dt[i] * element_coords[[i, j]];
452        }
453    }
454
455    // Normal = dx_ds × dx_dt
456    let normal = cross_product(&dx_ds, &dx_dt);
457    let jacobian = normal.dot(&normal).sqrt();
458
459    let el_norm = if jacobian > 1e-15 {
460        normal / jacobian
461    } else {
462        Array1::zeros(3)
463    };
464
465    (shape_fn, shape_ds, shape_dt, jacobian, el_norm, crd_poi)
466}
467
468/// Subelement data for adaptive integration
469#[derive(Debug, Clone)]
470pub struct Subelement {
471    /// Center local coordinate (xi) - used for quads
472    pub xi_center: f64,
473    /// Center local coordinate (eta) - used for quads
474    pub eta_center: f64,
475    /// Dimension factor (2^-j for subdivision level j) - used for quads
476    pub factor: f64,
477    /// Gauss order for this subelement
478    pub gauss_order: usize,
479    /// Vertex local coordinates for triangular subelements (for affine mapping)
480    /// Format: [(xi0, eta0), (xi1, eta1), (xi2, eta2)]
481    pub tri_vertices: Option<[(f64, f64); 3]>,
482}
483
484/// Generate adaptive subelements based on source point proximity
485///
486/// This implements NC_GenerateSubelements from the C++ code.
487/// Elements are recursively subdivided until the distance from the
488/// source point to subelement center exceeds a tolerance.
489///
490/// # Arguments
491/// * `source_point` - Source point coordinates
492/// * `element_coords` - Node coordinates of the element
493/// * `element_type` - Triangle or quad element
494/// * `element_area` - Area of the element
495///
496/// # Returns
497/// Vector of Subelements with their centers, scale factors, and Gauss orders
498pub fn generate_subelements(
499    source_point: &Array1<f64>,
500    element_coords: &Array2<f64>,
501    element_type: ElementType,
502    element_area: f64,
503) -> Vec<Subelement> {
504    const MAX_NSE: usize = 60;
505    const NSE: usize = 4; // Subdivision factor
506    // NumCalc uses ~3.0 for quasi-singular threshold
507    // Increased from 1.5 to improve near-singular integration accuracy
508    const TOL_F: f64 = 3.0; // Distance tolerance factor (was 1.5)
509    const GAU_MAX: usize = 7; // Maximum Gauss order
510    const GAU_MIN: usize = 4; // Minimum Gauss order (was 3, increased for better accuracy)
511    const GAU_ACCU: f64 = 0.0005; // Accuracy tolerance
512
513    let num_vertices = element_type.num_nodes();
514    let mut result = Vec::new();
515
516    // Local coordinates of vertices
517    let (csi_nodes, eta_nodes): (Vec<f64>, Vec<f64>) = match element_type {
518        ElementType::Tri3 => (CSI6[0..3].to_vec(), ETA6[0..3].to_vec()),
519        ElementType::Quad4 => (CSI8[0..4].to_vec(), ETA8[0..4].to_vec()),
520    };
521
522    // Working arrays for subelement vertices
523    let mut xi_sfp: Vec<Vec<f64>> = vec![vec![0.0; num_vertices]; MAX_NSE];
524    let mut et_sfp: Vec<Vec<f64>> = vec![vec![0.0; num_vertices]; MAX_NSE];
525
526    // Initialize with original element
527    for j in 0..num_vertices {
528        xi_sfp[0][j] = csi_nodes[j];
529        et_sfp[0][j] = eta_nodes[j];
530    }
531
532    let mut nsfl = 1; // Number of subelements at current level
533    let mut faclin = 2.0; // Relative edge length factor
534
535    loop {
536        let mut ndie = 0; // Number of elements to subdivide
537        faclin *= 0.5;
538        let arels = element_area * faclin * faclin;
539        let nsel = nsfl;
540
541        // Copy current level
542        let xi_sep: Vec<Vec<f64>> = xi_sfp[..nsel].to_vec();
543        let et_sep: Vec<Vec<f64>> = et_sfp[..nsel].to_vec();
544
545        for idi in 0..nsel {
546            // Compute center of current subelement
547            let scent: f64 = xi_sep[idi].iter().take(num_vertices).sum::<f64>() / num_vertices as f64;
548            let tcent: f64 = et_sep[idi].iter().take(num_vertices).sum::<f64>() / num_vertices as f64;
549
550            // Global coordinates of center
551            let crd_poip = local_to_global(element_coords, element_type, scent, tcent);
552
553            // Distance ratio
554            let dist = distance(&crd_poip, source_point);
555            let ratdis = dist / arels.sqrt();
556
557            if ratdis < TOL_F {
558                // Need to subdivide
559                ndie += 1;
560                if ndie > 15 {
561                    // Too many subdivisions, stop
562                    break;
563                }
564
565                nsfl = ndie * NSE;
566                let nsf0 = nsfl - NSE;
567
568                // Generate midside points
569                let mut xisp = vec![0.0; num_vertices * 2];
570                let mut etsp = vec![0.0; num_vertices * 2];
571
572                for j in 0..num_vertices {
573                    let j1 = (j + 1) % num_vertices;
574                    xisp[j] = xi_sep[idi][j];
575                    xisp[j + num_vertices] = (xi_sep[idi][j] + xi_sep[idi][j1]) / 2.0;
576                    etsp[j] = et_sep[idi][j];
577                    etsp[j + num_vertices] = (et_sep[idi][j] + et_sep[idi][j1]) / 2.0;
578                }
579
580                // Generate 4 (or 3 for triangle) subelements
581                for j in 0..num_vertices {
582                    let nsu = nsf0 + j;
583                    let j1 = j + num_vertices;
584                    let j2 = if j1 > num_vertices {
585                        j1 - 1
586                    } else {
587                        j1 + num_vertices - 1
588                    };
589
590                    match element_type {
591                        ElementType::Quad4 => {
592                            xi_sfp[nsu] = vec![xisp[j], xisp[j1], scent, xisp[j2]];
593                            et_sfp[nsu] = vec![etsp[j], etsp[j1], tcent, etsp[j2]];
594                        }
595                        ElementType::Tri3 => {
596                            xi_sfp[nsu] = vec![xisp[j], xisp[j1], xisp[j2]];
597                            et_sfp[nsu] = vec![etsp[j], etsp[j1], etsp[j2]];
598
599                            // Central triangle for Tri3
600                            if j == num_vertices - 1 {
601                                let nsu_center = nsf0 + NSE - 1;
602                                xi_sfp[nsu_center] = vec![
603                                    xisp[num_vertices],
604                                    xisp[num_vertices + 1],
605                                    xisp[num_vertices + 2],
606                                ];
607                                et_sfp[nsu_center] = vec![
608                                    etsp[num_vertices],
609                                    etsp[num_vertices + 1],
610                                    etsp[num_vertices + 2],
611                                ];
612                            }
613                        }
614                    }
615                }
616            } else {
617                // Store subelement result
618                let (xi_center, eta_center, scale, tri_verts) = match element_type {
619                    ElementType::Quad4 => {
620                        let xc = xi_sep[idi].iter().sum::<f64>() / 4.0;
621                        let ec = et_sep[idi].iter().sum::<f64>() / 4.0;
622                        (xc, ec, faclin, None)
623                    }
624                    ElementType::Tri3 => {
625                        // Store the actual triangle vertices for proper affine mapping
626                        let xc = (xi_sep[idi][0] + xi_sep[idi][1] + xi_sep[idi][2]) / 3.0;
627                        let ec = (et_sep[idi][0] + et_sep[idi][1] + et_sep[idi][2]) / 3.0;
628                        let vertices = [
629                            (xi_sep[idi][0], et_sep[idi][0]),
630                            (xi_sep[idi][1], et_sep[idi][1]),
631                            (xi_sep[idi][2], et_sep[idi][2]),
632                        ];
633                        (xc, ec, faclin, Some(vertices))
634                    }
635                };
636
637                // Compute Gauss order based on distance
638                let disfac = 0.5 / ratdis;
639                let gauss_order = compute_gauss_order(disfac, GAU_MIN, GAU_MAX, GAU_ACCU);
640
641                result.push(Subelement {
642                    xi_center,
643                    eta_center,
644                    factor: scale,
645                    gauss_order,
646                    tri_vertices: tri_verts,
647                });
648
649                if result.len() >= MAX_SUBELEMENTS {
650                    return result;
651                }
652            }
653        }
654
655        if ndie == 0 {
656            break;
657        }
658    }
659
660    result
661}
662
663/// Compute optimal Gauss order for given distance factor
664fn compute_gauss_order(disfac: f64, gau_min: usize, gau_max: usize, accuracy: f64) -> usize {
665    for order in gau_min..=gau_max {
666        let err_g = estimate_error_g(order, disfac);
667        let err_h = estimate_error_h(order, disfac);
668        let err_e = estimate_error_e(order, disfac);
669
670        if err_g < accuracy && err_h < accuracy && err_e < accuracy {
671            return order;
672        }
673    }
674    gau_max
675}
676
677/// Error estimate for G kernel (1/r) integration
678fn estimate_error_g(order: usize, disfac: f64) -> f64 {
679    // Empirical error formula from NC_IntegrationConstants.h
680    let n = order as f64;
681    (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 1)
682}
683
684/// Error estimate for H kernel (1/r²) integration
685fn estimate_error_h(order: usize, disfac: f64) -> f64 {
686    let n = order as f64;
687    (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 2)
688}
689
690/// Error estimate for E kernel (1/r³) integration
691fn estimate_error_e(order: usize, disfac: f64) -> f64 {
692    let n = order as f64;
693    (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 3)
694}
695
696/// Convert local coordinates to global coordinates
697fn local_to_global(
698    element_coords: &Array2<f64>,
699    element_type: ElementType,
700    s: f64,
701    t: f64,
702) -> Array1<f64> {
703    let shape_fn = match element_type {
704        ElementType::Tri3 => vec![1.0 - s - t, s, t],
705        ElementType::Quad4 => {
706            let s1 = 0.25 * (s + 1.0);
707            let s2 = 0.25 * (s - 1.0);
708            let t1 = t + 1.0;
709            let t2 = t - 1.0;
710            vec![s1 * t1, -s2 * t1, s2 * t2, -s1 * t2]
711        }
712    };
713
714    let num_nodes = element_type.num_nodes();
715    let mut result = Array1::zeros(3);
716    for i in 0..num_nodes {
717        for j in 0..3 {
718            result[j] += shape_fn[i] * element_coords[[i, j]];
719        }
720    }
721    result
722}
723
724/// Euclidean distance between two points
725fn distance(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
726    let diff = a - b;
727    diff.dot(&diff).sqrt()
728}
729
730/// Estimate characteristic element size (average edge length)
731fn estimate_element_size(element_coords: &Array2<f64>, element_type: ElementType) -> f64 {
732    let num_nodes = element_type.num_nodes();
733    let mut total_length = 0.0;
734
735    for i in 0..num_nodes {
736        let j = (i + 1) % num_nodes;
737        let mut edge_length_sq = 0.0;
738        for k in 0..3 {
739            let diff = element_coords[[j, k]] - element_coords[[i, k]];
740            edge_length_sq += diff * diff;
741        }
742        total_length += edge_length_sq.sqrt();
743    }
744
745    total_length / num_nodes as f64
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use ndarray::Array2;
752
753    fn make_test_triangle() -> Array2<f64> {
754        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()
755    }
756
757    #[test]
758    fn test_local_to_global_triangle() {
759        let coords = make_test_triangle();
760
761        // At center (1/3, 1/3)
762        let center = local_to_global(&coords, ElementType::Tri3, 1.0 / 3.0, 1.0 / 3.0);
763        assert!((center[0] - 1.0 / 3.0).abs() < 1e-10);
764        assert!((center[1] - 1.0 / 3.0).abs() < 1e-10);
765        assert!(center[2].abs() < 1e-10);
766    }
767
768    #[test]
769    fn test_generate_subelements_far_point() {
770        let coords = make_test_triangle();
771        let source = array![10.0, 10.0, 0.0]; // Far from element
772
773        let subelements = generate_subelements(&source, &coords, ElementType::Tri3, 0.5);
774
775        // Far point should not require many subdivisions
776        assert!(subelements.len() <= 4);
777    }
778
779    #[test]
780    fn test_singular_integration_basic() {
781        let coords = make_test_triangle();
782        let source = array![1.0 / 3.0, 1.0 / 3.0, 0.0]; // At center
783        let normal = array![0.0, 0.0, 1.0];
784
785        // Use low frequency so kr << 1 and cos(kr) ≈ 1 (positive real part)
786        let physics = PhysicsParams::new(10.0, 343.0, 1.21, false);
787
788        let result =
789            singular_integration(&source, &normal, &coords, ElementType::Tri3, &physics, None, 0, false);
790
791        // G integral should be finite and non-zero
792        assert!(result.g_integral.norm().is_finite());
793        assert!(result.g_integral.norm() > 0.0, "G integral was zero");
794
795        // At low frequency (small kr), the real part should be positive
796        assert!(result.g_integral.re > 0.0, "G integral real part was: {} (expected positive at low freq)", result.g_integral.re);
797
798        // H integral should be zero (source normal perpendicular to element)
799        assert!(result.dg_dn_integral.norm() < 1e-10, "H integral should be zero, was: {:?}", result.dg_dn_integral);
800    }
801
802    #[test]
803    fn test_compute_shape_and_jacobian() {
804        let coords = make_test_triangle();
805
806        let (shape, _, _, jac, normal, pos) =
807            compute_shape_and_jacobian(&coords, ElementType::Tri3, 0.5, 0.25);
808
809        // Shape functions should sum to 1
810        let sum: f64 = shape.iter().sum();
811        assert!((sum - 1.0).abs() < 1e-10);
812
813        // Jacobian should be 2 * area = 1 for this triangle
814        assert!((jac - 1.0).abs() < 1e-10);
815
816        // Normal should be (0, 0, 1)
817        assert!(normal[2].abs() > 0.99);
818    }
819}