//! Invariants every finite element shape function set must satisfy. //! //! These hold for any correct element regardless of order or geometry, so a //! failure localises to the basis itself rather than to an accuracy budget. //! Checking them across the whole element library at once is what catches an //! element that was written but never exercised — `Wedge15` summed to 2 at //! mid-height and `Quadrilateral9` emitted its functions in a node order no //! other element in this crate uses. //! //! Two invariants, and the second is the one that gets skipped: //! //! 1. **Partition of unity**: `Σ Nᵢ(ξ) = 1` everywhere. An interpolation that //! does not reproduce a constant cannot represent rigid-body motion. //! 2. **Vanishing derivative sum**: `Σ ∂Nᵢ/∂ξⱼ = 0` everywhere. This follows //! from the first by differentiation, but the derivatives are usually //! written out by hand separately from the values, so they can drift apart //! — and when they do, the shape functions are consistent while the strain //! they produce is not. use rtx_fea::elements::shape_functions::{ Hexahedron8, Hexahedron20, Pyramid5, Pyramid13, Quadrilateral4, Quadrilateral8, Quadrilateral9, ShapeFunctions, Tetrahedron4, Tetrahedron10, Triangle3, Triangle6, Wedge6, Wedge15, }; /// Sample points inside the reference domain of a quadrilateral or hexahedron, /// which spans `[-1, 1]` in each direction. const CUBE_SAMPLES: &[[f64; 3]] = &[ [0.0, 0.0, 0.0], [0.5, -0.25, 0.75], [-0.9, 0.9, -0.3], [0.33, 0.67, 0.1], [1.0, -1.0, 1.0], ]; /// Sample points inside a simplex reference domain, where the coordinates are /// non-negative and sum to at most one. const SIMPLEX_SAMPLES: &[[f64; 3]] = &[ [0.25, 0.25, 0.25], [0.1, 0.2, 0.3], [0.0, 0.0, 0.0], [1.0 / 3.0, 1.0 / 3.0, 0.0], [0.5, 0.25, 0.125], ]; /// Wedges are a triangle in `(r, s)` extruded over `t ∈ [-1, 1]`, so they need /// their own sample set. `t = 0` is included deliberately: it is where a /// missing vertical mid-edge correction shows up most strongly. const WEDGE_SAMPLES: &[[f64; 3]] = &[ [1.0 / 3.0, 1.0 / 3.0, 0.0], [0.25, 0.25, 0.0], [0.2, 0.3, -0.6], [0.5, 0.1, 0.8], [0.0, 0.0, -1.0], ]; fn check_element(name: &str, element: &S, samples: &[[f64; 3]]) { let expected_nodes = element.num_nodes(); for point in samples { let xi = &point[..]; let values = element .evaluate(xi) .unwrap_or_else(|e| panic!("{name}: evaluate failed at {point:?}: {e}")); assert_eq!( values.len(), expected_nodes, "{name}: returned {} values for {expected_nodes} nodes", values.len() ); let sum: f64 = values.iter().sum(); assert!( (sum - 1.0).abs() < 1e-10, "{name}: shape functions sum to {sum} at {point:?}, not 1 — \ the element cannot reproduce a constant field" ); let derivatives = element .derivatives(xi) .unwrap_or_else(|e| panic!("{name}: derivatives failed at {point:?}: {e}")); assert_eq!( derivatives.nrows(), expected_nodes, "{name}: derivative matrix has {} rows for {expected_nodes} nodes", derivatives.nrows() ); for direction in 0..derivatives.ncols() { let column_sum: f64 = derivatives.column(direction).iter().sum(); assert!( column_sum.abs() < 1e-10, "{name}: d/d(xi_{direction}) of the shape functions sums to \ {column_sum} at {point:?}, not 0 — the derivatives are not \ those of the values" ); } } } #[test] fn triangles_satisfy_the_invariants() { check_element("Triangle3", &Triangle3, SIMPLEX_SAMPLES); check_element("Triangle6", &Triangle6, SIMPLEX_SAMPLES); } #[test] fn quadrilaterals_satisfy_the_invariants() { check_element("Quadrilateral4", &Quadrilateral4, CUBE_SAMPLES); check_element("Quadrilateral8", &Quadrilateral8, CUBE_SAMPLES); check_element("Quadrilateral9", &Quadrilateral9, CUBE_SAMPLES); } #[test] fn tetrahedra_satisfy_the_invariants() { check_element("Tetrahedron4", &Tetrahedron4, SIMPLEX_SAMPLES); check_element("Tetrahedron10", &Tetrahedron10, SIMPLEX_SAMPLES); } #[test] fn hexahedra_satisfy_the_invariants() { check_element("Hexahedron8", &Hexahedron8, CUBE_SAMPLES); check_element("Hexahedron20", &Hexahedron20, CUBE_SAMPLES); } #[test] fn wedges_satisfy_the_invariants() { check_element("Wedge6", &Wedge6, WEDGE_SAMPLES); check_element("Wedge15", &Wedge15, WEDGE_SAMPLES); } #[test] fn pyramid5_satisfies_the_invariants() { check_element("Pyramid5", &Pyramid5, SIMPLEX_SAMPLES); } /// `Pyramid13` is not implemented and must say so rather than return values. /// /// It previously returned a basis summing to 4 at the element centre, and its /// derivative routine indexed past the end of the matrix it had allocated. /// Reporting the gap is the honest behaviour until a correct rational basis /// and a matching pyramid quadrature rule both exist. #[test] fn pyramid13_reports_that_it_is_unimplemented() { let error = Pyramid13 .evaluate(&[0.0, 0.0, 0.0]) .expect_err("Pyramid13 must not return shape function values"); assert!( error.to_string().to_lowercase().contains("not implemented"), "error should say the element is unimplemented, got: {error}" ); let error = Pyramid13 .derivatives(&[0.0, 0.0, 0.0]) .expect_err("Pyramid13 must not return shape function derivatives"); assert!(error.to_string().to_lowercase().contains("not implemented")); }