//! TDD Tests for Hexahedron FiniteElement Implementations //! Following strict Red-Green-Refactor cycle //! No mocks, stubs, or TODOs - only full implementations #[cfg(test)] mod hexahedron20_tests { use nalgebra::Vector3; use rtx_fea::elements::shape_functions::shape_3d::Hexahedron20; use rtx_fea::elements::{FiniteElement, NaturalCoords}; use rtx_fea::mesh::ElementType; #[test] fn test_hex20_element_type() { // RED: Test that Hexahedron20 implements FiniteElement and returns correct type let hex20 = Hexahedron20::new(); // GREEN: Hexahedron20 should return ElementType::Hex20 assert_eq!(hex20.element_type(), ElementType::Hex20); } #[test] fn test_hex20_num_nodes() { // RED: Test that Hexahedron20 correctly reports number of nodes let hex20 = Hexahedron20::new(); // GREEN: Hexahedron20 has 20 nodes (8 corners + 12 mid-edges) assert_eq!(hex20.num_nodes(), 20); } #[test] fn test_hex20_dimensions() { // RED: Test spatial and parametric dimensions let hex20 = Hexahedron20::new(); // GREEN: Hexahedron20 is 3D element with 3D parametric space assert_eq!(hex20.spatial_dimension(), 3); assert_eq!(hex20.parametric_dimension(), 3); } #[test] fn test_hex20_shape_functions_at_corner() { // RED: Test shape function evaluation at corner node let hex20 = Hexahedron20::new(); // Test at corner (−1,−1,−1) - should be 1 at node 0, 0 at others let coords = NaturalCoords::new_3d(-1.0, -1.0, -1.0); let shape = hex20.shape_functions(&coords).unwrap(); // GREEN: Verify shape function properties assert!((shape.value(0).unwrap() - 1.0).abs() < 1e-10); for i in 1..20 { assert!(shape.value(i).unwrap().abs() < 1e-10); } } #[test] fn test_hex20_jacobian() { // RED: Test Jacobian computation let hex20 = Hexahedron20::new(); // The reference cube spanning [-1, 1] in each direction, in the node // order the shape functions are written in: four bottom corners // counter-clockwise, four top corners, then the bottom, top and // vertical mid-edges. // // This previously listed the corners in lexicographic order and put // *all twelve* mid-edge nodes at the origin, commented "simplified // for test". That is not a degenerate hexahedron so much as not a // hexahedron: collapsing the mid-side nodes to a point makes the // mapping genuinely singular. It only passed because the dN/dr and // dN/ds derivatives carried sign errors at nodes 1, 3, 5 and 7, which // produced a non-zero determinant for a geometry that has none. let node_coords = vec![ // Bottom corners Vector3::new(-1.0, -1.0, -1.0), Vector3::new(1.0, -1.0, -1.0), Vector3::new(1.0, 1.0, -1.0), Vector3::new(-1.0, 1.0, -1.0), // Top corners Vector3::new(-1.0, -1.0, 1.0), Vector3::new(1.0, -1.0, 1.0), Vector3::new(1.0, 1.0, 1.0), Vector3::new(-1.0, 1.0, 1.0), // Bottom mid-edges Vector3::new(0.0, -1.0, -1.0), Vector3::new(1.0, 0.0, -1.0), Vector3::new(0.0, 1.0, -1.0), Vector3::new(-1.0, 0.0, -1.0), // Top mid-edges Vector3::new(0.0, -1.0, 1.0), Vector3::new(1.0, 0.0, 1.0), Vector3::new(0.0, 1.0, 1.0), Vector3::new(-1.0, 0.0, 1.0), // Vertical mid-edges Vector3::new(-1.0, -1.0, 0.0), Vector3::new(1.0, -1.0, 0.0), Vector3::new(1.0, 1.0, 0.0), Vector3::new(-1.0, 1.0, 0.0), ]; let coords = NaturalCoords::new_3d(0.0, 0.0, 0.0); let jac = hex20.jacobian(&coords, &node_coords).unwrap(); // The element occupies its own reference domain, so the mapping is the // identity and the Jacobian determinant is exactly 1. assert!((jac.determinant() - 1.0).abs() < 1e-10); } #[test] fn test_hex20_quadrature() { // RED: Test quadrature rule generation let hex20 = Hexahedron20::new(); // GREEN: Get default quadrature rule let quad_rule = hex20.quadrature_rule(None).unwrap(); assert!(quad_rule.points.len() > 0); // For order 3, should have at least 8 points (2x2x2) let quad_rule_3 = hex20.quadrature_rule(Some(3)).unwrap(); assert!(quad_rule_3.points.len() >= 8); } } #[cfg(test)] mod hexahedron27_tests { use nalgebra::Vector3; use rtx_fea::elements::shape_functions::shape_3d::Hexahedron27; use rtx_fea::elements::{FiniteElement, NaturalCoords}; use rtx_fea::mesh::ElementType; #[test] fn test_hex27_element_type() { // RED: Test that Hexahedron27 implements FiniteElement let hex27 = Hexahedron27::new(); // GREEN: Hexahedron27 should return ElementType::Hex27 assert_eq!(hex27.element_type(), ElementType::Hex27); } #[test] fn test_hex27_num_nodes() { // RED: Test node count let hex27 = Hexahedron27::new(); // GREEN: Hexahedron27 has 27 nodes (8 corners + 12 mid-edges + 6 face-centers + 1 center) assert_eq!(hex27.num_nodes(), 27); } #[test] fn test_hex27_partition_of_unity() { // RED: Test that shape functions sum to 1 everywhere let hex27 = Hexahedron27::new(); let test_points = vec![ (0.0, 0.0, 0.0), // Center (0.5, 0.5, 0.5), // Random point (-0.5, 0.3, -0.7), // Another random point ]; for (x, y, z) in test_points { let coords = NaturalCoords::new_3d(x, y, z); let shape = hex27.shape_functions(&coords).unwrap(); // GREEN: Sum of all shape functions should be 1 let sum: f64 = (0..27).map(|i| shape.value(i).unwrap()).sum(); assert!((sum - 1.0).abs() < 1e-10); } } } #[cfg(test)] mod integration_tests { use rtx_fea::elements::shape_functions::shape_3d::{Hexahedron20, Hexahedron27}; use rtx_fea::elements::{FiniteElement, NaturalCoords}; #[test] fn test_hex20_numerical_integration() { // RED: Test numerical integration over element let hex20 = Hexahedron20::new(); let quad_rule = hex20.quadrature_rule(Some(3)).unwrap(); // Integrate constant function f=1 over reference element [-1,1]^3 let mut integral = 0.0; for point in &quad_rule.points { let coords = NaturalCoords::new_3d(point.coords.xi(), point.coords.eta(), point.coords.zeta()); let _shape = hex20.shape_functions(&coords).unwrap(); integral += 1.0 * point.weight; } // GREEN: Volume of reference hex should be 8 assert!((integral - 8.0).abs() < 1e-10); } #[test] fn test_hex27_derivative_consistency() { // RED: Test that shape function derivatives are consistent let hex27 = Hexahedron27::new(); let test_points = vec![(0.1, 0.2, 0.3), (-0.3, 0.4, -0.2)]; for (x, y, z) in test_points { let coords = NaturalCoords::new_3d(x, y, z); let shape_eval = hex27.shape_functions(&coords).unwrap(); // GREEN: Sum of derivatives should be zero (constant preservation) let mut sum_dxi = 0.0; let mut sum_deta = 0.0; let mut sum_dzeta = 0.0; for i in 0..27 { sum_dxi += shape_eval.derivative(i, 0).unwrap(); sum_deta += shape_eval.derivative(i, 1).unwrap(); sum_dzeta += shape_eval.derivative(i, 2).unwrap(); } assert!(sum_dxi.abs() < 1e-10); assert!(sum_deta.abs() < 1e-10); assert!(sum_dzeta.abs() < 1e-10); } } }