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
CI / Format Check (push) Canceled after 0s
Lifts the 27 `#[ignore]` markers on rtx-cfd and rtx-fea. 21 of them fail;
6 were stale, marking components that have since been implemented. The
suite now reports the truth, which means it is red.
The eigensolver had three independent defects, each individually fatal.
Found by writing closed-form tests first and confirming red:
- The generalized reduction formed M^-1 K and ran Lanczos on it.
M^-1 K has the right eigenvalues but is not symmetric even when K
and M both are, and Lanczos assumes symmetry -- so it returned a
wrong answer rather than an inaccurate one. On a 2-DOF spring-mass
chain with M = diag(2,1) it gave 1.633 against an exact root of
1 - sqrt(2)/2 ~= 0.293. Replaced with the Cholesky reduction
B = L^-1 (K - sigma M) L^-T.
- Output was unsorted. nalgebra's symmetric_eigen gives no ordering
guarantee and none was imposed; modal analysis names modes by index,
so the ordering is part of the contract.
- Eigenvectors could not be transformed back out of the Krylov basis.
The Lanczos block was (n x num_iter) and the tridiagonal
eigenvectors (min(num_iter, k) x k); whenever those differed the
multiply panicked on a dimension mismatch -- that is, on every
problem with more DOFs than requested modes, which is every real
modal analysis.
Lanczos now runs shift-invert by default. Plain Lanczos converges to the
eigenvalues of largest magnitude and modal analysis wants the lowest, so
without it the solver returns the modes nobody asked for. Also switched
to full reorthogonalization, twice per step, so converged eigenvalues do
not reappear as ghosts indistinguishable from genuine repeated roots.
ModalResults computed f = sqrt(lambda / 2pi) instead of
sqrt(lambda) / 2pi. The two agree only at lambda = 2pi, so a smoke test
asserting a positive frequency would never separate them. A
`#[cfg(disabled)]` module in the same file asserted the correct formula
-- the module was disabled rather than the bug fixed. That module is
removed; tests/eigenvalue_closed_form.rs supersedes it with every
expected value derived analytically.
Corrected a fixture rather than loosening its tolerance:
implementation_tests expected the smallest eigenvalue of
tridiag(-1, 4, -1) at order 3 to be 4 - 2 sqrt(2) ~= 1.172. The
eigenvalues of tridiag(c, a, c) are a + 2c cos(k pi / (n+1)), so the
true value is 4 - sqrt(2) ~= 2.586. The test had been quarantined for
failing to match an expectation that was never right.
rtx-fsi is untouched and stays 26/26.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
126 lines
4.2 KiB
Rust
126 lines
4.2 KiB
Rust
//! TDD Tests for Quadrilateral9 FiniteElement Implementation
|
|
//! Following strict Red-Green-Refactor cycle
|
|
//! No mocks, stubs, or TODOs - only full implementations
|
|
|
|
#[cfg(test)]
|
|
mod quadrilateral9_tests {
|
|
use nalgebra::Vector3;
|
|
use rtx_fea::elements::shape_functions::shape_2d::Quadrilateral9;
|
|
use rtx_fea::elements::{FiniteElement, NaturalCoords};
|
|
use rtx_fea::mesh::ElementType;
|
|
|
|
#[test]
|
|
fn test_quad9_constructor() {
|
|
// RED: Test that Quadrilateral9 can be constructed
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
// GREEN: Constructor should create valid instance
|
|
assert_eq!(quad9.num_nodes(), 9);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quad9_element_type() {
|
|
// RED: Test that Quadrilateral9 implements FiniteElement and returns correct type
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
// GREEN: Quadrilateral9 should return ElementType::Quad9
|
|
assert_eq!(quad9.element_type(), ElementType::Quad9);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quad9_num_nodes() {
|
|
// RED: Test that Quadrilateral9 correctly reports number of nodes
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
// GREEN: Quadrilateral9 has 9 nodes (4 corners + 4 mid-edges + 1 center)
|
|
assert_eq!(quad9.num_nodes(), 9);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quad9_dimensions() {
|
|
// RED: Test spatial and parametric dimensions
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
// GREEN: Quadrilateral9 is 2D element with 2D parametric space
|
|
assert_eq!(quad9.spatial_dimension(), 2);
|
|
assert_eq!(quad9.parametric_dimension(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quad9_shape_functions_at_corner() {
|
|
// RED: Test shape function evaluation at corner node
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
// Test at corner (-1,-1) - should be 1 at node 0, 0 at others
|
|
let coords = NaturalCoords::new_2d(-1.0, -1.0);
|
|
let shape = quad9.shape_functions(&coords).unwrap();
|
|
|
|
// GREEN: Verify shape function properties
|
|
assert!((shape.value(0).unwrap() - 1.0).abs() < 1e-10);
|
|
for i in 1..9 {
|
|
assert!(shape.value(i).unwrap().abs() < 1e-10);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_quad9_partition_of_unity() {
|
|
// RED: Test that shape functions sum to 1 everywhere
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
let test_points = vec![
|
|
(0.0, 0.0), // Center
|
|
(0.5, 0.5), // Random point
|
|
(-0.5, 0.3), // Another random point
|
|
];
|
|
|
|
for (x, y) in test_points {
|
|
let coords = NaturalCoords::new_2d(x, y);
|
|
let shape = quad9.shape_functions(&coords).unwrap();
|
|
|
|
// GREEN: Sum of all shape functions should be 1
|
|
let sum: f64 = (0..9).map(|i| shape.value(i).unwrap()).sum();
|
|
assert!((sum - 1.0).abs() < 1e-10);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_quad9_jacobian() {
|
|
// RED: Test Jacobian computation
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
// Create a regular quad with side length 2
|
|
let node_coords = vec![
|
|
Vector3::new(-1.0, -1.0, 0.0), // Corner nodes
|
|
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(0.0, -1.0, 0.0), // Mid-edge nodes
|
|
Vector3::new(1.0, 0.0, 0.0),
|
|
Vector3::new(0.0, 1.0, 0.0),
|
|
Vector3::new(-1.0, 0.0, 0.0),
|
|
Vector3::new(0.0, 0.0, 0.0), // Center node
|
|
];
|
|
|
|
let coords = NaturalCoords::new_2d(0.0, 0.0);
|
|
let jac = quad9.jacobian(&coords, &node_coords).unwrap();
|
|
|
|
// GREEN: For regular quad, Jacobian at center should be diagonal
|
|
assert!(jac.determinant.abs() > 1e-10); // Non-singular
|
|
assert!((jac.determinant - 1.0).abs() < 1e-10); // Unit determinant for unit square
|
|
}
|
|
|
|
#[test]
|
|
fn test_quad9_quadrature() {
|
|
// RED: Test quadrature rule generation
|
|
let quad9 = Quadrilateral9::new();
|
|
|
|
// GREEN: Get default quadrature rule
|
|
let quad_rule = quad9.quadrature_rule(None).unwrap();
|
|
assert!(quad_rule.points.len() > 0);
|
|
|
|
// For order 3, should have at least 9 points (3x3)
|
|
let quad_rule_3 = quad9.quadrature_rule(Some(3)).unwrap();
|
|
assert_eq!(quad_rule_3.points.len(), 9);
|
|
}
|
|
}
|