Commit Graph
23 Commits
Author SHA1 Message Date
Omar SobhandClaude Fable 5.1 a2086a59de P6-b: the fictitious added mass for the partitioned loop — rtx-fea NonlinearDynamicStepper::set_added_lumped_mass (a lumped per-DOF mass in the Newmark inertial residual and effective tangent, never the consistent mass or the rest state; zero = the plain stepper bit for bit) with its pin (compensated step reproduces the plain step to 2.5e-9, uncompensated moves it 11 %); the FSI2 overset harness carries RTX_FSI2O_FICT_MASS=α (α × ρ_f π (c/2)² spread over the wetted nodes) and adds the compensating load M_f ü_k of the previous subiterate to every structure solve (predictor and passes), printed in the header
CI / Format Check (push) Failing after 7s
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Clippy Check (push) Failing after 5s
CI / Build (ubuntu-latest) (push) Failing after 5s
Performance Benchmarks / Run Benchmarks (push) Failing after 6s
Documentation / Build User Guide (push) Successful in 6s
Documentation / Build API Documentation (push) Failing after 25s
CI / Build CPU-Only (Explicit) (push) Failing after 1m15s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
2026-09-15 08:30:55 -05:00
Omar SobhandClaude Fable 5.1 e76271ac67 test(rtx-fsi): coupling-level rescue rung A (RTX_FSI{2,3}_CRESCUE, default off) + TRACE_FROM autopsy window
CI / Build CPU-Only (Explicit) (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
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
Documentation / Build API Documentation (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
Coupling-rescue campaign (omni-cortex docs/coupling_rescue_campaign.md).
Every recorded FSI2 s=1 coupling death is the END of a multi-step
runaway (74-march burst scan: green marches never exceed 2.4x their own
p95 tip jump; 27/28 deaths burst 5-37 steps first). The traced autopsy
of the (1.27, 2.0) death (replay digit-identical, CSV byte-identical)
shows a growing period-2 instability of the CONVERGED coupled scheme at
the tip's max-velocity crossing: increment x33 and converged load
1e4 -> 1e6 N over 200 steps with the coupling converging on 111 of the
first 112 steps.

- march.rs: RTX_{prefix}_TRACE_FROM (print-only autopsy window: per-pass
  residual/load + one line per step with increment, tol_step, retry_at,
  acceptable, outcome, committed tip jump); RTX_{prefix}_CRESCUE (default
  off): on a fatal stall or a committed tip jump > 3x the running p95
  (trailing 2000 non-rescued steps), reject the step and repeat the
  interval as 2/4/8/16/32 coupled substeps of dt/n (Mayr-Wall-Gee
  reduced-step repetition, five repetitions); per-rescue record printed,
  MarchResult.coupling_rescues/_failures/rescue_records, rate cap 20
  rescues per second of march (dies loudly).
- rescue.rs (new): the substep ladder — each substep a complete coupled
  step at dt/n with its own predictor, fresh coupler, C1 velocity
  chaining, tolerances at the substep's increment; the march's own step
  path is NOT routed through it (digit identity by construction).
- rtx-fea NonlinearDynamicStepper::step_with_dt (step at an explicit dt;
  step() delegates float-for-float); Fsi2Harness::advance_subcycled_with
  (explicit fluid dt; advance_subcycled delegates).

Verified: FSI2 + FSI3 committed defaults and the noise probe
digit-identical knob-off vs same-day / 2026-08-31 baselines; knob ON
on the anchor u=1.00 r=1: green, zero rescues, CSV byte-identical to
TWIN-1's u1.00.csv; fmt + clippy clean on the touched files.

Verdict of rung A on the provocation set: REFUTED 3/3 by mechanism —
every rescued interval was carried but reproduced the rejected step's
motion (dt/2..dt/32 give the same jump), so the runaway lives in the
coupled LOAD at the crossing, not in the time integration; the
SUBCYCLE=2 closure marches through the same crossing. The knob stays,
default off, as the instrument that measured this.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
2026-09-02 21:36:06 -07:00
Omar SobhandClaude Fable 5 0b4f306ed1 rtx-fea: reduced Newmark (mor::dynamic) + the phase-4a offline replay — the ≥10x gate is REFUTED by measurement at the validated resolution
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 dynamic layer over ReducedNonlinearModel: reduced consistent mass
V'MV (full element sum, never ECSW-sampled — ECSW weights are trained
on internal-force virtual work and would conserve the wrong inertia),
reduced_force_and_jacobian exposed (solve() refactored onto it), and
ReducedNewmark mirroring NonlinearDynamicStepper::newmark_newton in
reduced coordinates (same predictor, residual, tangent shape; no
rescue ladder by design — a reduced Newton death is a finding).

TDD (tests/reduced_newmark.rs): identity-basis march reproduces the
full stepper to 2.4e-14 over 15 steps (both Newton loops tightened to
1e-10 so only solver rounding separates them); rigid-translation
reduced mass = rho*A to 1e-9; a 6-mode POD basis tracks its training
trajectory at 4.2e-4 rms against a 1.0e-4 projection floor.

Phase 4a (fsi3_ecsw_offline.rs, fsi3_reduced_newmark_replay,
env-gated): reduced Newmark replay of the harvested FSI3 trajectory at
record cadence (dt_rec = 5x march dt), driven by the recorded
end-of-step loads. Measured, m=12/20:

- COST (dt-independent, the verdict): 3,068/3,580 us/step at 4.6/5.0
  Newton iters — 2.0-2.3x the banded full-order structural step
  (7,200 us/pass, bandedlu_fsi3_ny62_t85). The >=10x gate needs
  <=720 us/step; one reduced eval alone costs ~640 us because phase 2
  refuted hyperreduction (every eval loops all 70 elements). The gate
  arithmetic is closed: reduced Newton needs >=2 evals, capping the
  ROM at ~5x. THE CAMPAIGN GATE (pinned cycle bands at >=10x
  structural speedup) CANNOT BE MET at the validated resolution.
- TRACKING at record cadence diverges in the release transient (dies
  t=4.35-4.45) — and the RTX_REPLAY_IDENTITY control dies EARLIER
  (t=4.13) in the exact subspace: the death is the 5x-coarse
  integration + aliased loads, NOT the reduction. The record-cadence
  replay cannot judge subspace dynamics; the projection floor
  (1.1e-3 at m=12) remains the honest subspace statement.

Campaign verdict to be recorded in omni-cortex in the pre-registered
words.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
2026-08-30 07:07:49 -05:00
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 50e382c046 rtx-fsi: ECSW phase-2 offline study — POD subspace confirmed, hyperreduction refuted at this resolution
CI / Clippy Check (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
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
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
fsi3_ecsw_offline.rs (env-gated on RTX_ECSW_SNAP; committed default is
a no-op) measures the three pre-registered quantities on the harvested
run-7 flag trajectory, with a numbering self-check (clamped-DOF
displacement exactly 0.0 across all 3,294 snapshots).

Measured: (1) the flapping manifold compresses 560 free DOFs to 12-20
POD modes at ~0.5% held-out rms projection error — the subspace
exists. (2) ECSW has NOTHING TO EXPLOIT on the 70-element flag: the
NNLS residual sits at single-element scale until nearly every element
joins (31 el -> 10%, 51 -> 10%, 67 -> 2.5%, 70 -> 1e-15; held-out ==
training everywhere; cycle-only manifold identical) — each macroscopic
Quad8 carries non-redundant virtual work, so no sub-percent sample
smaller than the mesh exists. (3) full reduced assembly 548 us/eval;
ECSW at best 1.4x at a useless 10% residual.

ReducedNonlinearModel gains assemble_reduced_force (the measured
quantity). The campaign's >=10x structural gate cannot come from
hyperreduction at the validated resolution — recorded in the campaign
doc with the re-scope options.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
2026-08-29 15:06:33 -05:00
Omar SobhandClaude Fable 5 d9d8801f1a rtx-fea: mor gains total-Lagrangian operators and a held-out ECSW residual
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 mor scope was small-strain only ("geometrically linear") — a basis
trained on total-Lagrangian trajectories (the FSI flag marches
with_total_lagrangian) sampled through small-strain operators would
conserve the virtual work of the wrong force. ElementOperator now
carries a Formulation (SmallStrain | TotalLagrangian), the TL branch
mirroring NonlinearDynamicAnalysis exactly (SVK from Lame parameters,
total_lagrangian::internal_force_and_tangent); train_ecsw /
ReducedNonlinearModel::new keep their behavior and delegate, with
_formulated variants added. ecsw_residual evaluates a trained model's
||Cw - b||/||b|| on arbitrary snapshots — the held-out generalization
measurement; on the training set it reproduces training_residual to
1e-12 (pinned).

Verified sharply: identity-basis reduced TL solve vs a
tight-tolerance full TL solve agrees to 4.4e-15 (machine precision)
while small-strain operators land 1.1e-2 away at the same load — the
switch is exercised and exact. (At the default 1e-6 convergence
criteria the reference itself stops 1.7e-4 short; measured and
recorded in the test comment.)

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
2026-08-29 15:01:09 -05:00
Omar SobhandClaude Fable 5 d2c82af91b rtx-fea: rescue the nonlinear Newmark Newton — line search, then step subdivision
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
Both FSI3 study deaths were the flag's SVK Newton returning
ConvergenceFailed{60} inside a coupling pass at a violent mid-cycle
load. The plain full-step Newton stays float-op identical (FSI2 and
FSI3 committed defaults re-verified digit-for-digit, Newton rescues
(0,0)); only on failure does the stepper retry: a backtracking line
search on ||R|| (Armijo, alpha down to 2^-29 — 2^-8 was measured too
shallow when the tangent K_T + M/(beta dt^2) is near singular and the
solved direction enormous and inexact), then 2/4/8/16 Newmark substeps
of dt/n, each line-searched. Rescues are counted and surfaced through
MarchResult and both FSI test printouts.

Measured before writing (tests/newton_rescue.rs): a static tip load
from rest NEVER defeats plain Newton (1e6 N converges in 19 its — from
a quiescent state the predictor is the current configuration and
M/(beta dt^2) regularizes the walk) — pinned as a negative result; the
killer is a mid-swing load REVERSAL (1e4 N tip load, 3 steps of swing
at dt 5e-3, then reversed: dead in 60 its), the FSI3 turning-point
shape — now rescued by the line search alone and consistent with a
dt/32 reference march of the same interval (-0.341 vs -0.195 m, same
branch), with determinism (bit-identical re-step) and
march-continuation pinned alongside.

Also: pin the s = 1 FSI2 benchmark cycle at study horizons (iqn /
subcycle 1, t_end >= 16: f in [1.85, 2.0], uy amp in [70e-3, 92e-3] —
the mode-2 s = 2 cycle fails both bands, so losing the benchmark cycle
stays loud).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-24 19:14:13 -05:00
Omar SobhandClaude Fable 5 4534d90684 rtx-fea + rtx-cfd: the single-step seams FSI2 stands on
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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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
rtx-fea: NonlinearDynamicAnalysis refactored onto a NonlinearDynamicStepper
- set_nodal_forces on both (the interface load of a coupling subiteration,
  replaceable between steps and between subiterations of one step);
- step(&DynamicState) is a pure function of the start-of-step state and
  the current forces - commits nothing, so a partitioned coupling re-runs
  one Newmark step to the interface fixed point (the piston semantics);
- run() marches through the same stepper: one code path, pinned from both
  ends (linear limit, CSM3, and a new manual-drive == run() assertion);
- new test: a nodal step load oscillates about the *static* nonlinear
  analysis's deflection (cross-code-path, mean within 3%, amplitude 6%),
  with re-run determinism and force-swap sensitivity asserted mid-march
  (a one-step response to a force change is ~ beta dt^2 - the first
  assertion draft demanded 10% and was corrected against the physics).

rtx-cfd: the subiteration seam and the moving no-slip closure
- EmbeddedPisoSolver::snapshot()/restore() (mask + time + init flag; the
  mask is now Clone): re-running a fluid step within a subiteration is
  bit-identical to never having diverted - proven on a moving body with
  cells flipping in the re-run window;
- polygon_interface_velocity: nearest-edge linear interpolation of
  per-vertex velocities, exact for the linear-along-edge boundary data a
  finite-element interface hands over - the no-slip closure that replaces
  FSI1's zero-velocity polygon.

Suites: rtx-fea 567, rtx-cfd 325, rtx-fsi piston+transfer - all green.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-20 20:00:49 -07:00
Omar SobhandClaude Fable 5 b82f307cae rtx-fsi + rtx-cfd + rtx-fea: FSI1 — the coupled cylinder and flag
The summit rung C1: all the verified pieces joined into the first coupled
Turek–Hron computation (rtx-fsi/tests/turek_hron_fsi1.rs). The embedded
fluid computes tractions on the DEFORMED flag surface
(EmbeddedMask::traction_at, factored from surface_force); the flag's
wetted boundary is a polygon whose vertex list sits behind a lock, so the
moving-body mask rebuild picks up every shape update
(EmbeddedBody::polygon + pub polygon_signed_distance); WettedSurface —
rebuilt on the deformed interface every subiteration — carries the loads
to the flag's boundary nodes (NonlinearStaticAnalysis::set_nodal_forces);
Subiterated::aitken drives the exchange, each pass marching the fluid to
flag-load stagnation so the coupling map is a function of geometry, not
of the fluid's transient.

Result (ny = 62, 6 Aitken passes, 420 s): coupled drag 15.360 (+7.5%,
the rigid CFD1 band at this grid), lift 0.7977 (+4.4%), ux(A) 2.647e-5
vs 2.270e-5 (+16.6%; +6.1% at ny = 82), uy(A) 3.90e-4 vs 8.21e-4 at
h = 6.6 mm and 1.124e-3 (+37%) at h = 5 mm — the resolutions BRACKET the
reference through the flag's 3 -> 4-cell thickness transition, like the
rigid-flag lift; conservation 7.4e-12 every pass. Bands asserted are the
measured ones; RTX_FSI1_NY runs studies.

Two real rtx-fsi defects found by this rung (15th and 16th of the
campaign), both regression-tested (tests/transfer_curved_edge.rs):

1. solve_weights built its constraint Gram from RAW coordinates: the
   condition number grows as (position/spacing)^2 — ~1e4 for a flag edge
   at x ~ 0.26 with 5 mm spacing — and the 4x4 SVD pseudo-inverse lost
   enough accuracy that the (correctly strict) partition-of-unity /
   reproduction verification rejected healthy neighbourhoods: the
   operator's behaviour depended on WHERE the interface sat. Now centred
   on the face and scaled by the neighbourhood radius — identical
   constraints, O(1) conditioning, translation-invariant.

2. A NEARLY collinear neighbourhood (the nearest nodes of a face on a
   smoothly deformed edge: y is almost linear in x, off by the curvature
   sagitta) cannot satisfy exact centroid reproduction with bounded
   weights — the offending singular value is too large to truncate and
   too small to invert. The recruitment now widens (8 -> 16 -> 32 -> all)
   until the verified constraints hold; for a thin structure that pulls
   in the opposite face, exactly the transverse spread the system needs.

Findings measured before believed: the transfer is faithful (a strictly
local two-node split of the same tractions moved the tip by 2%); the
uy error is the sampled lift PROFILE on a 3-cell flag (a uniform
distribution of the same net lift bends 4x more), confirmed by the
resolution study; TVD limiter chatter (+-0.5% steady load — limited
schemes stall short of machine steady state) defeats steady fixed-point
coupling, so steady coupled cases run upwind while the time-marched
FSI2/FSI3 keep TVD; and the mask never chattered at FSI1's sub-cell
amplitude (fluid-cell count constant through every pass).

rtx-fsi 29 -> 31 green (lib 27, piston 2, curved-edge 1, FSI1 1).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 17:22:27 -07:00
Omar SobhandClaude Fable 5 38ca5ef080 rtx-fea: S2 — nonlinear Newmark, and Turek–Hron CSM3 to 0.1–1.3%
analysis::nonlinear_dynamic::NonlinearDynamicAnalysis: Newmark-beta with
a full Newton solve on the internal force inside every step — residual
F_ext − f_int(u) − M a(u), consistent Jacobian K_T(u) + M/(beta dt^2),
the same total-Lagrangian St. Venant–Kirchhoff / small-strain seam the
nonlinear static analysis uses, consistent mass assembled once (element
mass is configuration-independent in a total-Lagrangian setting),
undamped, homogeneous Dirichlet only (prescribed interface motion
arrives with the FSI rung). The existing NewmarkStepper stays what it
is: the right tool for constant matrices.

Verified (tests/nonlinear_newmark_csm3.rs):
- linear limit (strains ~1e-9): the TL nonlinear stepper reproduces the
  verified linear NewmarkStepper on the same dense M, K, F to a max
  deviation of 1.7e-16 — 1.3e-9 of the oscillation amplitude — over 120
  steps;
- Turek–Hron CSM3 (35x2 Quad8, dt = 0.005, gravity switched on at rest,
  6 s): ux(A) = −14.291 ± 14.291 mm vs the reference −14.305 ± 14.305
  (0.1%); uy(A) = −63.644 ± 64.295 mm vs −63.607 ± 65.160 (0.06% mean,
  1.3% amplitude); frequency 1.1020 Hz vs 1.0995 (0.23%); two Newton
  iterations per step throughout; the undamped amplitude persists
  (half-window drift 0.2%).

rtx-fea 564 -> 566 green (full suite).

With this the S-side of the Turek–Hron ladder is complete: statics
(CSM1/CSM2) and dynamics (CSM3) both land on the benchmark. Next rung is
the coupling: FSI1 with the embedded fluid, the TL flag, and rtx-fsi's
transfer rebuilt on the deformed interface each subiteration.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 14:02:15 -07:00
Omar SobhandClaude Fable 5 c25f15b3c4 rtx-cfd + rtx-fea: embedded-boundary PISO and total-Lagrangian SVK — the first two Turek–Hron rungs
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 Turek–Hron geometry decision (omni-cortex
docs/turek_hron_geometry_decision.md) chose an embedded boundary on the
fixed Cartesian MAC grid over body-fitted unstructured ALE; this commit
builds the first rung on each side of the ladder, verified MMS-first.

rtx-cfd — solvers::incompressible::{embedded, embedded_body}:
EmbeddedPisoSolver is the fixed-grid PISO predictor/projection with
per-side domain boundaries (ALE's SideBoundary semantics, so the channel
has an outlet), a (x, y, t) boundary-velocity function, and an optional
EmbeddedBody (signed distance + surface velocity; circle / rectangle /
union). EmbeddedMask classifies cells (fluid iff phi > 0 at the centre)
and faces (fluid iff both cells fluid; ghost within 1.5 h; solid deeper);
the predictor updates fluid faces only, the projection enforces continuity
on fluid cells with zero coefficient across prescribed faces, ghost faces
are re-imposed after each projection from a boundary-intercept
least-squares linear fit (exact for linear fields), the net ghost mass flux
is removed uniformly so a Neumann projection stays compatible, and loads
come by two routes: surface-stress reconstruction (full viscous traction)
and a control-volume momentum balance.

Verified (tests/embedded_mms.rs, tests/turek_hron_cfd.rs):
- no body, closed box: bit-identical to PisoSolver over 200 steps;
- embedded off-centre circle MMS 16/32/64: velocity orders 0.92, 0.97
  (plain PISO 0.85, 0.91), pressure 0.96, 0.90, max |div u| <= 9e-8 on
  every fluid cell, compatibility correction 6e-4 -> 3e-5; force on the
  circle vs the exact surface integral: surface route 0.52 -> 0.29 -> 0.15,
  control-volume route 0.61 -> 0.30 -> 0.15 (both first order, two
  unrelated readings of the same solution);
- Turek–Hron CFD1 (Re 20, h = 10 mm, flag two cells thick), settled to
  four digits: surface drag 15.71 / lift 0.94, control-volume drag 15.62 /
  lift 1.08 vs reference 14.29 / 1.119 — the drag routes agree to 0.6%,
  both +9.5%. A coarse first number; the refinement study waits on a
  multigrid projection (SOR: 0.1 s/step at 250x41 in the test profile).

Fourteenth defect of the campaign: the fixed-grid PISO predictor zeroes
the transverse convective face velocity on its domain sides (exact for
walls); carried into a solver with an outlet it dropped the OUTGOING
momentum flux through the outlet side of the v control volumes, the last
column accumulated, and CFD1 went NaN at t ~ 4 s. Found by printing where
max |u| lived (x = 2.5) after halving dt changed nothing. Fluxes now come
from the stored boundary faces on every side.

rtx-fea — elements::total_lagrangian + NonlinearStaticAnalysis::
with_total_lagrangian(): Green–Lagrange strain, second Piola–Kirchhoff
stress from a St. Venant–Kirchhoff law on the material's Lamé parameters
(plane strain in 2-D), B_L of the current deformation, material plus
geometric tangent; dead-load body force per reference volume.

Verified (tests/total_lagrangian_svk.rs):
- zero displacement: the plane-strain stiffness to 1e-13;
- tangent = d f_int/du by central differences at 20% random displacement
  (Quad4, Quad8, Hex8): relative < 1e-7, symmetric to 1e-12;
- a 34-degree rigid rotation produces no internal force; the small-strain
  routine does (negative control);
- manufactured finite-strain solution, body force by FD of the exact
  P = F S: Quad4 orders 1.95, 1.98; Quad8 2.93, 3.03, 3.02 (an 8%
  amplitude, Green–Lagrange strain to -0.25 near SVK's compressive limit
  E = -1/3, broke Newton on fine meshes — the material, not the code; 3%
  is clean);
- Turek–Hron CSM1 at 70x4 Quad8: u(A) = (-7.060, -65.43) mm vs
  (-7.188, -66.10), 1.0% / 1.8%, converging from below (35x2: -65.14);
  CSM2: (-0.4604, -16.79) vs (-0.4690, -16.97), 1.1% / 1.8%.

rtx-cfd 293 -> 301 green (5 unit + 3 integration), rtx-fea 559 -> 564.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 08:51:32 -07:00
Omar SobhandClaude Fable 5 8071d5888d rtx-fea: ECSW model-order reduction — POD-Galerkin plus hyper-reduction, verified end to end
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 third Farhat gap. New rtx_fea::mor module:

- pod::pod_basis — orthonormal SVD basis with an energy-criterion
  truncation. Verified: rank-2 data yields exactly 2 orthonormal modes that
  reconstruct every snapshot to machine precision; a loose tolerance
  truncates a dominant-mode-plus-noise set to one mode.
- nnls — Lawson-Hanson non-negative least squares with the early stop that
  makes ECSW work: iteration ends at the requested residual, and the
  active-set structure caps the support at one column per outer iteration,
  so sparsity falls out of the stopping tolerance. Verified against KKT
  conditions, exact positive solutions, negative-clipping, and a
  sparsity-vs-tolerance case. Its thresholds are RELATIVE to the problem's
  own scales — the first version used absolute cutoffs (1e-14) that
  silently ended the iteration on ECSW's small-magnitude training systems
  at 1.2e-3 instead of the requested 1e-4.
- ecsw::train_ecsw — element weights such that a small subset reproduces
  the reduced internal force (the virtual work against the basis) over the
  training snapshots. w = 1 solves the system exactly by construction, so
  it is always consistent; nonnegativity is what keeps a sampled element
  from producing energy.
- reduced::ReducedNonlinearModel — Newton in POD coordinates, assembling
  either every element (POD-Galerkin) or the ECSW sample, on the same
  per-element force/tangent machinery the nonlinear analysis uses.

End-to-end verification (tests/ecsw_mor.rs): a clamped nonlinear block,
snapshots from a 4-point load sweep, evaluated at an UNSEEN load factor:

    POD modes: 2         ECSW sample: 5 of 24 elements
    training residual 2.2e-7 (requested 1e-4)
    error vs full solve: POD-Galerkin 3.09e-7, ECSW 3.08e-7
    hyper-reduction cost (ECSW vs full ROM): 1.6e-8

And the assertion with the most teeth: the same 5 elements with their
weights forced to 1 read a relative error of 1.22 — a completely wrong
field — so the accuracy is carried by the WEIGHTS, not by the subset
happening to be representative.

Scope, stated plainly: geometrically linear, materially nonlinear,
homogeneous Dirichlet only (no lifting); the basis lives on the free DOFs.

559 rtx-fea tests, 0 failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 00:43:32 -07:00
Omar SobhandClaude Fable 5 4da70faa1e rtx-fea: QM6 as an opt-in bending path; cantilever pinned to Euler-Bernoulli directly
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 QM6 incompatible-modes stiffness existed and was verified
(compute_stiffness_matrix_incompatible) but nothing could reach it: the
assembler always routed Quad4 through the compatible element. AssemblyOptions
gains use_incompatible_modes (default false — every existing matrix is
byte-identical, which the manufactured-solution verification depends on),
threaded through GlobalAssembler into StandardFiniteElement; element types
QM6 does not apply to keep their standard stiffness either way.

What it buys, measured on the cantilever first bending mode against
Euler-Bernoulli's 40.3848 Hz:

    mesh    QM6 (error)          compatible (error)
    8x2     40.4020  (+0.04%)    81.8102  (+102.6%)
    16x4    40.3402  (-0.11%)    53.8022  (+33.2%)
    32x8    40.3242  (-0.15%)    44.0796  (+9.2%)

The frequency is now asserted against the closed form directly (0.5% band)
instead of as convergence-from-above, plus the condensation theorem — QM6
can only soften, so its frequency must sit at or below the compatible one on
every mesh. The slight undershoot on finer meshes is physical: the 2-D solid
carries the transverse shear flexibility the beam theory neglects.

552 rtx-fea tests, 0 failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 19:47:25 -07:00
Omar SobhandClaude Fable 5 6510045b5d rtx-fea: wire NonlinearStaticAnalysis — Newton on the consistent tangent, MMS-verified at second order
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
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
NonlinearStaticAnalysis::run returned DVector::zeros unconditionally, like
ModalAnalysis and DynamicAnalysis before their repair. It is now full
Newton-Raphson on R(u) = f_ext - f_int(u):

- ElementMatrixComputer::compute_internal_force_and_tangent integrates
  f_int = int(B' sigma dV) and K_T = int(B' D_T B dV) in ONE quadrature
  sweep from a constitutive closure in the element's reduced Voigt space —
  computing both together is what keeps the tangent consistent with the
  stress, which is what quadratic convergence rides on.
- materials::reduced_constitutive bridges the Material trait (Voigt-6) to
  that closure: 3-D passes the total strain straight through; 2-D supports
  the linear plane-stress closed form and refuses nonlinear materials
  explicitly, since plane-stress condensation of a general law needs a
  per-point iteration that is not implemented yet.
- Dirichlet DOFs are held at their (load-scaled) values and Newton runs on
  the free DOFs, so the prescribed motion enters through f_int itself — no
  K_fc bookkeeping to get wrong. Body force enters via set_body_force, the
  same hook pattern the CFD solvers use for manufactured solutions. Uniform
  load stepping; other strategies and quasi-Newton refuse explicitly.
- StandardFiniteElement::compute_internal_forces, previously a zeros stub,
  now delegates to the same machinery.
- Mesh::validate is now called in run() (the old TODO), and NonlinearConfig
  gained a Default.

Verified two ways (tests/nonlinear_static.rs):

- Equivalence: with LinearElastic the loop lands on the directly assembled
  linear solution to 1e-10 in exactly one Newton step — same B, quadrature
  and solver, so any disagreement is the nonlinear assembly.
- Manufactured solution with a genuinely nonlinear material (energy
  W = 1/2 e'De + alpha/3 I1^3, so stress and tangent are exact derivatives;
  body force by central differences of the closed-form stress): L2 errors
  6.032e-2, 1.780e-2, 4.595e-3 on 2/4/8 Hex8 — observed orders 1.76 and
  1.95, climbing to the theoretical 2. The forcing contains the nonlinear
  term, so the order is reachable only if it is solved; an inconsistent
  tangent is caught separately by the iteration-count bound.

This unblocks ECSW model-order reduction, which needs a working nonlinear
solve underneath it. 551 rtx-fea tests, 0 failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 19:44:08 -07: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 Fable 5 8495a690d9 rtx-fea: build the missing mesh-generation APIs and re-enable 8 test modules
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
The largest cluster of the 128 compile errors behind the disabled test
modules was one missing API family. Now built, each with invariant tests
a plausible-wrong mesh fails:

- Rectangle::generate_quad_mesh / generate_tri_mesh — structured grids,
  CCW elements, exact area sums asserted
- Circle::generate_tri_mesh — centre fan plus ring bands; tiles the
  inscribed polygon exactly
- Box3D::generate_hex_mesh / generate_tet_mesh — the tet split is the
  Kuhn/Freudenthal 6-tet subdivision, conforming across cells, positive
  volumes summing exactly to the box
- Sphere::generate_tet_mesh — concentric UV shells, centre fan, prisms
  split by the Dompierre smallest-index diagonal rule so neighbouring
  prisms agree; conformity and closed-boundary asserted via face counting
- Mesh::validate — empty/inconsistent/orphan checks plus signed-area
  orientation for planar Tri3/Quad4, which is what an inverted
  connectivity fails
- Mesh::find_boundary_edges / find_boundary_faces / calculate_edge_normal,
  Node::distance_to / with_label

Re-enabling the tests found a real defect: geometry::Face derived
order-sensitive PartialEq/Hash, so the same face listed by two adjacent
elements (different start node, opposite winding) never compared equal.
A 2x2x2 hex mesh reported 32 boundary faces instead of 24 — and
find_boundary_nodes in 3-D and the 3-D surface-area statistic sit on the
same counting. Face identity is now canonical (sorted ids; quads keep
their diagonal pairing).

Partitioning: the fixtures targeted an instance API that never existed —
MeshPartitioner::partition is an associated function. Two real gaps fixed:
interface_elements was never populated, and requesting more partitions
than elements produced useless empty partitions (now clamps).

Fixtures corrected rather than the code where they encoded abandoned
designs: global DOF numbers on nodes (DofMap's job), element
thickness/property bags nothing reads, a 0-to-1 quality score that never
existed, and a clockwise sliver that validate now rightly rejects. The
GPU data conversion test stays disabled with the GPU solver tranche.

Lib tests 72 -> 117, stable across 5 runs, all integration suites green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 18:29:55 -07:00
Omar SobhandClaude Opus 5 698c844926 solvers: near-wall momentum, Newmark dynamics, QM6, and MMS across elements
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
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
Four parallel work items plus two defects found while integrating them.
561 -> 592 tests, 0 failing, verified stable over repeated runs.

## rtx-cfd: solve the near-wall velocity lines

Every u row sits at y = (j+0.5) dy and every v column at x = (i+0.5) dx --
strictly interior. The sweeps froze rows 0 and ny-1 and columns 0 and
nx-1 and treated whatever was stored there as a boundary condition, which
imposed wall values half a cell inside the domain. They are now unknowns,
with the wall entering through the control volume's half-cell conductance
(mu dx / (dy/2)), zero convective flux through the wall, and the wall's
tangential velocity in the source.

That in turn makes continuity enforceable on every cell, with a neighbour
coefficient zero only for a genuine boundary face. Extending continuity
had been tried before and broke convergence; it works now because the
near-wall lines are no longer frozen. Order matters here.

Manufactured solutions, which is how any of this is known:

    n     L2 velocity   order      max |p - p_exact|
    16    3.516212e-2      -          9.245576e-2
    32    1.953751e-2    0.85         5.225739e-2
    64    1.037523e-2    0.91         2.796415e-2

Velocity error is 7.4x smaller at n=16, and the observed order rises from
0.48 toward 1. The pressure error was 0.408 -> 0.624 -> 0.756, *growing*
with refinement; it now falls. Divergence on the outer ring of cells goes
from 1.0e1 to 2.5e-10.

A separate defect found on the way: u_source_term was computed and never
called, so the x-momentum equation carried no body force at all while the
y-momentum one did. That is exactly the u-versus-v asymmetry the earlier
diagnosis had flagged as an unexplained clue.

Cavity at 65^2, against Ghia's u_min = -0.2109 at y = 0.4531:
-0.1792 at 0.3906 before, -0.1932 at 0.5000 after, in 733 iterations
rather than 971.

The cavity test now sets FreeSlipWall on all four sides plus the lid
through the new set_wall_velocity hook. That is not a weakened benchmark:
on a staggered grid the only velocity component living *on* a boundary is
the normal one, which is what FreeSlipWall prescribes, and the tangential
no-slip arrives through the half-cell wall term with wall velocity zero on
the three stationary walls. Prescribing whole u rows and v columns, as
before, pins lines half a cell inside the domain and over-determines the
cells beside them once every cell has a continuity equation.

## rtx-fea: DynamicAnalysis, previously a stub returning zeros

Newmark-beta in acceleration form -- the displacement form divides by
beta dt^2, singular at beta = 0 -- with Rayleigh damping, the effective
matrix Cholesky-factorised once and reused. Initial acceleration is solved
from M a0 = F0 - C v0 - K u0 rather than assumed zero, which would destroy
the second-order rate.

Verified two ways that cannot both be faked: against the closed-form
single-degree-of-freedom response, undamped and damped, with the measured
order of accuracy; and against the free-vibration period of the same bar
whose modal frequencies are already validated. Time domain and frequency
domain come from different code paths.

## rtx-fea: QM6 incompatible modes

Wilson's Q6 with Taylor's correction, added alongside compute_stiffness_
matrix rather than replacing it -- the existing method is byte-identical,
which matters because the manufactured-solution verification depends on
it. Internal modes statically condensed; the incompatible strain block
evaluated at the element centre, which is what makes the patch test pass
on distorted elements.

## rtx-fea: manufactured solutions across the element library

    Quad4  order 2.00      Tri3   order 1.98
    Quad8  order 3.00      Hex8   order 1.96  (new 3-D solution)

Each element asserts its own theoretical rate.

## Two defects found while integrating

Reverse Cuthill-McKee node ordering was nondeterministic. All three of its
orderings -- seed selection, neighbour ordering, and the trailing sweep --
were decided by HashMap/HashSet iteration order, which std randomises per
process. On a rectangular mesh every corner ties at minimum degree, so two
calls to displacement_only on the same mesh in the same process returned
different DOF indices for the same node, agreeing in only 5 of 20 measured
runs. Ties now break by node id. This surfaced as a coin-flip test failure
-- 12 in 25 runs -- and would have been dismissed as flaky rather than
diagnosed had the integration pass not re-run it.

Quadrature: triangle(3) weights summed to 0.25 against a reference area of
0.5, and tetrahedron(3) to 1/36 against a volume of 1/6. Both divided
weights that were already tabulated for the reference measure by that
measure again, so both rules integrated everything to a fraction of its
value -- invisibly, since a scaled quadrature leaves the stiffness matrix
symmetric, the mass matrix positive definite and the rigid-body modes
exact. New test asserts every rule integrates 1 to its reference measure,
across every family and order, plus Gauss-Legendre exactness to degree
2n-1.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 15:39:20 -07:00
Omar SobhandClaude Opus 5 8615fc5783 rtx-fea: verify elastostatics by manufactured solution — second order confirmed
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
Adds the Method of Manufactured Solutions to this crate, and it immediately
paid for itself by finding a bug that every existing test missed.

MMS asserts something stronger than "close enough to a value someone
believed": that the discretisation converges to the exact solution at the
rate the theory predicts. Choose a smooth field, substitute it into the
governing equations, and whatever they fail to balance is the body force
that makes it exact. Solve, refine, read off log2(e_h / e_h/2).

Observed order for Quad4 displacement in L2:

    n =  8   L2 error = 7.816681e-3   order    -
    n = 16   L2 error = 1.962845e-3   order 1.99
    n = 32   L2 error = 4.912786e-4   order 2.00
    (n = 64 reads 2.00 as well, at ten times the cost)

That verifies the whole chain at once -- element matrices, quadrature,
Jacobian, assembly, DOF numbering, constraints and the linear solver --
against a solution none of them can represent exactly. It is the check
that none of the sixteen defects fixed in this crate would have survived.

The manufactured field is u = sin(pi x) sin(pi y), v = x^2(1-x) y(1-y):
smooth, not in the bilinear element space, with the two components
different in form and a non-zero shear strain, so the shear block of the
constitutive matrix is exercised rather than skipped.

Found by it: `compute_shape_functions` inferred how many parametric
coordinates to pass from the coordinate *values* --

    match coords.eta() {
        0.0 if coords.zeta() == 0.0 => vec![coords.xi()],   // 1 component
        ...

-- so any evaluation on an axis was handed a one-component slice, which
every 2-D and 3-D element rejects. That includes the element centre and
the middle point of every odd-order Gauss rule. It survived only because
the default 2-point rule never samples zero; asking for a 3-point rule to
integrate the error was enough to trip it. Dimensionality now comes from
the element, which is where it belongs.

Two prerequisites, both real functional gaps rather than test scaffolding:

  - Consistent body-force integration. `BodyForceBC` distributed load as
    force * volume / num_nodes, which is exact only for a constant force
    on a symmetric element and otherwise first-order -- enough to cap the
    measured order of the whole solver at 1 regardless of the element.
    `ElementMatrixComputer::compute_body_force_vector` now integrates
    the consistent form, taking the force as a closure so a spatially
    varying load can be expressed at all.

  - Non-homogeneous Dirichlet conditions did not exist.
    `StaticLinearAnalysis` read the prescribed value out of the boundary
    condition and discarded it, and `extract_free_system` built the
    reduced right-hand side without the K_fc u_c coupling, so every
    Dirichlet condition behaved as zero whatever the caller asked for.
    `GlobalSystem::set_prescribed_value` and the coupling term close
    that, reusing `extract_submatrix` and `multiply_vector` rather than
    a dof-by-dof loop.

560 tests across the three crates, 0 failing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 12:04:46 -07:00
Omar SobhandClaude Opus 5 e30cfe4ce9 rtx-fea: repair the element library; the crate is now green with no quarantine
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
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
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
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00
redclawsystems 4d88dc0584 Initial commit 2026-03-04 00:08:42 +00:00