Files
rustytorch/crates/specialized/rtx-fea/tests/hexahedron_finite_element_tdd.rs
T
Omar SobhandClaude Opus 5 e30cfe4ce9
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
rtx-fea: repair the element library; the crate is now green with no quarantine
Follows the assembly repair. Takes rtx-fea from 21 failures to 253 passing,
0 failing, 0 ignored, with every `#[ignore]` marker gone.

Shape function bugs, all found by one new test asserting two invariants
across the whole element library at once -- partition of unity, and that
the hand-written derivatives sum to zero. The second is the one that gets
skipped, and it is what caught Hexahedron20.

  - Wedge15 summed to 2 at mid-height. Adding a node on a vertical edge
    contributes L_i (1 - t^2) to the sum, so the two corners sharing that
    edge must each give up half of it; the correction was absent. A
    quadratic wedge that doubles every field interpolated through it.

  - Hexahedron20 had sign errors in four hand-written corner
    derivatives -- nodes 3 and 7 in dN/dr, nodes 1 and 5 in dN/ds. The
    values were correct, so partition of unity passed; only the
    derivative-sum invariant exposed it. The strain computed from this
    element was wrong while its interpolation looked right.

  - Quadrilateral9 emitted its shape functions in raw lexicographic
    lattice order while Quad4 and Quad8 use the standard finite-element
    order. A mesh written the usual way paired each node with the wrong
    basis function, which at the element centre made the Jacobian exactly
    singular.

  - Pyramid13 was not a quadratic pyramid basis: it summed to 4 at the
    element centre, and its `derivatives` allocated a 13x3 matrix then
    wrote rows 13 through 15, having been copied from a sixteen-node
    layout, so it panicked before the wrong values could be used. A
    correct 13-node basis is rational, and there is no pyramid quadrature
    rule to integrate it with, so implementing the basis alone would not
    make the element usable. Both now report the gap explicitly rather
    than panicking. Pyramid5 is unaffected and works.

Fixtures corrected rather than tolerances loosened:

  - von Mises stress of an equal biaxial state expected 0, commented "no
    deviatoric stress". Only a hydrostatic state has that. The correct
    value is 100, and expecting 0 would mean a biaxially loaded sheet
    could never yield. The unequal case expected |100-50|; the von Mises
    stress is not a principal difference.
  - A 3-point Gauss rule was required to integrate sin to 1e-10. No
    correct implementation can. Replaced with a convergence assertion,
    which a wrong rule cannot satisfy by luck.
  - MathUtils::SMALL was asserted below EPSILON * 1000, which inverts the
    relationship a practical zero-threshold needs.
  - The Hex20 Jacobian test put all twelve mid-edge nodes at the origin,
    commented "simplified for test". That is not a hexahedron, and its
    mapping is genuinely singular; it only passed because of the
    derivative sign errors above.
  - ElementFactory was required to build every ElementType including
    Point, which has no interpolation and is deliberately rejected.

MemoryInfo displayed decimal GB while its own test constructed binary
GiB, rendering an 8 GiB device as 8.59. Now GiB throughout.

test_mesh_has_real_algorithms searched the *text* of mesh/mod.rs for the
strings "add_node" and "add_element". It broke when those moved into
submodules, but the real problem is that a source-text search cannot tell
a working function from one returning zeros -- it passed throughout the
period when element matrices were a stub and quadrature returned no
points. Replaced with a test that builds a mesh and checks the result.

The crate doc example imported solvers::DirectSolver and
analysis::StaticAnalysis, neither of which has ever existed, so the
doctest never compiled. Replaced with a modal analysis that runs. Also
dropped the "Production Ready: No mocks, stubs, or TODOs - complete
implementation" line, and replaced it with what is actually validated and
what is not.

rtx-fsi unaffected at 26/26.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 08:21:58 -07:00

223 lines
7.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
}
}