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 j1 > num_vertices {
584 j1 - 1
585 } else {
586 j1 + num_vertices - 1
587 };
588
589 match element_type {
590 ElementType::Quad4 => {
591 xi_sfp[nsu] = vec![xisp[j], xisp[j1], scent, xisp[j2]];
592 et_sfp[nsu] = vec![etsp[j], etsp[j1], tcent, etsp[j2]];
593 }
594 ElementType::Tri3 => {
595 xi_sfp[nsu] = vec![xisp[j], xisp[j1], xisp[j2]];
596 et_sfp[nsu] = vec![etsp[j], etsp[j1], etsp[j2]];
597
598 if j == num_vertices - 1 {
600 let nsu_center = nsf0 + NSE - 1;
601 xi_sfp[nsu_center] = vec![
602 xisp[num_vertices],
603 xisp[num_vertices + 1],
604 xisp[num_vertices + 2],
605 ];
606 et_sfp[nsu_center] = vec![
607 etsp[num_vertices],
608 etsp[num_vertices + 1],
609 etsp[num_vertices + 2],
610 ];
611 }
612 }
613 }
614 }
615 } else {
616 let (xi_center, eta_center, scale, tri_verts) = match element_type {
618 ElementType::Quad4 => {
619 let xc = xi_sep[idi].iter().sum::<f64>() / 4.0;
620 let ec = et_sep[idi].iter().sum::<f64>() / 4.0;
621 (xc, ec, faclin, None)
622 }
623 ElementType::Tri3 => {
624 let xc = (xi_sep[idi][0] + xi_sep[idi][1] + xi_sep[idi][2]) / 3.0;
626 let ec = (et_sep[idi][0] + et_sep[idi][1] + et_sep[idi][2]) / 3.0;
627 let vertices = [
628 (xi_sep[idi][0], et_sep[idi][0]),
629 (xi_sep[idi][1], et_sep[idi][1]),
630 (xi_sep[idi][2], et_sep[idi][2]),
631 ];
632 (xc, ec, faclin, Some(vertices))
633 }
634 };
635
636 let disfac = 0.5 / ratdis;
638 let gauss_order = compute_gauss_order(disfac, GAU_MIN, GAU_MAX, GAU_ACCU);
639
640 result.push(Subelement {
641 xi_center,
642 eta_center,
643 factor: scale,
644 gauss_order,
645 tri_vertices: tri_verts,
646 });
647
648 if result.len() >= MAX_SUBELEMENTS {
649 return result;
650 }
651 }
652 }
653
654 if ndie == 0 {
655 break;
656 }
657 }
658
659 result
660}
661
662fn compute_gauss_order(disfac: f64, gau_min: usize, gau_max: usize, accuracy: f64) -> usize {
664 for order in gau_min..=gau_max {
665 let err_g = estimate_error_g(order, disfac);
666 let err_h = estimate_error_h(order, disfac);
667 let err_e = estimate_error_e(order, disfac);
668
669 if err_g < accuracy && err_h < accuracy && err_e < accuracy {
670 return order;
671 }
672 }
673 gau_max
674}
675
676fn estimate_error_g(order: usize, disfac: f64) -> f64 {
678 let n = order as f64;
680 (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 1)
681}
682
683fn estimate_error_h(order: usize, disfac: f64) -> f64 {
685 let n = order as f64;
686 (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 2)
687}
688
689fn estimate_error_e(order: usize, disfac: f64) -> f64 {
691 let n = order as f64;
692 (disfac / (2.0 * n + 1.0)).powi(2 * order as i32 + 3)
693}
694
695fn local_to_global(
697 element_coords: &Array2<f64>,
698 element_type: ElementType,
699 s: f64,
700 t: f64,
701) -> Array1<f64> {
702 let shape_fn = match element_type {
703 ElementType::Tri3 => vec![1.0 - s - t, s, t],
704 ElementType::Quad4 => {
705 let s1 = 0.25 * (s + 1.0);
706 let s2 = 0.25 * (s - 1.0);
707 let t1 = t + 1.0;
708 let t2 = t - 1.0;
709 vec![s1 * t1, -s2 * t1, s2 * t2, -s1 * t2]
710 }
711 };
712
713 let num_nodes = element_type.num_nodes();
714 let mut result = Array1::zeros(3);
715 for i in 0..num_nodes {
716 for j in 0..3 {
717 result[j] += shape_fn[i] * element_coords[[i, j]];
718 }
719 }
720 result
721}
722
723fn distance(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
725 let diff = a - b;
726 diff.dot(&diff).sqrt()
727}
728
729fn estimate_element_size(element_coords: &Array2<f64>, element_type: ElementType) -> f64 {
731 let num_nodes = element_type.num_nodes();
732 let mut total_length = 0.0;
733
734 for i in 0..num_nodes {
735 let j = (i + 1) % num_nodes;
736 let mut edge_length_sq = 0.0;
737 for k in 0..3 {
738 let diff = element_coords[[j, k]] - element_coords[[i, k]];
739 edge_length_sq += diff * diff;
740 }
741 total_length += edge_length_sq.sqrt();
742 }
743
744 total_length / num_nodes as f64
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use ndarray::Array2;
751
752 fn make_test_triangle() -> Array2<f64> {
753 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()
754 }
755
756 #[test]
757 fn test_local_to_global_triangle() {
758 let coords = make_test_triangle();
759
760 let center = local_to_global(&coords, ElementType::Tri3, 1.0 / 3.0, 1.0 / 3.0);
762 assert!((center[0] - 1.0 / 3.0).abs() < 1e-10);
763 assert!((center[1] - 1.0 / 3.0).abs() < 1e-10);
764 assert!(center[2].abs() < 1e-10);
765 }
766
767 #[test]
768 fn test_generate_subelements_far_point() {
769 let coords = make_test_triangle();
770 let source = array![10.0, 10.0, 0.0]; let subelements = generate_subelements(&source, &coords, ElementType::Tri3, 0.5);
773
774 assert!(subelements.len() <= 4);
776 }
777
778 #[test]
779 fn test_singular_integration_basic() {
780 let coords = make_test_triangle();
781 let source = array![1.0 / 3.0, 1.0 / 3.0, 0.0]; let normal = array![0.0, 0.0, 1.0];
783
784 let physics = PhysicsParams::new(10.0, 343.0, 1.21, false);
786
787 let result = singular_integration(
788 &source,
789 &normal,
790 &coords,
791 ElementType::Tri3,
792 &physics,
793 None,
794 0,
795 false,
796 );
797
798 assert!(result.g_integral.norm().is_finite());
800 assert!(result.g_integral.norm() > 0.0, "G integral was zero");
801
802 assert!(
804 result.g_integral.re > 0.0,
805 "G integral real part was: {} (expected positive at low freq)",
806 result.g_integral.re
807 );
808
809 assert!(
811 result.dg_dn_integral.norm() < 1e-10,
812 "H integral should be zero, was: {:?}",
813 result.dg_dn_integral
814 );
815 }
816
817 #[test]
818 fn test_compute_shape_and_jacobian() {
819 let coords = make_test_triangle();
820
821 let (shape, _, _, jac, normal, _pos) =
822 compute_shape_and_jacobian(&coords, ElementType::Tri3, 0.5, 0.25);
823
824 let sum: f64 = shape.iter().sum();
826 assert!((sum - 1.0).abs() < 1e-10);
827
828 assert!((jac - 1.0).abs() < 1e-10);
830
831 assert!(normal[2].abs() > 0.99);
833 }
834}