rtx-fea: re-enable the remaining CPU test modules; fix three real defects they caught
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

All 33 remaining #[cfg(disabled)] test modules outside the GPU cluster are
now enabled: assembly (dof_mapping, constraints, global assembly), boundary
(mod + dirichlet/neumann/robin/thermal/contact), analysis (mod + static),
materials (mod, linear_elastic, hyperelastic, plasticity), elements (mod,
element_matrices, isoparametric, jacobian, quadrature), mesh (element_types,
connectivity, topology, topology_repair), solvers (mod, direct, iterative,
nonlinear) and lib.rs. Lib tests 117 -> 335, stable across repeated runs.
Only gpu_solver_tests and the GpuMeshData fixture stay disabled — they need
CUDA hardware and belong to the GPU tranche.

Three real defects found by the newly-compiling tests, each fixed:

- Direct solvers reused factorizations keyed on matrix SIZE alone.
  In a Newton loop the Jacobian changes every iteration but never its
  dimension, so LuDirect/CholeskyDirect/LdltDirect silently solved with the
  first iteration's factorization forever — Newton on x^2-4 crawled to
  x=1.955 in 1000 iterations instead of converging in 5. Invisible in
  single-solve linear analysis, which is why every green test passed over
  it. solve() now factorizes the matrix it is given.

- AdaptiveQuadrature's refinement re-integrated the WHOLE domain once per
  subdomain, so each level multiplied the estimate by the subdomain count:
  integrating e^x over [-1,1] at tolerance 1e-10 returned ~75 instead of
  2.35. The recursion now descends into each sub-box with its share of the
  error budget.

- compute_skewness read Jacobian columns as coordinate-line tangents, but
  the trait's jacobian() stores tangents in ROWS: on a sheared
  parallelogram whose tangents meet at 14 degrees it reported skewness 0.43
  instead of 0.84 — measuring per-component gradients, not mesh skew.

Fixtures corrected rather than the code where the fixture was wrong:
sigma_yy ~ 0 asserted uniaxial-stress physics on a uniaxial-strain state
(exact Lame values now asserted); an "unstable" orthotropic parameter set
that satisfies the determinant stability condition (delta = 0.187 > 0); a
unit-cube hex Jacobian of 1.0 that assumed a unit reference element (it is
0.125 from [-1,1]^3); a "distorted" quad whose centre Jacobian is exactly
orthogonal, asserted as skewed (flattening and shearing now tested
separately); a quality score below the implementation's own calibration;
Rayleigh damping fed the scalar-field mass (now expanded via the Kronecker
identity, with C = alpha*M + beta*K asserted entry-wise); an element
factory required to construct Point/Line types that have no implementation;
and DOF counts that encoded the repaired 3-DOFs-per-node-on-2-D defect.

