Skip to main content

math_audio_bem/core/mesh/
element.rs

1//! Element shape functions and geometry computations
2//!
3//! Direct port of shape function computations from NC_3dFunctions.cpp.
4
5use ndarray::{Array1, Array2, array};
6
7use crate::core::types::{Element, ElementType};
8
9/// Shape functions for triangular and quadrilateral elements
10impl Element {
11    /// Compute shape functions at local coordinates (s, t)
12    ///
13    /// For triangles: Area coordinates with standard vertex mapping:
14    /// - (0,0) -> node 0
15    /// - (1,0) -> node 1
16    /// - (0,1) -> node 2
17    ///
18    /// For quads: Bilinear functions on [-1,1]²
19    pub fn shape_functions(&self, s: f64, t: f64) -> Array1<f64> {
20        match self.element_type {
21            ElementType::Tri3 => {
22                // Triangle shape functions:
23                // N0 = 1-s-t (vertex at (0,0))
24                // N1 = s (vertex at (1,0))
25                // N2 = t (vertex at (0,1))
26                array![1.0 - s - t, s, t]
27            }
28            ElementType::Quad4 => {
29                // Quad: bilinear on [-1,1]²
30                let s1 = 0.25 * (s + 1.0);
31                let s2 = 0.25 * (s - 1.0);
32                let t1 = t + 1.0;
33                let t2 = t - 1.0;
34                array![s1 * t1, -s2 * t1, s2 * t2, -s1 * t2]
35            }
36        }
37    }
38
39    /// Compute shape function derivatives dN/ds and dN/dt
40    pub fn shape_derivatives(&self, s: f64, t: f64) -> (Array1<f64>, Array1<f64>) {
41        match self.element_type {
42            ElementType::Tri3 => {
43                // dN/ds = [-1, 1, 0], dN/dt = [-1, 0, 1]
44                (array![-1.0, 1.0, 0.0], array![-1.0, 0.0, 1.0])
45            }
46            ElementType::Quad4 => {
47                // dN/ds
48                let dns = array![
49                    0.25 * (t + 1.0),
50                    -0.25 * (t + 1.0),
51                    0.25 * (t - 1.0),
52                    -0.25 * (t - 1.0)
53                ];
54                // dN/dt
55                let dnt = array![
56                    0.25 * (s + 1.0),
57                    0.25 * (1.0 - s),
58                    0.25 * (s - 1.0),
59                    -0.25 * (s + 1.0)
60                ];
61                (dns, dnt)
62            }
63        }
64    }
65
66    /// Compute global coordinates from local coordinates (s, t)
67    pub fn local_to_global(&self, s: f64, t: f64, nodes: &Array2<f64>) -> Array1<f64> {
68        let n = self.shape_functions(s, t);
69        let mut global = Array1::zeros(3);
70
71        for (i, &node_idx) in self.connectivity.iter().enumerate() {
72            for j in 0..3 {
73                global[j] += n[i] * nodes[[node_idx, j]];
74            }
75        }
76
77        global
78    }
79
80    /// Compute unit normal vector and Jacobian at local coordinates (s, t)
81    ///
82    /// Returns (normal, jacobian) where jacobian is the surface element dS.
83    pub fn normal_and_jacobian(&self, s: f64, t: f64, nodes: &Array2<f64>) -> (Array1<f64>, f64) {
84        let (dns, dnt) = self.shape_derivatives(s, t);
85
86        // Compute dx/ds and dx/dt
87        let mut dxds = Array1::zeros(3);
88        let mut dxdt = Array1::zeros(3);
89
90        for (i, &node_idx) in self.connectivity.iter().enumerate() {
91            for j in 0..3 {
92                dxds[j] += dns[i] * nodes[[node_idx, j]];
93                dxdt[j] += dnt[i] * nodes[[node_idx, j]];
94            }
95        }
96
97        // Normal = dxds × dxdt
98        let normal = cross_product(&dxds, &dxdt);
99        let length = normal.dot(&normal).sqrt();
100
101        if length > 1e-15 {
102            (normal / length, length)
103        } else {
104            (Array1::zeros(3), 0.0)
105        }
106    }
107}
108
109/// Cross product of two 3D vectors
110pub fn cross_product(a: &Array1<f64>, b: &Array1<f64>) -> Array1<f64> {
111    array![
112        a[1] * b[2] - a[2] * b[1],
113        a[2] * b[0] - a[0] * b[2],
114        a[0] * b[1] - a[1] * b[0]
115    ]
116}
117
118/// Dot product of two 3D vectors
119pub fn dot_product(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
120    a.dot(b)
121}
122
123/// Normalize a 3D vector
124pub fn normalize(v: &Array1<f64>) -> (Array1<f64>, f64) {
125    let len = v.dot(v).sqrt();
126    if len > 1e-15 {
127        (v / len, len)
128    } else {
129        (Array1::zeros(3), 0.0)
130    }
131}
132
133/// Compute element center (centroid)
134pub fn compute_element_center(nodes: &Array2<f64>, connectivity: &[usize]) -> Array1<f64> {
135    let n = connectivity.len();
136    let mut center = Array1::zeros(3);
137
138    for &node_idx in connectivity {
139        for j in 0..3 {
140            center[j] += nodes[[node_idx, j]];
141        }
142    }
143
144    center / n as f64
145}
146
147/// Compute element area
148pub fn compute_element_area(
149    nodes: &Array2<f64>,
150    connectivity: &[usize],
151    element_type: ElementType,
152) -> f64 {
153    match element_type {
154        ElementType::Tri3 => {
155            // Triangle area = 0.5 * |v1 × v2|
156            let p0 = nodes.row(connectivity[0]);
157            let p1 = nodes.row(connectivity[1]);
158            let p2 = nodes.row(connectivity[2]);
159
160            let v1 = array![p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
161            let v2 = array![p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
162
163            let cross = cross_product(&v1, &v2);
164            0.5 * cross.dot(&cross).sqrt()
165        }
166        ElementType::Quad4 => {
167            // Quad area via average of both diagonal splits
168            // This handles warped quads correctly
169            let p0 = nodes.row(connectivity[0]);
170            let p1 = nodes.row(connectivity[1]);
171            let p2 = nodes.row(connectivity[2]);
172            let p3 = nodes.row(connectivity[3]);
173
174            // Triangle 0-1-2 (split along diagonal 0-2)
175            let v1 = array![p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
176            let v2 = array![p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
177            let cross1 = cross_product(&v1, &v2);
178            let area1 = 0.5 * cross1.dot(&cross1).sqrt();
179
180            // Triangle 0-2-3 (completing the quad)
181            let v3 = array![p3[0] - p0[0], p3[1] - p0[1], p3[2] - p0[2]];
182            let cross2 = cross_product(&v2, &v3);
183            let area2 = 0.5 * cross2.dot(&cross2).sqrt();
184
185            // Triangle 1-2-3 (split along diagonal 1-3)
186            let v4 = array![p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
187            let v5 = array![p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
188            let cross3 = cross_product(&v4, &v5);
189            let area3 = 0.5 * cross3.dot(&cross3).sqrt();
190
191            // Triangle 1-3-0
192            let v6 = array![p0[0] - p1[0], p0[1] - p1[1], p0[2] - p1[2]];
193            let cross4 = cross_product(&v5, &v6);
194            let area4 = 0.5 * cross4.dot(&cross4).sqrt();
195
196            // Average of both splits for better accuracy on warped quads
197            0.5 * (area1 + area2 + area3 + area4)
198        }
199    }
200}
201
202/// Compute element normal at center
203pub fn compute_element_normal(
204    nodes: &Array2<f64>,
205    connectivity: &[usize],
206    element_type: ElementType,
207) -> Array1<f64> {
208    match element_type {
209        ElementType::Tri3 => {
210            let p0 = nodes.row(connectivity[0]);
211            let p1 = nodes.row(connectivity[1]);
212            let p2 = nodes.row(connectivity[2]);
213
214            let v1 = array![p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
215            let v2 = array![p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
216
217            let cross = cross_product(&v1, &v2);
218            let (normal, _) = normalize(&cross);
219            normal
220        }
221        ElementType::Quad4 => {
222            // Use diagonals for quad
223            let p0 = nodes.row(connectivity[0]);
224            let p2 = nodes.row(connectivity[2]);
225            let p1 = nodes.row(connectivity[1]);
226            let p3 = nodes.row(connectivity[3]);
227
228            let d1 = array![p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
229            let d2 = array![p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
230
231            let cross = cross_product(&d1, &d2);
232            let (normal, _) = normalize(&cross);
233            normal
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::core::types::{BoundaryCondition, ElementProperty};
242    use num_complex::Complex64;
243
244    fn make_test_triangle() -> (Element, Array2<f64>) {
245        let nodes =
246            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])
247                .unwrap();
248
249        let element = Element {
250            connectivity: vec![0, 1, 2],
251            element_type: ElementType::Tri3,
252            property: ElementProperty::Surface,
253            normal: array![0.0, 0.0, 1.0],
254            node_normals: Array2::zeros((3, 3)),
255            center: array![1.0 / 3.0, 1.0 / 3.0, 0.0],
256            area: 0.5,
257            boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)]),
258            group: 0,
259            dof_addresses: vec![0],
260        };
261
262        (element, nodes)
263    }
264
265    #[test]
266    fn test_triangle_shape_functions() {
267        let (element, _nodes) = make_test_triangle();
268
269        // At vertex 0 (s=0, t=0) -> N = [1, 0, 0]
270        let n = element.shape_functions(0.0, 0.0);
271        assert!((n[0] - 1.0).abs() < 1e-10);
272        assert!(n[1].abs() < 1e-10);
273        assert!(n[2].abs() < 1e-10);
274
275        // At vertex 1 (s=1, t=0) -> N = [0, 1, 0]
276        let n = element.shape_functions(1.0, 0.0);
277        assert!(n[0].abs() < 1e-10);
278        assert!((n[1] - 1.0).abs() < 1e-10);
279        assert!(n[2].abs() < 1e-10);
280
281        // At center (s=1/3, t=1/3) -> N = [1/3, 1/3, 1/3]
282        let n = element.shape_functions(1.0 / 3.0, 1.0 / 3.0);
283        assert!((n[0] - 1.0 / 3.0).abs() < 1e-10);
284        assert!((n[1] - 1.0 / 3.0).abs() < 1e-10);
285        assert!((n[2] - 1.0 / 3.0).abs() < 1e-10);
286    }
287
288    #[test]
289    fn test_triangle_local_to_global() {
290        let (element, nodes) = make_test_triangle();
291
292        // At vertex 0 (s=0, t=0) -> node 0 at (0,0,0)
293        let p = element.local_to_global(0.0, 0.0, &nodes);
294        assert!((p[0] - 0.0).abs() < 1e-10);
295        assert!((p[1] - 0.0).abs() < 1e-10);
296
297        // At vertex 1 (s=1, t=0) -> node 1 at (1,0,0)
298        let p = element.local_to_global(1.0, 0.0, &nodes);
299        assert!((p[0] - 1.0).abs() < 1e-10);
300        assert!((p[1] - 0.0).abs() < 1e-10);
301
302        // At vertex 2 (s=0, t=1) -> node 2 at (0,1,0)
303        let p = element.local_to_global(0.0, 1.0, &nodes);
304        assert!((p[0] - 0.0).abs() < 1e-10);
305        assert!((p[1] - 1.0).abs() < 1e-10);
306    }
307
308    #[test]
309    fn test_triangle_normal() {
310        let (element, nodes) = make_test_triangle();
311
312        let (normal, jacobian) = element.normal_and_jacobian(0.5, 0.25, &nodes);
313
314        // Normal should be (0, 0, 1) for this XY-plane triangle
315        assert!((normal[0]).abs() < 1e-10);
316        assert!((normal[1]).abs() < 1e-10);
317        assert!((normal[2] - 1.0).abs() < 1e-10);
318
319        // Jacobian should be constant for linear triangle = 2 * area = 1
320        assert!((jacobian - 1.0).abs() < 1e-10);
321    }
322
323    #[test]
324    fn test_element_area() {
325        let nodes =
326            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])
327                .unwrap();
328
329        let area = compute_element_area(&nodes, &[0, 1, 2], ElementType::Tri3);
330        assert!((area - 0.5).abs() < 1e-10);
331    }
332}