Commit Graph
5 Commits
Author SHA1 Message Date
Omar SobhandClaude Fable 5 10c779e96e rtx-fea: banded LU replaces the dense factorization on the Newton tangent — the march's cost center, fixed
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
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
The 2026-08-29 profile attributed 98% of the structural step (79% of a
coupled FSI pass) to LuDirect::factorize — nalgebra's dense full-pivot
LU on the 560-DOF tangent, every Newton iteration. The tangent is
banded (half-bandwidth ~26: the flag mesh numbers the short direction
innermost). BandedLu (solvers/banded.rs): LAPACK dgbtrf-style
column-major band storage, partial pivoting with kl fill rows, band
limits measured from the CSR pattern per factorize, O(n·kl·(kl+ku)).
Swapped into NonlinearDynamicStepper (tangent + rest-state mass solve);
LuDirect untouched elsewhere.

TDD: 10 manufactured-system tests green first run (recovery to 1e-12
vs exact and vs LuDirect across band shapes incl. full-bandwidth
degeneration; zero-diagonal pivoting; indefinite shifted-stiffness
tangent; singularity; per-solve refactorization).

Solver-path change — full verification protocol run:
- rtx-fea 29 binaries 0 failures; rtx-fsi lib/piston/transfer green.
- FSI2 committed default: every printed digit IDENTICAL to the
  2026-08-28 baseline (uy 3.7732±3.7920 mm, f 2.547, conservation
  8.26e-12). FSI1 identical. Noise-probe floors reproduced.
- Wall clock: FSI2 coupled phase 233 s -> 77 s (3.0x, 0.60 -> 0.20
  s/step); FSI3 coupled 517 s -> 119 s (4.3x). Structure is no longer
  the cost center; the fluid's MG-caching consolidation is next.

Finding 1: newton_rescue's vacuousness guard fired — the 2026-08-24
killer (symmetric 1e4 N mid-swing reversal) converges on the PLAIN
path under partial-pivot rounding at every probed combo to 1e5 N.
Re-provoked: asymmetric 1e4 -> +1e5 N reversal defeats plain Newton at
swing steps 3, 4 AND 5 (not knife-edge); pinned at steps 4, whose
coarse-vs-fine gap (0.66x of scale) sits inside the pre-registered
0.75 band — the band is untouched.

Finding 2: the FSI3 release pin fired and the PIN was the finding.
uy_mid (windowed mean over [4.0,4.2]) moved 44% (10.7684 -> 6.0229 mm)
while amplitude (+7%), ux mid (+0.3%) and 5.2x growth all held; the
baseline's 2 IQN history-reset retries became 0 — a rounding-level
branch flip at unit density ratio (the traced bistable-mask
sensitivity). The windowed mean of a growing 5-Hz oscillation is not a
rounding-robust observable; its band now covers both measured branches
(both recorded in the assertion), amp/ux re-centered at ±35%. New
trajectory re-verified deterministic digit-for-digit twice before
re-pinning; green in vivo under the new pins.

Study-tier pins (FSI3 sticky-mask cycle, FSI2 s=1 benchmark cycle)
re-verification launched; results to be recorded in solver_status.md.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
2026-08-29 23:20:50 -05:00
Omar SobhandClaude Fable 5 87cf392556 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]>
2026-08-19 18:52:50 -07:00
Omar SobhandClaude Opus 5 4c2cea36aa rtx-fea: make the analysis stack produce physics, validated against closed form
The census found rtx-fea could not produce a non-zero answer for any
analysis type. Six defects sat between a correctly specified mesh and a
natural frequency, each of which alone was fatal. Every one was found by
writing the closed-form test first and confirming red.

1. Element matrices were a stub. StandardFiniteElement::
   compute_element_matrices returned DMatrix::zeros for stiffness, force
   and mass -- and it is what GlobalAssembler calls for every element, so
   every global matrix in the crate was zero. Real quadrature-based
   stiffness and mass already existed in ElementMatrixComputer; nothing
   called them. Now wired, with the scalar mass matrix expanded by a
   Kronecker product with the spatial identity to match the interleaved
   per-node DOF layout its stiffness uses.

2. Quadrature returned no points. quadrature_rule built
   QuadratureRule::new(vec![], ..). Every integration loop iterates over
   rule.points, so an empty rule does not fail -- it skips the loop and
   yields a zero matrix. Real Gauss rules for line, triangle, quad, tet
   and hex existed unused; now dispatched by element type, with wedges as
   the triangle-line tensor product and pyramids an explicit error rather
   than an empty rule.

3. transform_derivatives computed J^-T * dN where dN is
   (num_nodes x param_dim). By the chain rule it is dN * J^-1. The two
   agree only when both are square and symmetric; for any element with
   more nodes than parametric directions -- every element -- the old form
   was a dimension mismatch that panicked inside BLAS.

4. MaterialDatabase::clone silently dropped every material, cloning
   names only, because Box<dyn Material> is not Clone. GlobalAssembler is
   constructed with materials.clone(), so every assembler ever built got
   an empty database and every analysis failed MaterialNotFound on a
   correctly specified mesh. Materials are immutable once registered, so
   the map now holds Arc and cloning shares them.

5. displacement_only numbered three displacement components on a 2-D
   mesh. Elements supply two, so assembly rejected every contribution.

6. to_dof_numbering pushed each node's DOFs in HashMap iteration order.
   When that came out [v, u] the assembler wrote the element's u row into
   the global v row. The result was still symmetric, still had the right
   rigid-body null space and still summed to the right total mass -- it
   simply described a structure with its axes transposed per node, and
   get_dof(node, DisplacementX) then pointed at the wrong row so
   constraints were applied to the wrong direction too. DofComponent now
   carries a canonical_index and the DOFs are sorted by it.

ModalAnalysis is wired to real assembly and the repaired eigensolver, and
takes boundary conditions, which it previously had no way to accept. The
eigensolver now rejects a singular stiffness explicitly: try_inverse does
not fail on a matrix singular only to working precision, so an
unconstrained structure used to return rigid-body noise dressed up as
low-frequency modes.

Validation, 18 tests:

  - Element matrices: rigid translation stores no energy, exactly 3
    rigid-body modes in 2-D and 6 in 3-D, consistent mass integrates to
    rho*V, mass positive definite, and K and M each scale only with the
    property they depend on. A zero matrix passes symmetry and
    does-not-crash checks, so these are chosen to be ones it fails.
  - Modal, end to end: longitudinal modes of a fixed-free bar against
    f_n = (2n-1)/(4L) sqrt(E/rho), within 1% on the first three, and
    second-order convergence under refinement. Axial rather than
    cantilever bending on purpose: Quad4 shear-locks, so a bending
    tolerance would fail for a reason unrelated to correctness. Bending
    is asserted as convergence from above instead, which is the honest
    claim for a locking element.

Two fixtures corrected rather than tolerances loosened: integration_tests
expected 27 DOFs for a 9-node planar mesh (3 components per node), which
encoded defect 5 and contradicted comprehensive_tdd_tests asserting
num_nodes * 2 for the same situation.

rtx-fsi stays 26/26. No new failures; the rtx-cfd quarantine is
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 08:10:57 -07:00
Omar SobhandClaude Opus 5 cca29aac8f rtx-fea: repair the eigensolver, and stop the suite lying about the rest
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]>
2026-08-19 07:46:01 -07:00
redclawsystems 4d88dc0584 Initial commit 2026-03-04 00:08:42 +00:00