1use 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
14pub const MAX_SUBELEMENTS: usize = 110;
16
17#[derive(Debug, Clone, Copy)]
22pub struct QuadratureParams {
23 pub edge_gauss_order: usize,
25 pub subelement_gauss_order: usize,
27 pub edge_sections: usize,
29 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 pub fn for_ka(ka: f64) -> Self {
49 if ka < 0.3 {
50 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 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 Self {
68 edge_gauss_order: 5,
69 subelement_gauss_order: 6,
70 edge_sections: 8,
71 subtriangles_per_section: 3,
72 }
73 } else {
74 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 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
95const 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
101const 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
105pub 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 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
151pub 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 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 for ieg in 0..num_nodes {
179 let ig1 = (ieg + 1) % num_nodes;
180 let ig2 = ieg + num_nodes;
181
182 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 let diff_poo = &diff_poi / leneg;
193 let leneg_scaled = leneg / (2.0 * nsec1 as f64);
194
195 let mut zre = Complex64::new(0.0, 0.0);
197
198 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 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 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 let (unit_r, dis_fsp) = normalize(&diff_fsp);
220
221 if dis_fsp < 1e-15 {
222 continue;
223 }
224
225 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 let z1 = Complex64::new(-1.0 / dis_fsp, wavruim);
232 let zg_factor = zg * z1;
233
234 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 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 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 for isec in 0..nsec2 {
258 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 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 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 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 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 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 let z1 = Complex64::new(-1.0 / dis_fsp, wavruim);
345 let zhh_base = zg * z1;
346 let re1_h = unit_r.dot(&el_norm); let re2_h = -unit_r.dot(source_normal); let zhh = zhh_base * re1_h;
349 let zht = zhh_base * re2_h;
350
351 result.g_integral += zg;
353 result.dg_dn_integral += zhh;
354 result.dg_dnx_integral += zht;
355
356 result.d2g_dnxdny_integral += zg * k2 * source_normal.dot(&el_norm);
358
359 if compute_rhs && bc_type == 0 {
361 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 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#[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 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 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 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 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#[derive(Debug, Clone)]
469pub struct Subelement {
470 pub xi_center: f64,
472 pub eta_center: f64,
474 pub factor: f64,
476 pub gauss_order: usize,
478 pub tri_vertices: Option<[(f64, f64); 3]>,
481}
482
483pub 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; const TOL_F: f64 = 3.0; const GAU_MAX: usize = 7; const GAU_MIN: usize = 4; const GAU_ACCU: f64 = 0.0005; let num_vertices = element_type.num_nodes();
513 let mut result = Vec::new();
514
515 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 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 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; let mut faclin = 2.0; loop {
533 let mut ndie = 0; faclin *= 0.5;
535 let arels = element_area * faclin * faclin;
536 let nsel = nsfl;
537
538 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 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 let crd_poip = local_to_global(element_coords, element_type, scent, tcent);
551
552 let dist = distance(&crd_poip, source_point);
554 let ratdis = dist / arels.sqrt();
555
556 if ratdis < TOL_F {
557 ndie += 1;
559 if ndie > 15 {
560 break;
562 }
563
564 nsfl = ndie * NSE;
565 let nsf0 = nsfl - NSE;
566
567 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 for j in 0..num_vertices {
581 let nsu = nsf0 + j;
582 let j1 = j + num_vertices;
583 let j2 = if j == 0 {
589 num_vertices + num_vertices - 1 } else {
591 j - 1 + num_vertices };
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 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 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 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 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
667fn 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
681fn estimate_error_g(order: usize, disfac: f64) -> f64 {
683 let n = order as f64;
685 (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 1)
686}
687
688fn 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
694fn 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
700fn 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
728fn distance(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
730 let diff = a - b;
731 diff.dot(&diff).sqrt()
732}
733
734fn estimate_element_size(element_coords: &Array2<f64>, element_type: ElementType) -> f64 {
739 let _num_nodes = element_type.num_nodes();
740
741 match element_type {
743 ElementType::Tri3 => {
744 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 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 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]; let subelements = generate_subelements(&source, &coords, ElementType::Tri3, 0.5);
828
829 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]; let normal = array![0.0, 0.0, 1.0];
838
839 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 assert!(result.g_integral.norm().is_finite());
855 assert!(result.g_integral.norm() > 0.0, "G integral was zero");
856
857 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 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 let sum: f64 = shape.iter().sum();
881 assert!((sum - 1.0).abs() < 1e-10);
882
883 assert!((jac - 1.0).abs() < 1e-10);
885
886 assert!(normal[2].abs() > 0.99);
888 }
889}