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 two triangles
168            let p0 = nodes.row(connectivity[0]);
169            let p1 = nodes.row(connectivity[1]);
170            let p2 = nodes.row(connectivity[2]);
171            let p3 = nodes.row(connectivity[3]);
172
173            // Triangle 0-1-2
174            let v1 = array![p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
175            let v2 = array![p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
176            let cross1 = cross_product(&v1, &v2);
177            let area1 = 0.5 * cross1.dot(&cross1).sqrt();
178
179            // Triangle 0-2-3
180            let v3 = array![p3[0] - p0[0], p3[1] - p0[1], p3[2] - p0[2]];
181            let cross2 = cross_product(&v2, &v3);
182            let area2 = 0.5 * cross2.dot(&cross2).sqrt();
183
184            area1 + area2
185        }
186    }
187}
188
189/// Compute element normal at center
190pub fn compute_element_normal(
191    nodes: &Array2<f64>,
192    connectivity: &[usize],
193    element_type: ElementType,
194) -> Array1<f64> {
195    match element_type {
196        ElementType::Tri3 => {
197            let p0 = nodes.row(connectivity[0]);
198            let p1 = nodes.row(connectivity[1]);
199            let p2 = nodes.row(connectivity[2]);
200
201            let v1 = array![p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
202            let v2 = array![p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
203
204            let cross = cross_product(&v1, &v2);
205            let (normal, _) = normalize(&cross);
206            normal
207        }
208        ElementType::Quad4 => {
209            // Use diagonals for quad
210            let p0 = nodes.row(connectivity[0]);
211            let p2 = nodes.row(connectivity[2]);
212            let p1 = nodes.row(connectivity[1]);
213            let p3 = nodes.row(connectivity[3]);
214
215            let d1 = array![p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
216            let d2 = array![p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
217
218            let cross = cross_product(&d1, &d2);
219            let (normal, _) = normalize(&cross);
220            normal
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::core::types::{BoundaryCondition, ElementProperty};
229    use num_complex::Complex64;
230
231    fn make_test_triangle() -> (Element, Array2<f64>) {
232        let nodes =
233            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])
234                .unwrap();
235
236        let element = Element {
237            connectivity: vec![0, 1, 2],
238            element_type: ElementType::Tri3,
239            property: ElementProperty::Surface,
240            normal: array![0.0, 0.0, 1.0],
241            node_normals: Array2::zeros((3, 3)),
242            center: array![1.0 / 3.0, 1.0 / 3.0, 0.0],
243            area: 0.5,
244            boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)]),
245            group: 0,
246            dof_addresses: vec![0],
247        };
248
249        (element, nodes)
250    }
251
252    #[test]
253    fn test_triangle_shape_functions() {
254        let (element, _nodes) = make_test_triangle();
255
256        // At vertex 0 (s=0, t=0) -> N = [1, 0, 0]
257        let n = element.shape_functions(0.0, 0.0);
258        assert!((n[0] - 1.0).abs() < 1e-10);
259        assert!(n[1].abs() < 1e-10);
260        assert!(n[2].abs() < 1e-10);
261
262        // At vertex 1 (s=1, t=0) -> N = [0, 1, 0]
263        let n = element.shape_functions(1.0, 0.0);
264        assert!(n[0].abs() < 1e-10);
265        assert!((n[1] - 1.0).abs() < 1e-10);
266        assert!(n[2].abs() < 1e-10);
267
268        // At center (s=1/3, t=1/3) -> N = [1/3, 1/3, 1/3]
269        let n = element.shape_functions(1.0 / 3.0, 1.0 / 3.0);
270        assert!((n[0] - 1.0 / 3.0).abs() < 1e-10);
271        assert!((n[1] - 1.0 / 3.0).abs() < 1e-10);
272        assert!((n[2] - 1.0 / 3.0).abs() < 1e-10);
273    }
274
275    #[test]
276    fn test_triangle_local_to_global() {
277        let (element, nodes) = make_test_triangle();
278
279        // At vertex 0 (s=0, t=0) -> node 0 at (0,0,0)
280        let p = element.local_to_global(0.0, 0.0, &nodes);
281        assert!((p[0] - 0.0).abs() < 1e-10);
282        assert!((p[1] - 0.0).abs() < 1e-10);
283
284        // At vertex 1 (s=1, t=0) -> node 1 at (1,0,0)
285        let p = element.local_to_global(1.0, 0.0, &nodes);
286        assert!((p[0] - 1.0).abs() < 1e-10);
287        assert!((p[1] - 0.0).abs() < 1e-10);
288
289        // At vertex 2 (s=0, t=1) -> node 2 at (0,1,0)
290        let p = element.local_to_global(0.0, 1.0, &nodes);
291        assert!((p[0] - 0.0).abs() < 1e-10);
292        assert!((p[1] - 1.0).abs() < 1e-10);
293    }
294
295    #[test]
296    fn test_triangle_normal() {
297        let (element, nodes) = make_test_triangle();
298
299        let (normal, jacobian) = element.normal_and_jacobian(0.5, 0.25, &nodes);
300
301        // Normal should be (0, 0, 1) for this XY-plane triangle
302        assert!((normal[0]).abs() < 1e-10);
303        assert!((normal[1]).abs() < 1e-10);
304        assert!((normal[2] - 1.0).abs() < 1e-10);
305
306        // Jacobian should be constant for linear triangle = 2 * area = 1
307        assert!((jacobian - 1.0).abs() < 1e-10);
308    }
309
310    #[test]
311    fn test_element_area() {
312        let nodes =
313            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])
314                .unwrap();
315
316        let area = compute_element_area(&nodes, &[0, 1, 2], ElementType::Tri3);
317        assert!((area - 0.5).abs() < 1e-10);
318    }
319}