MaterialDatabase::add_material call sites updated to the (id, material,
name) signature; ConnectivityInfo::build takes elements only;
TopologyRepair::triangle_quality (normalized 4*sqrt(3)*A/sum(a^2)) added
for the repair tests; create_subdomain_rule_* widened to pub(super) for the
quadrature tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-19 18:52:50 -07:00
co-authored by Claude Fable 5
parent 8495a690d9
commit 87cf392556
31 changed files with 384 additions and 225 deletions
@@ -45,11 +45,6 @@ impl LuDirect {
.into())
}
}
/// Check if we can reuse existing factorization.
fn can_reuse_factorization(&self, matrix: &SparseMatrix) -> bool {
self.factorization.is_some() && self.last_size == Some(matrix.nrows())
}
}
impl Default for LuDirect {
@@ -79,10 +74,13 @@ impl LinearSolver for LuDirect {
.into());
}
// Factorize if needed
if !self.can_reuse_factorization(matrix) {
self.factorize(matrix)?;
}
// Always factorize the matrix we were handed. Reuse used to be
// keyed on SIZE alone, so a Newton loop — where the Jacobian changes
// every iteration but never its dimension — silently solved with the
// first iteration's factorization forever, degrading Newton to a
// stale-Jacobian iteration that crawls or diverges. Callers that
// want deliberate reuse cache the matrix itself (see ModifiedNewton).
self.factorize(matrix)?;
// Solve using cached factorization
let solution = if let Some(ref lu) = self.factorization {
@@ -164,11 +162,6 @@ impl CholeskyDirect {
.into()),
}
}
/// Check if we can reuse existing factorization.
fn can_reuse_factorization(&self, matrix: &SparseMatrix) -> bool {
self.factorization.is_some() && self.last_size == Some(matrix.nrows())
}
}
impl Default for CholeskyDirect {
@@ -198,10 +191,13 @@ impl LinearSolver for CholeskyDirect {
.into());
}
// Factorize if needed
if !self.can_reuse_factorization(matrix) {
self.factorize(matrix)?;
}
// Always factorize the matrix we were handed. Reuse used to be
// keyed on SIZE alone, so a Newton loop — where the Jacobian changes
// every iteration but never its dimension — silently solved with the
// first iteration's factorization forever, degrading Newton to a
// stale-Jacobian iteration that crawls or diverges. Callers that
// want deliberate reuse cache the matrix itself (see ModifiedNewton).
self.factorize(matrix)?;
// Solve using cached factorization
let solution = if let Some(ref chol) = self.factorization {
@@ -347,11 +343,6 @@ impl LdltDirect {
Ok(x)
}
/// Check if we can reuse existing factorization.
fn can_reuse_factorization(&self, matrix: &SparseMatrix) -> bool {
self.factorization_data.is_some() && self.last_size == Some(matrix.nrows())
}
}
impl Default for LdltDirect {
@@ -381,10 +372,13 @@ impl LinearSolver for LdltDirect {
.into());
}
// Factorize if needed
if !self.can_reuse_factorization(matrix) {
self.factorize(matrix)?;
}
// Always factorize the matrix we were handed. Reuse used to be
// keyed on SIZE alone, so a Newton loop — where the Jacobian changes
// every iteration but never its dimension — silently solved with the
// first iteration's factorization forever, degrading Newton to a
// stale-Jacobian iteration that crawls or diverges. Callers that
// want deliberate reuse cache the matrix itself (see ModifiedNewton).
self.factorize(matrix)?;
// Solve using LDLT factorization
let solution = self.solve_ldlt(rhs)?;
@@ -548,7 +542,7 @@ impl LinearSolver for SparseDirect {
}
}
#[cfg(disabled)]
#[cfg(test)]
mod tests {
use super::*;
use crate::assembly::SparseMatrix;
@@ -497,7 +497,7 @@ impl PreconditionerFactory {
}
}
#[cfg(disabled)]
#[cfg(test)]
mod tests {
use super::*;
use crate::assembly::SparseMatrix;
@@ -592,7 +592,7 @@ impl SolverBenchmarker {
}
}
#[cfg(disabled)]
#[cfg(test)]
mod tests {
use super::*;
use crate::assembly::SparseMatrix;
@@ -238,7 +238,7 @@ impl NonlinearSolver for QuasiNewton {
}
}
#[cfg(disabled)]
#[cfg(test)]
mod tests {
use super::*;
use crate::assembly::SparseMatrix;
@@ -292,7 +292,11 @@ mod tests {
assert!(result.is_ok());
let (solution, info) = result.unwrap();
assert!((solution[0] - 2.0).abs() < 0.1); // Should converge to x = 2
// Newton from x=1 on x^2-4 converges quadratically; anything slower
// means the linear solver reused a stale factorization. The default
// options exit on relative residual 1e-6, i.e. |x - 2| < ~1e-6.
assert!((solution[0] - 2.0).abs() < 1e-5);
assert!(info.converged);
assert!(info.iterations <= 10, "took {} iterations", info.iterations);
}
}