//! Finite Element Helpers for Structural Analysis //! //! This module provides element types, shape functions, and numerical //! integration routines for finite element analysis. use structural_shared::{Element2D, Element3D, Point2D, Point3D}; // ============================================================================ // Element Trait // ============================================================================ /// Trait for finite element shape functions. pub trait Element { /// Number of nodes in the element. fn num_nodes(&self) -> usize; /// Evaluate shape functions at natural coordinates. fn shape_functions(&self, xi: &[f64]) -> Vec; /// Evaluate shape function derivatives at natural coordinates. /// Returns (dN/d_xi, dN/d_eta, ...) for each node. fn shape_derivatives(&self, xi: &[f64]) -> Vec>; /// Get the number of spatial dimensions. fn dimensions(&self) -> usize; } // ============================================================================ // Triangle Element (3-node) // ============================================================================ /// Linear triangle element (3 nodes). #[derive(Debug, Clone, Copy)] pub struct Triangle { /// Node coordinates. nodes: [Point2D; 3], } impl Triangle { /// Create a new triangle element. pub fn new(nodes: [Point2D; 3]) -> Self { Self { nodes } } /// Create from connectivity and vertex list. pub fn from_connectivity(connectivity: &[usize; 3], vertices: &[Point2D]) -> Self { Self { nodes: [ vertices[connectivity[0]], vertices[connectivity[1]], vertices[connectivity[2]], ], } } /// Compute element area. pub fn area(&self) -> f64 { let [n0, n1, n2] = self.nodes; 0.5 * ((n1.x - n0.x) * (n2.y - n0.y) - (n2.x - n0.x) * (n1.y - n0.y)).abs() } /// Compute element centroid. pub fn centroid(&self) -> Point2D { let [n0, n1, n2] = self.nodes; Point2D::new((n0.x + n1.x + n2.x) / 3.0, (n0.y + n1.y + n2.y) / 3.0) } /// Map natural coordinates to physical coordinates. pub fn map_to_physical(&self, xi: f64, eta: f64) -> Point2D { let n = self.shape_functions(&[xi, eta]); let [n0, n1, n2] = self.nodes; Point2D::new( n[0] * n0.x + n[1] * n1.x + n[2] * n2.x, n[0] * n0.y + n[1] * n1.y + n[2] * n2.y, ) } /// Compute Jacobian matrix. pub fn jacobian(&self) -> [[f64; 2]; 2] { let [n0, n1, n2] = self.nodes; // For linear triangle, Jacobian is constant [[n1.x - n0.x, n2.x - n0.x], [n1.y - n0.y, n2.y - n0.y]] } /// Compute Jacobian determinant. pub fn jacobian_det(&self) -> f64 { let j = self.jacobian(); j[0][0] * j[1][1] - j[0][1] * j[1][0] } /// Get nodes. pub fn nodes(&self) -> &[Point2D; 3] { &self.nodes } } impl Element for Triangle { fn num_nodes(&self) -> usize { 3 } fn shape_functions(&self, xi: &[f64]) -> Vec { // Natural coordinates for triangle: xi (L2), eta (L3) // L1 = 1 - xi - eta, L2 = xi, L3 = eta let (xi_val, eta_val) = (xi[0], xi[1]); vec![1.0 - xi_val - eta_val, xi_val, eta_val] } fn shape_derivatives(&self, _xi: &[f64]) -> Vec> { // dN/dxi, dN/deta for each node // N1 = 1 - xi - eta: dN1/dxi = -1, dN1/deta = -1 // N2 = xi: dN2/dxi = 1, dN2/deta = 0 // N3 = eta: dN3/dxi = 0, dN3/deta = 1 vec![ vec![-1.0, -1.0], // Node 1 vec![1.0, 0.0], // Node 2 vec![0.0, 1.0], // Node 3 ] } fn dimensions(&self) -> usize { 2 } } // ============================================================================ // Quadrilateral Element (4-node) // ============================================================================ /// Bilinear quadrilateral element (4 nodes). #[derive(Debug, Clone, Copy)] pub struct Quad { /// Node coordinates (counter-clockwise ordering). nodes: [Point2D; 4], } impl Quad { /// Create a new quad element. pub fn new(nodes: [Point2D; 4]) -> Self { Self { nodes } } /// Create from connectivity and vertex list. pub fn from_connectivity(connectivity: &[usize; 4], vertices: &[Point2D]) -> Self { Self { nodes: [ vertices[connectivity[0]], vertices[connectivity[1]], vertices[connectivity[2]], vertices[connectivity[3]], ], } } /// Compute approximate element area. pub fn area(&self) -> f64 { let [n0, n1, n2, n3] = self.nodes; // Area = 0.5 * |diagonal1 x diagonal2| 0.5 * ((n2.x - n0.x) * (n3.y - n1.y) - (n3.x - n1.x) * (n2.y - n0.y)).abs() } /// Compute element centroid. pub fn centroid(&self) -> Point2D { let [n0, n1, n2, n3] = self.nodes; Point2D::new( (n0.x + n1.x + n2.x + n3.x) / 4.0, (n0.y + n1.y + n2.y + n3.y) / 4.0, ) } /// Map natural coordinates to physical coordinates. pub fn map_to_physical(&self, xi: f64, eta: f64) -> Point2D { let n = self.shape_functions(&[xi, eta]); let nodes = &self.nodes; Point2D::new( n[0] * nodes[0].x + n[1] * nodes[1].x + n[2] * nodes[2].x + n[3] * nodes[3].x, n[0] * nodes[0].y + n[1] * nodes[1].y + n[2] * nodes[2].y + n[3] * nodes[3].y, ) } /// Compute Jacobian matrix at natural coordinates. pub fn jacobian(&self, xi: f64, eta: f64) -> [[f64; 2]; 2] { let dn = self.shape_derivatives(&[xi, eta]); let nodes = &self.nodes; let mut j = [[0.0; 2]; 2]; for i in 0..4 { j[0][0] += dn[i][0] * nodes[i].x; j[0][1] += dn[i][0] * nodes[i].y; j[1][0] += dn[i][1] * nodes[i].x; j[1][1] += dn[i][1] * nodes[i].y; } j } /// Compute Jacobian determinant at natural coordinates. pub fn jacobian_det(&self, xi: f64, eta: f64) -> f64 { let j = self.jacobian(xi, eta); j[0][0] * j[1][1] - j[0][1] * j[1][0] } /// Get nodes. pub fn nodes(&self) -> &[Point2D; 4] { &self.nodes } } impl Element for Quad { fn num_nodes(&self) -> usize { 4 } fn shape_functions(&self, xi: &[f64]) -> Vec { // Bilinear shape functions on [-1, 1] x [-1, 1] let (xi_val, eta_val) = (xi[0], xi[1]); vec![ 0.25 * (1.0 - xi_val) * (1.0 - eta_val), // N1 at (-1, -1) 0.25 * (1.0 + xi_val) * (1.0 - eta_val), // N2 at (1, -1) 0.25 * (1.0 + xi_val) * (1.0 + eta_val), // N3 at (1, 1) 0.25 * (1.0 - xi_val) * (1.0 + eta_val), // N4 at (-1, 1) ] } fn shape_derivatives(&self, xi: &[f64]) -> Vec> { let (xi_val, eta_val) = (xi[0], xi[1]); vec![ vec![-0.25 * (1.0 - eta_val), -0.25 * (1.0 - xi_val)], // dN1 vec![0.25 * (1.0 - eta_val), -0.25 * (1.0 + xi_val)], // dN2 vec![0.25 * (1.0 + eta_val), 0.25 * (1.0 + xi_val)], // dN3 vec![-0.25 * (1.0 + eta_val), 0.25 * (1.0 - xi_val)], // dN4 ] } fn dimensions(&self) -> usize { 2 } } // ============================================================================ // Tetrahedron Element (4-node) // ============================================================================ /// Linear tetrahedron element (4 nodes). #[derive(Debug, Clone, Copy)] pub struct Tetrahedron { /// Node coordinates. nodes: [Point3D; 4], } impl Tetrahedron { /// Create a new tetrahedron element. pub fn new(nodes: [Point3D; 4]) -> Self { Self { nodes } } /// Create from connectivity and vertex list. pub fn from_connectivity(connectivity: &[usize; 4], vertices: &[Point3D]) -> Self { Self { nodes: [ vertices[connectivity[0]], vertices[connectivity[1]], vertices[connectivity[2]], vertices[connectivity[3]], ], } } /// Compute element volume. pub fn volume(&self) -> f64 { let [n0, n1, n2, n3] = self.nodes; let v10 = (n1.x - n0.x, n1.y - n0.y, n1.z - n0.z); let v20 = (n2.x - n0.x, n2.y - n0.y, n2.z - n0.z); let v30 = (n3.x - n0.x, n3.y - n0.y, n3.z - n0.z); // Volume = |det([v10, v20, v30])| / 6 let det = v10.0 * (v20.1 * v30.2 - v20.2 * v30.1) - v10.1 * (v20.0 * v30.2 - v20.2 * v30.0) + v10.2 * (v20.0 * v30.1 - v20.1 * v30.0); det.abs() / 6.0 } /// Compute element centroid. pub fn centroid(&self) -> Point3D { let [n0, n1, n2, n3] = self.nodes; Point3D::new( (n0.x + n1.x + n2.x + n3.x) / 4.0, (n0.y + n1.y + n2.y + n3.y) / 4.0, (n0.z + n1.z + n2.z + n3.z) / 4.0, ) } /// Get nodes. pub fn nodes(&self) -> &[Point3D; 4] { &self.nodes } } impl Element for Tetrahedron { fn num_nodes(&self) -> usize { 4 } fn shape_functions(&self, xi: &[f64]) -> Vec { // Natural coordinates: xi (L2), eta (L3), zeta (L4) // L1 = 1 - xi - eta - zeta let (xi_val, eta_val, zeta_val) = (xi[0], xi[1], xi[2]); vec![1.0 - xi_val - eta_val - zeta_val, xi_val, eta_val, zeta_val] } fn shape_derivatives(&self, _xi: &[f64]) -> Vec> { // dN/dxi, dN/deta, dN/dzeta for each node vec![ vec![-1.0, -1.0, -1.0], // Node 1 vec![1.0, 0.0, 0.0], // Node 2 vec![0.0, 1.0, 0.0], // Node 3 vec![0.0, 0.0, 1.0], // Node 4 ] } fn dimensions(&self) -> usize { 3 } } // ============================================================================ // Gauss Quadrature // ============================================================================ /// Gauss quadrature points and weights. #[derive(Debug, Clone)] pub struct GaussQuadrature { /// Quadrature points (natural coordinates). pub points: Vec>, /// Quadrature weights. pub weights: Vec, } impl GaussQuadrature { /// Create 1-point quadrature for triangles. pub fn triangle_1() -> Self { Self { points: vec![vec![1.0 / 3.0, 1.0 / 3.0]], weights: vec![0.5], // Area of reference triangle } } /// Create 3-point quadrature for triangles. pub fn triangle_3() -> Self { Self { points: vec![ vec![1.0 / 6.0, 1.0 / 6.0], vec![2.0 / 3.0, 1.0 / 6.0], vec![1.0 / 6.0, 2.0 / 3.0], ], weights: vec![1.0 / 6.0; 3], } } /// Create 4-point (2x2) quadrature for quads. pub fn quad_2x2() -> Self { let c = 1.0 / 3.0f64.sqrt(); Self { points: vec![vec![-c, -c], vec![c, -c], vec![c, c], vec![-c, c]], weights: vec![1.0; 4], } } /// Create 9-point (3x3) quadrature for quads. pub fn quad_3x3() -> Self { let c = (3.0 / 5.0f64).sqrt(); let w1 = 5.0 / 9.0; let w2 = 8.0 / 9.0; let mut points = Vec::with_capacity(9); let mut weights = Vec::with_capacity(9); let coords = [-c, 0.0, c]; let wts = [w1, w2, w1]; for (j, &eta) in coords.iter().enumerate() { for (i, &xi) in coords.iter().enumerate() { points.push(vec![xi, eta]); weights.push(wts[i] * wts[j]); } } Self { points, weights } } /// Create 1-point quadrature for tetrahedra. pub fn tetrahedron_1() -> Self { Self { points: vec![vec![0.25, 0.25, 0.25]], weights: vec![1.0 / 6.0], // Volume of reference tetrahedron } } /// Create 4-point quadrature for tetrahedra. pub fn tetrahedron_4() -> Self { let a = (5.0 - 5.0f64.sqrt()) / 20.0; let b = (5.0 + 3.0 * 5.0f64.sqrt()) / 20.0; Self { points: vec![vec![a, a, a], vec![b, a, a], vec![a, b, a], vec![a, a, b]], weights: vec![1.0 / 24.0; 4], } } /// Number of integration points. pub fn num_points(&self) -> usize { self.points.len() } /// Integrate a function over the element. pub fn integrate(&self, func: F) -> f64 where F: Fn(&[f64]) -> f64, { let mut result = 0.0; for (i, point) in self.points.iter().enumerate() { result += self.weights[i] * func(point); } result } } // ============================================================================ // B-Matrix (Strain-Displacement) // ============================================================================ /// Compute B-matrix (strain-displacement) for 2D elements. pub fn compute_b_matrix_2d( shape_derivatives: &[Vec], jacobian_inv: &[[f64; 2]; 2], ) -> Vec> { let num_nodes = shape_derivatives.len(); // B-matrix: 3 rows (eps_xx, eps_yy, gamma_xy), 2*num_nodes columns let mut b = vec![vec![0.0; 2 * num_nodes]; 3]; for i in 0..num_nodes { // Transform derivatives to physical coordinates let dn_dx = jacobian_inv[0][0] * shape_derivatives[i][0] + jacobian_inv[0][1] * shape_derivatives[i][1]; let dn_dy = jacobian_inv[1][0] * shape_derivatives[i][0] + jacobian_inv[1][1] * shape_derivatives[i][1]; // B-matrix entries // eps_xx = du/dx b[0][2 * i] = dn_dx; // eps_yy = dv/dy b[1][2 * i + 1] = dn_dy; // gamma_xy = du/dy + dv/dx b[2][2 * i] = dn_dy; b[2][2 * i + 1] = dn_dx; } b } /// Compute inverse of 2x2 matrix. pub fn invert_2x2(m: &[[f64; 2]; 2]) -> Option<[[f64; 2]; 2]> { let det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; if det.abs() < 1e-15 { return None; } let inv_det = 1.0 / det; Some([ [m[1][1] * inv_det, -m[0][1] * inv_det], [-m[1][0] * inv_det, m[0][0] * inv_det], ]) } // ============================================================================ // Element Factory // ============================================================================ /// Create element from Element2D enum. pub fn create_element_2d(elem: &Element2D, vertices: &[Point2D]) -> Box { match elem { Element2D::Triangle(conn) => Box::new(Triangle::from_connectivity(conn, vertices)), Element2D::Quad(conn) => Box::new(Quad::from_connectivity(conn, vertices)), Element2D::Triangle6(_) => { // Simplified: use linear triangle Box::new(Triangle::new([vertices[0], vertices[1], vertices[2]])) } Element2D::Quad8(_) => { // Simplified: use bilinear quad Box::new(Quad::new([ vertices[0], vertices[1], vertices[2], vertices[3], ])) } } } /// Create element from Element3D enum. pub fn create_element_3d(elem: &Element3D, vertices: &[Point3D]) -> Box { match elem { Element3D::Tetrahedron(conn) => Box::new(Tetrahedron::from_connectivity(conn, vertices)), _ => { // Simplified: use linear tetrahedron for other types Box::new(Tetrahedron::new([ vertices[0], vertices[1], vertices[2], vertices[3], ])) } } } // ============================================================================ // Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_triangle_area() { let tri = Triangle::new([ Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0), Point2D::new(0.0, 1.0), ]); assert!((tri.area() - 0.5).abs() < 1e-10); } #[test] fn test_triangle_centroid() { let tri = Triangle::new([ Point2D::new(0.0, 0.0), Point2D::new(3.0, 0.0), Point2D::new(0.0, 3.0), ]); let c = tri.centroid(); assert!((c.x - 1.0).abs() < 1e-10); assert!((c.y - 1.0).abs() < 1e-10); } #[test] fn test_triangle_shape_functions() { let tri = Triangle::new([ Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0), Point2D::new(0.0, 1.0), ]); // At node 1 (0, 0): N1 = 1, N2 = 0, N3 = 0 let n = tri.shape_functions(&[0.0, 0.0]); assert!((n[0] - 1.0).abs() < 1e-10); assert!(n[1].abs() < 1e-10); assert!(n[2].abs() < 1e-10); // At centroid: N1 = N2 = N3 = 1/3 let n = tri.shape_functions(&[1.0 / 3.0, 1.0 / 3.0]); assert!((n[0] - 1.0 / 3.0).abs() < 1e-10); assert!((n[1] - 1.0 / 3.0).abs() < 1e-10); assert!((n[2] - 1.0 / 3.0).abs() < 1e-10); } #[test] fn test_quad_area() { let quad = Quad::new([ Point2D::new(0.0, 0.0), Point2D::new(2.0, 0.0), Point2D::new(2.0, 1.0), Point2D::new(0.0, 1.0), ]); assert!((quad.area() - 2.0).abs() < 1e-10); } #[test] fn test_quad_shape_functions() { let quad = Quad::new([ Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0), Point2D::new(1.0, 1.0), Point2D::new(0.0, 1.0), ]); // At corner (-1, -1): N1 = 1, others = 0 let n = quad.shape_functions(&[-1.0, -1.0]); assert!((n[0] - 1.0).abs() < 1e-10); assert!(n[1].abs() < 1e-10); assert!(n[2].abs() < 1e-10); assert!(n[3].abs() < 1e-10); // At center (0, 0): all = 0.25 let n = quad.shape_functions(&[0.0, 0.0]); for ni in &n { assert!((*ni - 0.25).abs() < 1e-10); } } #[test] fn test_tetrahedron_volume() { let tet = Tetrahedron::new([ Point3D::new(0.0, 0.0, 0.0), Point3D::new(1.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0), Point3D::new(0.0, 0.0, 1.0), ]); assert!((tet.volume() - 1.0 / 6.0).abs() < 1e-10); } #[test] fn test_tetrahedron_shape_functions() { let tet = Tetrahedron::new([ Point3D::new(0.0, 0.0, 0.0), Point3D::new(1.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0), Point3D::new(0.0, 0.0, 1.0), ]); // At node 1 (0, 0, 0): N1 = 1, others = 0 let n = tet.shape_functions(&[0.0, 0.0, 0.0]); assert!((n[0] - 1.0).abs() < 1e-10); assert!(n[1].abs() < 1e-10); assert!(n[2].abs() < 1e-10); assert!(n[3].abs() < 1e-10); } #[test] fn test_gauss_triangle_1() { let quad = GaussQuadrature::triangle_1(); assert_eq!(quad.num_points(), 1); assert!((quad.weights[0] - 0.5).abs() < 1e-10); } #[test] fn test_gauss_triangle_3() { let quad = GaussQuadrature::triangle_3(); assert_eq!(quad.num_points(), 3); let sum: f64 = quad.weights.iter().sum(); assert!((sum - 0.5).abs() < 1e-10); // Sum should equal reference area } #[test] fn test_gauss_quad_2x2() { let quad = GaussQuadrature::quad_2x2(); assert_eq!(quad.num_points(), 4); let sum: f64 = quad.weights.iter().sum(); assert!((sum - 4.0).abs() < 1e-10); // Sum should equal reference area (2x2) } #[test] fn test_gauss_integrate() { let quad = GaussQuadrature::quad_2x2(); // Integrate f(x,y) = 1 over [-1,1]x[-1,1], should give 4 let result = quad.integrate(|_| 1.0); assert!((result - 4.0).abs() < 1e-10); } #[test] fn test_gauss_integrate_polynomial() { let quad = GaussQuadrature::quad_2x2(); // Integrate f(x,y) = x^2 over [-1,1]x[-1,1] // Exact: integral of x^2 from -1 to 1 = 2/3, times 2 (y range) = 4/3 let result = quad.integrate(|xi| xi[0] * xi[0]); assert!((result - 4.0 / 3.0).abs() < 0.1); // 2x2 is exact for degree 3 } #[test] fn test_invert_2x2() { let m = [[2.0, 1.0], [1.0, 2.0]]; let inv = invert_2x2(&m).unwrap(); // Check M * M^-1 = I let i00 = m[0][0] * inv[0][0] + m[0][1] * inv[1][0]; let i11 = m[1][0] * inv[0][1] + m[1][1] * inv[1][1]; assert!((i00 - 1.0).abs() < 1e-10); assert!((i11 - 1.0).abs() < 1e-10); } #[test] fn test_b_matrix() { let tri = Triangle::new([ Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0), Point2D::new(0.0, 1.0), ]); let j = tri.jacobian(); let j_inv = invert_2x2(&j).unwrap(); let dn = tri.shape_derivatives(&[0.0, 0.0]); let b = compute_b_matrix_2d(&dn, &j_inv); assert_eq!(b.len(), 3); // 3 strain components assert_eq!(b[0].len(), 6); // 3 nodes * 2 DOFs } #[test] fn test_element_trait() { let tri = Triangle::new([ Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0), Point2D::new(0.0, 1.0), ]); assert_eq!(tri.num_nodes(), 3); assert_eq!(tri.dimensions(), 2); let tet = Tetrahedron::new([ Point3D::origin(), Point3D::new(1.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0), Point3D::new(0.0, 0.0, 1.0), ]); assert_eq!(tet.num_nodes(), 4); assert_eq!(tet.dimensions(), 3); } }