Commit Graph
429 Commits
Author SHA1 Message Date
Omar SobhandClaude Fable 5 647f247601 rtx-fsi: report Newton rescue counts in the coupling-failure panic
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
FSI3 run 3 died on a coupling stall 0.2 s after the rescue carried it
past run 2's Newton death, and the panic path printed nothing about
how many rescues had engaged. Observability only — the panic message
now carries rescue_counts.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-25 01:07:49 -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 555a72cbc0 rtx-fsi: spike-clamp the force-measurement probe
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
measure_force integrated raw traction samples while the coupling loads
carried the 20x-median clamp — so the s = 1 benchmark run's REPORTED
drag/lift were +-4,000-scale garbage against a +-78 reference while its
displacements matched the benchmark to 0.1%. Collect, clamp, then
integrate, both probes. Reporting only: the committed FSI2 default
reproduces its trajectory to every printed digit (uy 3.7732 / 3.7920
mm) and its rigid-phase drag (121.4) with the clamp in.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-24 05:59:18 -05:00
Omar SobhandClaude Fable 5 c1bcdc408e rtx-fsi: FSI3 floor 6e-5 — the floor rides with the motion; measure it through the cycle
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 first subcycle-2 study (t = 4 -> 8) ran 2.2 s of cycle — over
[5, 6] s: +-42.8 mm at 6.2 Hz, trending toward the reference +-34.9 mm
at 5.46 Hz from above — then stalled at t = 6.18 s: residual 1.9e-4
against an accept window of 1.5e-4 (5 x the 3e-5 floor the rest-state
probe suggested), at a turning point of the cycle where the step
increment, and with it the increment-relative part of the window,
collapses while the flip noise does not. Floor set from the cycle's
measured stall level (window 3e-4). Default re-verified: 581 steps,
3.5 subit/step (max 5), 0 stalls, conservation 1.5e-12, ux -2.90 +-
2.93 mm vs reference -2.86 +- 2.70.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-23 23:41:02 -05:00
Omar SobhandClaude Fable 5 c0666bf22a rtx-fsi: C3 — FSI3 opened: the added-mass regime, its impulse artefact, and the coupler hygiene it demanded
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 march is extracted from the FSI2 test into a shared
tests/fsi2_harness/march.rs (MarchConfig from env / MarchResult /
window statistics), the harness parameterised by BenchmarkCase (FSI2,
FSI3: inflow, solid density, modulus, rigid-flag drag reference), and
turek_hron_fsi3.rs written on top (Re 200, density ratio 1, E 5.6e6).
The committed FSI2 default is bit-identical through all of it —
every new knob defaults to FSI2's behaviour; re-verified twice to every
printed digit (uy 3.7732 / 3.7920 mm).

FSI3's first contact, traced pass by pass (RTX_FSI3_TRACE), exposed
the added-mass instability in its purest form and, one measured
mechanism at a time, what a partitioned coupling at unit density ratio
needs:

- C^1 interface motion (MarchConfig::c1_interface): a constant
  per-step interface velocity is a velocity JUMP at each step start,
  and the incompressible fluid answers with an impulsive added-mass
  load ~ rho L dv / dt_fluid (8x the physical reaction under
  subcycling): 1,600 N at release, 48,000 N and a 59 mm response one
  step later, the flag's Newton dead the pass after. Constant
  acceleration from the previous end velocity to 2 dd/dt - v_n removes
  the impulse (loads 1,700-2,400 N).
- IQN first-pass relaxation as a knob (initial_relaxation, FSI3 0.05):
  |1 - omega (1 + g)| must contract; 0.5 diverges past gain 3.
- The divergence verdict waits for the secant (IqnIls): the exploratory
  second pass on a high-gain map legitimately overshoots 10x before the
  first secant column exists. Pinned by a gain-40 model test.
- Kinematic predictor (predictor: "kinematic", velocity only): the
  structure-alone predictor ignores an added mass comparable to the
  flag's and overshoots 2-5x, drawing 5-6x loads every first pass; and
  NOT with the acceleration — Newmark average acceleration carries an
  inconsistent initial acceleration as a sign-alternating mode
  (d + dt v + dt^2 a / 2 predicted 22 mm at release; converged 0.14).
- Quiescent release (quiescent_release): the structure-alone
  "consistent" initial acceleration M^-1 F ignores the added mass.
- Coupler hygiene (IqnIls): a stalled or diverged step's secant columns
  are no longer retained (a bistable mask flip's columns extrapolated a
  30 mm interface jump on the next step); two-window stagnation
  detection reports a plateau early instead of bouncing to the budget
  (a single-window test misjudged a slowly converging step and is
  recorded as such); trust region tightened to 10x the residual. A
  noise-column filter at the tolerance was measured to HURT (stalled a
  converging step at 5.5e-4) and is disabled (threshold 0).
- The floor measured, not borrowed (fsi2_interface_noise.rs gains
  RTX_NOISE_CASE=fsi3): flip jumps 3.6e-4 (12x FSI2's), the subcycle-8
  release map stalling near 1e-6, the subcycle-2 map converging to
  9e-10; and through the release transient (flag at ~0.3 m/s) the
  subcycle-8 floor rides up to ~1e-3 with the motion — which moved the
  FSI3 default to subcycle 2 at a 3e-5 floor.

FSI3 committed default (ny 62, release t = 4, t_end 4.2, subcycle 2):
581 steps, 3.8 subit/step (max 8), 0 stalls, 0 retries, conservation
4.3e-13; uy 10.1 +- 27.5 mm and ux -3.1 +- 3.2 mm over the first 0.2 s
(reference cycle 1.45 +- 34.90, -2.86 +- 2.70). Machinery invariants
pinned; physics bands await the study record. Rigid-flag drag 426.9 vs
CFD3's 439.45 (-2.9%).

The FSI2 mode-2 study pin (IQN / subcycle 2, t_end >= 16) was measured
with the pre-hygiene coupler; re-verify on the next s = 2 study run.

48 lib tests green, clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-23 19:43:02 -05:00
Omar SobhandClaude Fable 5 be04e0e233 rtx-fsi: pin the mode-2 flapping cycle under the IQN / subcycle-2 configuration
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
Study horizons (t_end >= 16) under RTX_FSI2_COUPLER=iqn
RTX_FSI2_SUBCYCLE=2 now assert the measured mode-2 cycle: frequency in
[2.05, 2.45] Hz and uy amplitude in [45, 70] mm — bands that admit the
settled cycle at either grid (ny=62: 57.6 mm / 2.227 Hz at t=16,
55.9 / 2.224 at t=30; ny=82: 57.1 / 2.236 at t=20) and reject both the
wake attractor (3.73 Hz, ±17 mm) and the reference itself (1.93 Hz,
±82 mm) as material changes. The default configuration's pins are
unchanged. Module docs brought up to the eleventh-session record.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-23 16:54:12 -05:00
Omar SobhandClaude Fable 5 a342e703a4 rtx-fsi: trust-region cap on the IQN step + increment-scaled stall acceptance
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 s = 1 FSI2 march (deepest rung: 1.955 Hz, ±73 mm mid-growth) found
two coupler failure modes at peak motion:

- run 1 (budget 12): NotConverged at residual 6.1e-4 = 9% of the step's
  own increment, after the history-reset retry — killed at t = 11.1 s.
- run 2 (budget 30): the deeper budget let an ill-conditioned secant
  model extrapolate the locally violent map into a candidate interface
  that swept to the domain wall and crashed the mask build BEFORE any
  residual guard could fire (t ~ 9.9 s).

Fixes, both scale-relative per the absolute-threshold rule:

- STEP_CAP = 50: the full quasi-Newton step r + W alpha is capped at
  50x the current residual norm, direction kept. Legitimate large
  Newton steps (near-marginal gains) pass; thousand-fold geometric
  extrapolations cannot. Pinned by a noisy-map test asserting every
  iterate's step stays within the cap.
- The march accepts a stalled step at residual < max(5 x tolerance,
  0.1 x the step's own increment) — the rare violent step near peak
  motion carries an order-below-increment error, counted like every
  stall and bounded by the existing stall-fraction assert; the retry
  trigger mirrors the same bound.

46 lib tests green, clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-21 19:14:53 -07:00
Omar SobhandClaude Fable 5 d5f19ea497 rtx-fsi: IqnIls::reset_history — stale secant columns overshoot in rapid transients
Documentation / Build API Documentation (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
Documentation / Build User Guide (push) Canceled after 0s
The s = 2 ladder march (resonant growth finally open: 2.3 Hz, amplitude
through ±46 mm — 2.7x the wake attractor the loose coupling locked)
died at t = 11.12 s with CouplingDiverged at iteration 2: the predictor
left a 1e-4 first residual, and the first quasi-Newton update drove it
to 1.1e-3. Cross-step secant history assumes the interface Jacobian
drifts slowly; columns recorded at much smaller amplitude steered the
least-squares direction wrong at 2.7x that amplitude, and the
divergence guard read the overshoot as added mass. The map itself
converges deeply from a clean start (the noise probe's stall
measurement), so the recovery is: reset the history, retry the step
once from the predictor.

- IqnIls::reset_history(), with a cold-start-equivalence test.
- turek_hron_fsi2: on an unaccepted coupling verdict under the IQN
  coupler, reset + one retry from the predictor (Aitken carries no
  history — a retry would repeat the identical iteration, so the path
  is IQN-only); retried steps counted and reported like stalls.

Verified end-to-end: the re-run crossed the killing step and marched on
(t = 11.18: uy +56 mm, window amp ±50 mm, 1.4 subit/step). 45 lib
tests green, clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-21 09:34:10 -07:00
Omar SobhandClaude Fable 5 140310b223 rtx-fsi: the noise floor interrogated — smoothing refuted, IQN-ILS lands, tight coupling reopened
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 tenth session ended on "lower the interface noise floor". This
builds the levers and measures them, and the measurements overturn the
diagnosis:

- smooth_tractions: arclength moving average over the wetted surface,
  area-weighted, smooth normal-similarity factor so corners do not mix
  and the smoothed load stays continuous in the geometry. Nine unit
  tests. MEASURED NEGATIVE RESULT: the flip-scan floor is unchanged to
  0.2% at radii 1-3h — the flip's load jump is coherent through the
  fluid field (mask rebuild shifts the pressure around the flipped
  cell), and a surface average preserves coherent shifts. Default off;
  the probe pins the attribution so nobody re-reaches for this lever.

- IqnIls: interface quasi-Newton with inverse least squares (Degroote
  2009) — filtered MGS least squares over secant columns (filter
  RELATIVE to column norm), cross-step history reuse, per-step
  set_tolerance. Model-map tests: exact on anisotropic linear maps
  within dim+2 passes (scalar Aitken provably cannot be), scale
  invariant, history reuse shortens the next step, stalls at the noise
  scale instead of diverging (fixture lesson: per-pass noise, not
  state-dependent noise — the latter has a genuine fixed point).

- tests/fsi2_harness/: the FSI2 machinery extracted shared; verified
  pure code motion (committed release response reproduced to every
  printed digit). March gains RTX_FSI2_SMOOTH / RTX_FSI2_COUPLER=iqn /
  RTX_FSI2_REUSE knobs; pinned bands guard the default configuration.

- tests/fsi2_interface_noise.rs: the probe. Flip-scan floor at
  subcycle 8: 3.05e-5 (pinned); smoothing attribution pinned; the
  cross-subcycle scan recorded but unpinned (the fixed geometry
  increment's wall-velocity trend, increment/dt_c, swamps the flip
  signal at small dt_c — a dt_c^2 scaling hypothesis died in that
  operationalization). THE OPERATIONAL FLOOR — the real release step
  subiterated at tolerance 1e-9 with residuals traced — converges DEEP
  at both subcycles: s8 aitken 3.4e-9 / iqn 1.6e-9, s2 both ~6.4e-10
  in 5-6 passes. The flip jumps are events at specific geometries, not
  a floor under every step: the tenth session's subcycle-2 blowup was
  tolerance mis-budgeting (2e-4 held fixed while dt_c shrank), not an
  impassable floor. Probe bug found and fixed on the way: stale shared
  geometry leaked a 4.5e-5 phantom first residual into the first stall
  run; every measurement now resets the geometry on entry.

All 924+17 tests green: lib 44 (was 27), piston 2, curved edge 1,
FSI1, the committed FSI2 march (release response identical), the probe.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-21 06:42:05 -07:00
quantumandClaude Fable 5 c36cf2f8a7 rtx-backend-metal: fix swap_dims returning a corrupt strided view
swap_dims copied the buffer but wrote elements back at their ORIGINAL
positions (new_idx was computed with the swapped strides), returning a
stride-swapped non-contiguous tensor. Every other op in this backend —
elementwise kernels, MPS matmul, to_vec — reads raw buffers and ignores
strides, so any transpose consumer (notably the autograd matmul backward,
grad_a = grad_c @ b^T) silently computed on untransposed data. Found via
CPU-vs-Metal gradient parity on the DigiGraph HetGAT: forward matched,
gradients were ~2x off.

swap_dims now physically permutes into a contiguous result (reading
through the input's strides + offset), and reshape asserts contiguity
instead of silently reinterpreting a non-contiguous buffer. 3 new parity
tests incl. matmul-after-transpose (25 total pass on-device).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-21 06:11:38 -07:00
quantumandClaude Fable 5 9297976929 rtx-backend-metal: GPU index_select / index_add via one-hot CSR SpMM
Override the Backend trait's host-round-trip defaults: gather is S @ X with
S the [E x N] one-hot selection CSR; scatter-add is the adjoint S^T @ X,
whose CSR is built directly by counting sort so duplicate indices land in
one row and the spmm kernel (one thread per output element) accumulates
them without atomics. CSR matrices are cached per thread keyed by the
exact index list + dims, so a static graph topology (GNN message passing)
builds each matrix once. Host fallback on degenerate shapes or any
sparse-pipeline failure.

13 new parity tests vs CPU reference: duplicates, unreferenced rows,
D=1/2/3, 15k x 5k x 64 gather/scatter, cache reuse, adjoint roundtrip.
Verified on-device that the SpMM path (not the fallback) serves all 13.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-21 05:56:23 -07:00
quantumandClaude Fable 5 2e23d0f4c6 rtx-backend: gate CUDA parity tests to x86_64 Linux
The rtx-backend-cuda dev-dependency is already gated to
cfg(all(linux, x86_64)) in Cargo.toml, but the test file imported it
unconditionally, so cargo test -p rtx-backend failed to compile on macOS.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-21 05:52:03 -07:00
quantumandClaude Fable 5 72b41e3167 rtx-backend-metal: align to current rtx-metal API
rtx-metal dropped tensor_ops::{sin,cos,pow,clamp,gt_scalar,var} and never
had nn::{max_pool2d,avg_pool2d}; nn::conv2d grew a scalar 14-arg signature.
Give the missing ops correct host fallbacks (the sum_dim pattern), and
dispatch conv2d to the Metal kernel when its restricted signature applies
(symmetric stride/padding, dilation 1, groups 1), host fallback otherwise.

cargo test -p rtx-backend-metal: 22/22 parity tests pass on-device (M5).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-21 05:52:03 -07:00
Omar SobhandClaude Fable 5 4e90177aa9 rtx-fsi: C2 — FSI2's coupled march, and the wrong attractor measured
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 unsteady coupling on the piston pattern with the real solvers: per
TIME STEP, Aitken subiterations of (8x-subcycled embedded TVD/multigrid
fluid <-> flag nonlinear-Newmark step), the fluid re-runnable through
snapshot/restore + a field clone, the flag re-runnable because the
stepper commits nothing, the moving polygon carrying the flag's actual
interface velocity. Conservation 8.3e-12 over 9,263 coupled steps; the
rigid phase reproduces this solver's own CFD2 values at both grids
before anything couples.

What the 30 s studies measured (ny = 62 AND 82, three protocols): the
coupled system self-excites into a wake-forced cycle at 3.729/3.728 Hz
with uy(A) +-17.3 mm at both grids - grid-converged, protocol-
independent, kinematically genuine (ux locks 2x uy) - and NOT the
benchmark's mode-2 resonant cycle (1.93 Hz / 81.6 mm; the flag's vacuum
mode 2 is 1.9245 Hz). Diagnosis measured stepwise: the staggered phase
lag starves the resonant channel (a subcycle=2 probe redirected growth
to 1.9 Hz as predicted), and tighter dt_c is blocked because wall-
velocity noise = displacement-tolerance / dt_c while the needed
tolerance sits below the discrete interface noise floor (~1.3e-4 at
full inflow, mask flips through beta dt^2/m). The route to the
benchmark cycle is lowering that floor, not iterating against it.

Robustness, both measured: rare wild tractions (19 samples in 2.4M)
clamped at 20x the sample median - clamped, not dropped: a hard drop
makes the pass discontinuous and the subiteration bounces at step
scale; and Aitken "divergence" verdicts within 5x tolerance accepted as
noise bounces over well-predicted steps (counted, bounded).

The committed default (t_end = 7, ~9.5 min) pins the deterministic
release response (uy 3.773 +- 3.792 mm, band +-35%); study horizons
>= 25 s pin the measured 3.73 Hz attractor so any material change is
loud. Full study record in the module docs and omni-cortex
solver_status.md.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Lnyrw33Lu6rUhW42E9KHwq
2026-08-21 02:45:43 -07: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 c0f5a86f03 rtx-cfd: the embedded-body pieces FSI1 stands on (belongs with b82f307)
EmbeddedMask::traction_at (the per-sample traction factored out of
surface_force, so a coupling loop can load a structure at its own
quadrature points), EmbeddedBody::polygon and the public
polygon_signed_distance (a deformable interface as a vertex list, usable
behind a lock through EmbeddedBody::from_sdf). Left unstaged by mistake
in b82f307 — that commit's FSI1 test needs these to compile.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 17:22:44 -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 0ad31abb6b rtx-cfd: F2 — the moving embedded body, and falsifier 3 measured
EmbeddedPisoSolver::set_moving_body: the mask is rebuilt at the
end-of-step geometry every step, and the new mask's ghost values are
reconstructed FROM THE PREVIOUS CORRECTED FIELD (EmbeddedMask::
impose_from — the boundary-history principle extended to a moving wall),
so a stationary body run through the moving path is bit-identical to the
static path, which is the first test. A velocity face that flips
solid -> fluid enters the new interval holding exactly the ghost
reconstruction the previous step left on it — a consistent near-wall
value, not garbage; a fresh pressure cell is refilled from its fluid
neighbours before the predictor's gradient can read the value it kept
while inside the body. The body must move under a cell per step (the
convective dt limit already enforces this for bodies slower than the
local peak velocity). EmbeddedResult reports fresh_cells.

tests/embedded_moving.rs:
- a stationary body through the moving path: 0.0 difference over 100
  steps (and zero fresh cells, identical ghost corrections);
- a circle (r = 0.2) translating through the steady manufactured field
  with the exact field as its surface velocity — the solution must hold
  still while the mask sweeps 84 cells fresh over 300 steps at n = 32:
  max L2 velocity error 9.85e-3 = 1.16x the static steady level
  (8.489e-3), max L2 pressure error 4.67e-2 = 2.11x the static level
  (2.22e-2), bulk |div u| 1.6e-7, projection residual 5.9e-9 every step.

That pressure ratio is the geometry decision's falsifier 3 (omni-cortex
docs/turek_hron_geometry_decision.md): fresh-cell transients sit at ~2x
the static discretisation error, not orders above it — the falsifier
does not fire and no cut cells are needed. Measurement note, recorded in
the test: the divergence of body-adjacent cells read after the
end-of-step ghost re-imposition is a one-step lag by design (the next
projection honours the re-imposed prescribed fluxes — the same lag the
static path has); the continuity claims are the projection residual and
the bulk divergence over all-fluid-faced cells.

Deferred: an oscillating-cylinder benchmark against published force
histories (Duetsch et al. 1998) when the FSI rungs need it.

rtx-cfd 321 -> 323 green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 13:07:28 -07:00
Omar SobhandClaude Fable 5 38645c7c74 fix(autograd): AbsBackward produced NaN for exactly-zero inputs
sign was computed as x/|x|, which is 0/0 = NaN at x = 0; one zero element
in an |pred - target| loss poisoned every upstream gradient (hit
deterministically by dg-gnn HetGAT training). Compute x/(|x| + tiny)
instead so sign(0) = 0 (the subgradient convention). Regression test
included.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 11:53:30 -07:00
Omar SobhandClaude Fable 5 67c47898fa perf(backend-cpu): parallelize blocked gemm over row blocks with rayon
Each task owns a disjoint BLOCK_SIZE-row slice of the result; the inner
blocked kernel is unchanged. Needed for dg-gnn HetGAT training throughput
(node-level [M,64]x[64,64] matmuls dominated single-threaded step time).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 11:37:43 -07:00
Omar SobhandClaude Fable 5 35b2b2cdf4 rtx-cfd: TVD convection in the embedded predictor — the wake sheds — and Turek–Hron CFD2/CFD3
First-order upwind's numerical viscosity |u| h / 2 is ~10x the physical
viscosity on the Turek–Hron grids: the effective Reynolds number lands
near 20 and CFD3 (Re 200) produced NO vortex shedding at all — one lift
zero-crossing in three seconds at h = 10 mm. The physics, not a bug.

EmbeddedParameters gains `convection_scheme` (default Upwind, bit-
identical — the no-body degeneracy test still reads 0.0): the TVD branch
adds SIMPLE's limited face corrections (van Albada / van Leer,
`face_correction` now pub(crate)) directly in the explicit predictor —
no deferred iteration needed in an explicit step. Domain-side faces and
faces whose far-upwind node is outside fall back to upwind exactly as in
SIMPLE; near the body the stencil reads ghost values, which encode the
wall. Verified: the embedded-circle MMS error drops 10–16x below upwind
(8.16e-4 vs 8.49e-3 at n = 32) at observed order 1.56 (SIMPLE's TVD
measured 1.59–1.84).

tests/turek_hron_cfd23.rs — CFD2 (Re 100, steady) and CFD3 (Re 200,
periodic), both with the benchmark's inflow ramp, both measured as time
statistics over a window (never a snapshot), surface route primary and
the control volume printed as the diagnostic (its central-difference
evaluation truncation grows with the convective flux: the routes agree
to 0.6% at Re 20 and differ 15–25% at Re 100–200 on these grids).

Measured across h = 10 / 6.6 / 5 mm:
- CFD3 shedding frequency 4.2746 / 4.3400 / 4.3939 Hz vs the reference
  4.3956 — converging −2.8% -> −1.3% -> −0.04%;
- CFD3 lift mean −184 / +160 / −2.6 vs −11.9 — lands on the reference;
  lift amplitude ±438 / ±556 / ±557 vs ±437.8 — +27% at the finer grids,
  unconverged (the flag is 2/3/4 cells thick);
- CFD2 control-volume drag 152.4 / 143.3 / 139.4 vs 136.700 — +2.0% at
  5 mm; CFD2 surface drag sits ~−10% (the boundary layer is ~one cell);
  CFD2 lift −3.4 / +30.2 / +8.4 vs 10.53.
Suite defaults run CFD2 at ny = 62 and CFD3 at ny = 41 (cost); the
asserted bands are the measured ones (frequency 10%, mean drag 15%,
amplitude 35%), not accuracy claims; RTX_CFD2_NY / RTX_CFD3_NY run the
studies.

Also recorded: the CFD1 refinement study extended to h = 3.3 mm
(RTX_CFD1_NY): control-volume drag 14.8996 (+4.25%), apparent order
~0.70 sustained over four grids, control-volume lift 1.1332 vs 1.11905
(+1.3%).

rtx-cfd 318 -> 321 green (full suite 321 passed / 0 failed).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 11:37:04 -07:00
Omar SobhandClaude Fable 5 9969d8a661 feat(backend): add differentiable index_select / index_add row ops
Add two row-indexing ops along dim 0 to the `Backend` trait so gather /
scatter-add message passing (GNNs, segment softmax, bias tiling) can be
trained through `Autodiff<B>`:

- `index_select(tensor, indices)` — out[i, ..] = tensor[indices[i], ..]
- `index_add(tensor, indices, num_rows)` — out = zeros; out[idx[i], ..] += tensor[i, ..]

They are each other's adjoint, which is what the backward passes use.

Both trait methods have default bodies (host round-trip via to_data /
from_data) so every existing backend keeps compiling and is correct;
backends override with native kernels:

- rtx-backend-cpu: new ops/index.rs (rayon-parallel gather over output
  rows above a size threshold, sequential deterministic scatter-add),
  wired into CpuBackend and CpuBackendF64, with unit tests for D=1/2/3,
  duplicates, untouched rows, empty inputs, bounds panics and adjointness.
- rtx-autograd: Autodiff<B> overrides both ops and records
  IndexSelectBackward / IndexAddBackward (new ops/index.rs); finite-
  difference gradchecks on the real CpuBackend cover repeated-index
  accumulation, untouched-row zero grads, bias tiling via index_select
  of a [1,F] row, and a full per-segment softmax.
- rtx-fusion: forward both ops to the inner backend.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 11:05:42 -07:00
Omar SobhandClaude Fable 5 327da7ff47 rtx-cfd: multigrid-PCG projection — 30x faster, same answers — and the CFD1 refinement study
Performance Benchmarks / Run Benchmarks (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
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
Falsifier 4 of the Turek–Hron geometry decision fired (the SOR projection
cost 0.09 s/step at 250x41 and an hour per run at 5 mm); this answers it.

solvers::incompressible::poisson: PoissonProblem (cell-centred five-point
SPD operator as per-cell face coefficients + Dirichlet diagonal extra +
active mask) and solve_multigrid_pcg — conjugate gradient preconditioned
by one V-cycle of geometric multigrid: aggregation by 2 per direction (odd
sizes absorbed, coarse cell active iff any child is), the Galerkin coarse
operator for piecewise-constant prolongation / summation restriction,
symmetric Gauss–Seidel smoothing, coarse correction scaled by 2 (Braess's
under-correction of unsmoothed aggregation; scalar, so the preconditioner
stays symmetric and positive on range(A)), L1 TRUE-residual stop with a
stagnation guard. Singular systems are handled per connected component of
the active cells (mean projection and level per pure-Neumann component;
the anchor's component to p[anchor] = 0). PoissonSolverKind::{Sor,
Multigrid} on PisoParameters / EmbeddedParameters; Sor is the default and
its code is byte-for-byte untouched; an unconverged multigrid solve falls
back to the SOR sweeps for that projection.

Verified (poisson/tests.rs, tests/poisson_equivalence.rs):
- PCG iterations to cut the residual 1e-8 on the closed Neumann box at
  32^2..256^2: 4, 4, 4, 4; ragged masked domains 8/8/8;
- manufactured recoveries to ~1e-14; Galerkin identity A_c v = R A P v to
  7e-15 on every level (masked, outlet column, non-uniform conductances);
  V-cycle symmetric to 1e-14; NaN-poisoned inactive cells untouched;
- two Neumann components with opposite imbalances, and a Dirichlet
  component beside an imbalanced Neumann one (review scenarios): converge,
  each component right up to its own constant;
- speed vs plain SOR at the same stop: 22.7x (128^2), 41x (256^2);
- same answers as SOR: PISO MMS 4.6e-8 relative, Taylor–Green divergence
  1.4e-9 every step, embedded-circle MMS 7e-8, no-body bit-identity with MG
  on both solvers, channel+outlet+circle 1.4e-10; CFD1 loads identical to
  four digits at 0.003 s/step vs 0.094 (30x).

CFD1 refinement study (tests/turek_hron_cfd.rs, three grids, 257 s):
h = 10 / 6.6 / 5 mm -> control-volume drag 15.6156 / 15.2829 / 15.0988 vs
14.2929 (+9.25 / +6.93 / +5.64%), apparent order 0.71, Richardson
extrapolate 14.04; surface route and lift not monotone (flag 2/3/4 cells
thick) — the test asserts the measured band at the finest grid.

Built with a 4-agent workflow (core, integration, refinement study,
adversarial review); the review found no defects and four risks, three
fixed here (per-component projection, one symmetric smoother-sweep
parameter, acting on `converged` with an SOR fallback) and one recorded
(isotropic aggregation loses grid-independence on anisotropic cells).

rtx-cfd 301 -> 318 green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 10:20:25 -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 4bd98b5264 rtx-cfd + rtx-fsi: the added-mass piston — partitioned FSI on the real ALE fluid
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 first coupled fluid-structure computation in the workspace, verified
against a closed form, and the first time rtx-fsi's added-mass claims run
against a real discretised fluid rather than a linear model map.

ALE extensions: per-side boundaries (Velocity / SlipWall / PressureOutlet)
and moving boundary lines. A moving Velocity side is a material wall whose
prescribed normal velocity must equal the line's own motion; a pressure
outlet takes Dirichlet p' = 0 in the projection (replacing the Neumann
anchor) with a zero-gradient predictor on its faces.

Fluid half verified alone (tests/ale_piston_channel.rs): prescribed piston
motion, slip walls, outlet. The incompressible rigid column is exact
DISCRETELY - continuity forces every u to the wall's discrete velocity
(8e-12) and the projected pressure is exactly linear with gradient rho
times the wall's backward-difference acceleration (2.5e-9).

Coupled benchmark (rtx-fsi/tests/piston_added_mass.rs): elastic piston
(Newmark average acceleration) against added mass rho*L*H at mass ratio
6.25, rtx-fsi's Subiterated driving a real fluid/structure pass per step:
- plain staggered diverges in 7 subiterations (Causin-Gerbeau-Nobile on a
  real solver);
- Aitken converges at 3.0 subiterations/step onto T = 1.07009 vs the
  closed form 1.06999 - 9.8e-5 relative, halving with dt;
- outlet flux matches the piston sweep to ~1e-9 every step.

Discrete-analysis finding: Newmark beta scales the staggered added-mass
threshold - the iteration gain is beta*m_a/(M + K*beta*dt^2), so the
continuous ratio 2.5 CONVERGES at beta = 1/4 (gain 0.625, measured ~17
passes/step) and the benchmark needs ratio 6.25 (gain 1.56).

Two real defects found and fixed, twelfth and thirteenth of the campaign:

1. rtx-cfd ale::advance re-stamped boundary faces at t_old from the
   current boundary function, which in a coupling loop carries the NEW
   interval's wall velocity - the predictor's old state had interior
   u = w0 but wall face u = w1, leaving an O(dt) pressure artifact
   confined to the wall-adjacent cells (p exact to 6e-11 everywhere
   except the wall cell at 4.7e-5). The start-of-step boundary faces are
   whatever the previous step's end-of-step application left there.

2. rtx-fsi aitken_factor guarded its denominator - a SQUARED residual-
   difference norm - against a bare f64::EPSILON, silently disabling
   Aitken below residual ~1e-8 and degrading to unit relaxation exactly
   in the well-converged regime; the repulsive fixed point then amplified
   1e-9 residuals back up and the coupling diverged. Third instance of
   the absolute-threshold species (NNLS, ECSW). The guard is relative
   now; aitken_is_scale_invariant pins it at initial residual 1e-9.

rtx-cfd 293 green (+1), rtx-fsi 29 green (+3). rtx-fsi's lib gains only
the relative guard; the coupling layer still depends on no solver
(rtx-cfd is a dev-dependency of its tests).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 06:10:13 -07:00
Omar SobhandClaude Fable 5 259c5baa63 rtx-cfd: ALE on a moving tensor-product grid, DGCL-exact by construction
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
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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
The first brick of the Turek-Hron frontier: PISO (explicit conservative
predictor + SOR projection) generalised to a staggered grid whose x- and
y-lines move arbitrarily each step while the domain boundary stays fixed.

The discretisation choice that carries everything: time-averaged face
areas (A^n + A^{n+1})/2 in both the fluid fluxes and the face-swept
volumes. For tensor-product motion the discrete geometric conservation
law then holds as an algebraic identity, so uniform flow is a
machine-precision fixed point, not a truncation-order one:

- DGCL test: uniform (0.7, -0.4) on a 16x12 grid with interior lines
  wiggling out of phase, 400 steps: max deviation 7.9e-15 (~35 ulp).
  Negative control with end-of-step areas (per-step cell error exactly
  dw*dh/V, the cross term the identity absorbs): 1.5e-2 - a 1e12
  separation, so the test can fail.
- Degeneracy: zero motion on a uniform grid vs fixed-grid PISO over
  Taylor-Green steps: max difference 2.2e-16 - one ulp - pinning every
  geometric generalisation to the verified implementation.
- Physics under motion: Taylor-Green on the wiggling mesh, L2 error
  2.42e-2 -> 1.07e-2 (n=16 -> 32, order 1.17); moving-mesh error at
  n=32 sits below the fixed-mesh 1.1532e-2 (PISO's published value to
  four digits); energy decay unchanged by the motion.

One trap documented in the test: the projection's inner-stop floor
(0.1 * tolerance * reference_flux) at an engineering tolerance lets a
one-sweep partial p' accumulate into p, whose gradient perturbs the
velocities at ~1e-11 with the geometry blameless. The DGCL run must use
a rounding-level tolerance because machine-precision preservation is the
claim under test. Measured: 3.6e-11 at tol 1e-9, 7.9e-15 at 1e-13.

Incompressibility needs no mesh-velocity term: subtracting the GCL from
moving-cell mass conservation leaves plain div(u) = 0 on the current
geometry, so the projection is the fixed-grid one with non-uniform
coefficients.

292 rtx-cfd tests green (288 + 4).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 05:31:12 -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 b321a9aba7 rtx-cfd: Taylor-Green validates PISO's transient path — and fixes the projection's inner solve
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
With k = pi the decaying Taylor-Green vortex has zero normal velocity on
the unit box for all time, so it fits the closed staggered domain exactly,
with ZERO body force: convection is balanced identically by the true TG
pressure and the decay comes from viscosity alone. This exercises exactly
what the steady MMS harness cannot see — the time derivative, the unsteady
pressure coupling and the projection's splitting error. The time-decaying
tangential wall velocity enters by re-setting the wall hook each step.

Measured (16/32/64, dt ~ h^2): L2 velocity 2.267e-2, 1.153e-2, 5.841e-3 —
orders 0.97 and 0.98, first-order upwind's rate — and the kinetic-energy
deficit against the exact e^(-4 nu pi^2 T) halves per refinement
(0.0690, 0.0360, 0.0185; ratios 1.92, 1.95), within 2.3% on the finest
mesh. Every step divergence-free to ~1e-7.

Its first run caught two defects in the projection's inner solver:

- The inner Gauss-Seidel stop summed the per-sweep iterate CHANGE — the
  same movement-not-residual pseudo-criterion the SIMPLE census flagged:
  slow modes move little per sweep while their residual is still large.
- Plain GS contracts smooth modes by only 1 - O(h^2) per sweep, so the
  400-sweep cap left max |div u| ~ 1e-2, GROWING with mesh size (8e-3 at
  16^2 to 2e-2 at 64^2).

The inner stop now measures the true equation residual, the sweep is SOR
at the optimal Poisson factor omega = 2/(1 + sin(pi h)), and it converges
relative to each projection's own source with a floor tied to the outer
mass tolerance — so a long steady march no longer burns a hundred sweeps
per step polishing negligible corrections. The steady MMS harness had
masked all of this: a march to steady state iterates the projection to
death regardless, which is why its divergence read 1e-9 while a 205-step
transient left 1e-2.

mms_piso's steady-state criterion is 1e-6 (was 1e-7): per-step projection
noise at the mass tolerance floors |du/dt| just below 1e-6, and the L2
errors under measurement are 1e-2 to 1e-3. Its results are unchanged to
six figures and still match SIMPLE's.

288 rtx-cfd tests, 0 failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-20 00:35:06 -07:00
Omar SobhandClaude Fable 5 d8a30db155 rtx-cfd: Ghia Re=400 as a quantitative claim — and the stopping-tolerance trap
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
The Re=400 lid-driven cavity, 128^2 TVD on an exactly-unit domain, sampled
on the staggered faces that lie exactly on the centrelines, against Ghia,
Ghia & Shin (1982) — reference values sourced from two independent
transcriptions that agree digit for digit (Mramor/Vertnik/Sarler CMC 2013
Table 1, and the ivan-pi benchmark collection):

    u_min  -0.32667 at y = 0.2852   (Ghia: -0.32726 at 0.2813 — 0.18%)
    v_min  -0.45024 at x = 0.8633   (Ghia: -0.44993 at 0.8594 — 0.07%)
    v_max   0.30044 at x = 0.2305   (Ghia:  0.30203 at 0.2266 — 0.53%)
    max |u - Ghia| over 15 profile stations: 0.0051

Ghia's own values carry ~0.3% discretisation error (Sahin & Owens 2003 put
u_min at -0.32838), so this is at the reference's own accuracy.

The finding worth the commit message: the first run used the Re=100 test's
residual tolerance of 1e-4 and read u_min = -0.31987 — "converged", 2.3%
shy — and refining to 192^2 made it WORSE (-0.30879, profile deviation
doubled from 0.034 to 0.074). The residual had dropped below tolerance
while the field was still developing, and the effect grows with mesh size
because SIMPLE's per-iteration contraction weakens as h -> 0: at fixed
residual tolerance the finer mesh stops at an EARLIER stage of convergence.
Tightening the stop (3e-5, then 1.5e-5, until the movement per halving fell
below the reference's own error) was the fix, and the test's bands are set
so the premature-stop state fails all of them. "The residual converged"
must never stand in for "the answer stopped moving".

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 23:23: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 e94ad1be6b rtx-cfd: Poiseuille closed-form validation with exact-zero assertions
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
Plane channel driven by a uniform body force: u(y) = G/(2mu) y(1-y), v = 0,
p exactly constant. Convection vanishes identically, so this isolates
diffusion, the half-cell wall treatment and the pressure coupling — and two
of the three answers are exact zeros, which no benchmark comparison offers.

The ends are clamped to the profile the DISCRETISATION prefers — the 1-D
tridiagonal with half-cell wall closures, solved directly in the test —
rather than to the continuous parabola. That makes (u_hat, 0, const) an
exact fixed point of the 2-D discretisation, and the solver must sit on it:

    |u - u_hat| ~ 1e-10,  max |v| ~ 1e-10,  p spread ~ 8e-10   (16^2)

A first version clamped the ends to the continuous parabola instead; the
O(h^2) incompatibility between that profile and the discrete one drove a
weak secondary flow near the ends (max |v| = 1.3e-3) — a property of the
mismatched boundary data, not of the solver, recorded in the test docs so
nobody rediscovers it as a bug.

The wall treatment's own truncation is measured in isolation as
|u_hat - parabola|: 3.906e-3 at 16, 9.766e-4 at 32 — refinement ratio
exactly 4.00, second order, in closed form c h^2 / 4.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 19:32:37 -07:00
Omar SobhandClaude Fable 5 9b097fca0d rtx-cfd: PISO validated by manufactured solution — after fixing the inverted projection
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
PisoSolver was the only major solver in the workspace with no verification
of any kind. Writing the MMS harness for it (tests/mms_piso.rs) and
inspecting the implementation found the census's defect species again:

- The pressure correction had its SIGN inverted: it solved
  -lap(p') = +rho div(u*)/dt and then corrected with u = u* - (dt/rho)
  grad(p'), so each projection DOUBLED the divergence instead of removing
  it.
- The momentum sweeps froze the near-wall lines (1..ny-1) and the pressure
  correction skipped the outer ring of cells (1..nx-1) — both exactly the
  defects repaired in SIMPLE.
- The "explicit" predictor read neighbours the same sweep had already
  overwritten, so the step depended on sweep order.
- The pressure gradient was dropped entirely on the last interior face.

Rewritten as a genuinely explicit predictor plus anchored-Neumann
projection on the staggered grid, with the conventions SIMPLE now embodies:
near-wall lines are unknowns with half-cell wall diffusion, continuity on
every cell, boundary faces are prescribed data. Momentum-source and
wall-velocity hooks added so the manufactured solution can reach it.

Measured (16 -> 32 -> 64): L2 velocity 3.516214e-2, 1.953750e-2,
1.037512e-2 — orders 0.85 and 0.91, first-order upwind's rate — with
max |div u| ~ 1e-9 in every cell. The errors agree with SIMPLE's on the
same meshes to six or seven significant figures: an implicit under-relaxed
outer iteration and an explicit time-marching projection land on the same
discrete steady solution, which is what sharing a spatial discretisation
must produce and is very hard for two independently wrong solvers to fake.

285 tests, 0 failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 19:28:39 -07:00
Omar SobhandClaude Fable 5 796cf173e6 rtx-cfd: second-order convection by deferred-correction TVD; MMS order 1.84, cavity closes on Ghia
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
First-order upwind's O(h) numerical viscosity was the measured limit on the
whole discretisation: MMS order ~0.9 at Re = 20 against 2.05 in the Stokes
limit. This adds a ConvectionScheme parameter to SimPLE — Upwind (default,
behaviour unchanged), TvdVanAlbada, TvdVanLeer — implemented by deferred
correction: the upwind operator stays implicit, so a_p = sum(a_nb) and
diagonal dominance survive unconditionally, and the limited
high-order-minus-upwind flux difference enters the source explicitly at the
current iterate. At a fixed point the two agree, so the converged answer is
the TVD discretisation. Faces whose far-upwind node lies outside the domain
fall back to pure upwind; wall faces pass no mass, so no correction enters.

Measured by the manufactured solution (van Albada, 16 -> 32 -> 64):

    L2 velocity   1.325e-3   4.406e-4   1.232e-4    orders 1.59, 1.84
    (upwind)      3.516e-2   1.954e-2   1.038e-2    orders 0.85, 0.91

The error is 27x to 84x below upwind's at equal resolution, the order climbs
toward 2 (the shortfall is limiter clipping plus the boundary fallback, both
of which shrink with h), the pressure error falls at the same rate, and
continuity still holds to solver tolerance in every cell.

On the Re = 100 lid-driven cavity at 65^2 the centreline minimum moves from
-0.1932 (upwind) to -0.2036 against Ghia's -0.2109 — 59% of the remaining
gap closed at equal resolution, converged in 790 iterations — and the vortex
position moves from 0.5000 to 0.4844 toward Ghia's 0.4531. Both new cavity
bounds exclude the upwind values, so falling back to first order fails them.

284 tests, 0 failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 19:16:48 -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 1a740e0b2c rtx-cfd: the wall treatment is second order, not first — correct the record
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
The manufactured-solution test carried a hypothesis for why the observed
order sits below 1: that `(u_P - u_wall)/(dy/2)` approximates the wall
gradient at y = dy/4 rather than at the wall, making the near-wall rows
first order.

Measuring in the Stokes limit refutes it. With convection negligible every
remaining operator is second order, so the observed rate there reports the
wall treatment directly:

    rho = 1.000  (Re = 20.00)   3.52e-2  1.95e-2  1.04e-2   orders 0.85 0.91
    rho = 0.001  (Re =  0.02)   2.21e-3  5.35e-4  1.28e-4   orders 2.05 2.06

2.05 and 2.06. The half-cell wall term is second-order accurate and the
Stokes discretisation reaches its nominal rate. The shortfall at Re = 20 is
first-order upwind and nothing else, which is what a first-order convection
scheme is supposed to give.

Comment corrected rather than left standing: a plausible explanation that
happens to be wrong is worse than none, because it sends the next person
to fix something that is not broken.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 17:42:14 -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 b5814a304f rtx-cfd: manufactured solution finds the diffusion conductances were 1/h too
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
large

Applies MMS to the SIMPLE solver. It found a major discretisation error on
the first run, which is the point of the method.

The diffusion conductances read `mu / dx` and `mu / dy`. Finite volume
requires `Gamma * A / delta` — the face area over the distance between the
nodes it separates — so they should be `mu * dy / dx` and `mu * dx / dy`.
The face area was missing entirely, making viscosity too large by a factor
of `1/h`: sixty-five times on a 65x65 mesh. Every other term in the
equation was already a force (`dp * dy` for pressure, `rho u dy` for the
convective flux), so the mismatch was confined to diffusion.

The consequence was that the solver ran at an effective Reynolds number
far below the one requested. Before the fix the manufactured-solution
error did not reduce under refinement at all — observed order about -0.05,
because the spurious viscosity grows with the mesh. After it, the error
falls monotonically.

This also explains an apparent regression that is really a correction.
The cavity vortex position moved from y = 0.484 to y = 0.391 against
Ghia's 0.4531, which reads as worse agreement. It is not: a strongly
over-diffusive cavity approaches Stokes flow, whose vortex sits near
mid-height, so the old number was closer to the reference than the scheme
deserved. Correcting the viscosity exposed the discretisation's own error.
The test now states that disagreement plainly rather than asserting a band
around the reference.

What MMS reports now, and it is not yet good enough:

    n = 16   L2 velocity error = 2.586104e-1   order    -
    n = 32   L2 velocity error = 1.797373e-1   order 0.52
    n = 64   L2 velocity error = 1.277188e-1   order 0.49

First-order upwind should give 1. It gives about 0.5, and the u component
is markedly further from exact than v on the same mesh. Both say there is
at least one more defect in the discretisation or its boundary treatment,
and the asymmetry between the two momentum equations is the clue. The test
asserts only monotone error reduction — what is established — and records
the shortfall, because asserting a rate the solver does not achieve would
either redden the suite or invite someone to weaken it later.

This changes the plan: raising the observed order to 1 is now a
precondition for the second-order convection work rather than a
consequence of it. There is no value in adding a higher-order scheme to a
discretisation that has not demonstrated first order.

Supporting changes:

  - `SimpleSolver::set_momentum_source` applies a volumetric body force,
    which is what lets a manufactured solution be imposed at all.
  - Divergence is now detected by growth, not only by NaN. The 8x8 case at
    Reynolds 10^6 reached 1e149 before anything caught it, because
    `is_finite` stays true right up until it does not.
  - `test_simple_solver_workflow` specified water properties on a unit
    domain, which is Reynolds 10^6 on ten cells: no steady laminar
    solution exists and the solver diverges on it, correctly. It passed
    only while the excess diffusion stabilised it. Now set to Reynolds 100.

561 tests across the three crates, 0 failing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 12:25:52 -07:00
Omar SobhandClaude Opus 5 10e5f9cb90 rtx-cfd: fix the cell-centre velocity interpolation, which was half a cell out
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
`get_velocity_at` averaged u-faces `i - 1` and `i` to report the velocity
at cell `i`. On this staggered layout `u` is `(ny, nx + 1)` and cell `i`
is bounded by faces `i` and `i + 1` -- which is the convention
`compute_mass_source` uses to form the divergence, and therefore the one
that defines the grid. The two disagreed by one index.

Consequences: every profile read through this function was shifted half a
cell west of the field the solver actually computed, the first and last
cells were special-cased to a single face, and the outermost face was
never read at all.

It is a diagnostic path rather than a solve path -- the residuals are
byte-identical before and after -- but the cavity comparison against Ghia
is taken through it, so the reported vortex position was affected. The
corrected grid study, unchanged in the solve:

    n      u_min      y
    17^2   -0.1257    0.4375
    33^2   -0.1550    0.4688
    65^2   -0.1743    0.4844
    97^2   -0.1825    0.5000
    Ghia   -0.2109    0.4531

The shift matters most where the grid is coarse and washes out under
refinement, which is what a half-cell offset should do.

Found while establishing where each staggered variable physically sits, a
prerequisite for applying the method of manufactured solutions to this
solver.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 12:10:41 -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 03a9bdf41f rtx-cfd: apply boundary conditions to u* before using its divergence
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
Closes the relaxation-factor dependence. Converged solutions are now
identical for velocity relaxation 0.3, 0.5, 0.7 and 0.9 -- bit for bit --
where they previously spread 18%.

The cause was ordering, not formulation. Boundary conditions were applied
only at the end of the iteration, so `copy_to_starred` snapshotted a
predicted field whose boundary faces held whatever the momentum sweep had
written there: values the wall overwrote with zero moments later. The
divergence of that field is the pressure equation's source, so those
un-constrained faces entered it as a spurious mass source, concentrated at
the two lid corners where the moving lid meets a stationary wall. The
swept value scales with the relaxation factor, so the spurious source did
too -- and so did the answer.

Diagnosis is worth recording because the symptom pointed away from the
cause. At the stalled state the interior momentum equations were satisfied
to machine precision at every relaxation factor: a fresh Gauss-Seidel
sweep moved the interior by 1e-15, the pressure correction was 1e-14, and
the momentum residual was 3.6e-16. The entire residual floor lived in the
*mass* term, and only that term varied with alpha -- 3.7e-4 at 0.3 against
1.6e-4 at 0.9. Each relaxation factor was converging honestly, to the
solution of a slightly different problem.

The correction also moves the cavity substantially closer to the reference,
because the spurious corner source had been suppressing the recirculation:

  grid    before    after     Ghia (1982)
  17^2    -0.068    -0.123    -0.2109
  33^2    -0.109    -0.154
  65^2    -0.142    -0.174
  97^2    -0.157    -0.182

Richardson extrapolation on the two finest grids now gives about -0.199
against Ghia's -0.2109, within 6%, with the remaining gap consistent with
first-order upwind's numerical viscosity. The residual floor falls roughly
linearly with mesh size (1.6e-3, 5.2e-4, 1.6e-4, 7.8e-5), which is the
signature of the corner singularity rather than of an unconverged solve --
the same one Botella & Peyret (1998) subtract analytically.

Two further fixes fell out of it:

  - The solver returned NaN rather than reporting divergence. Asked for an
    8x8 cavity at a Reynolds number of a million it now stops, says it did
    not converge, and reports the last finite residual, instead of handing
    back a field of NaN that poisons everything downstream. Previously the
    false transient's large diagonal damped that case into crawling rather
    than diverging, which hid it.

  - `apply_boundary_condition` used `start_index` where it meant
    `end_index` for the bottom wall. The other three arms are correct; with
    both indices unset the default masked it.

558 tests across the three crates, 0 failing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 10:27:33 -07:00
Omar SobhandClaude Opus 5 2db4e28760 rtx-cfd: make SIMPLE a steady solver; the converged answer no longer depends
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
on the pseudo-time step

Acting on a literature pass. Standard SIMPLE is a steady-state algorithm:
it has no pseudo-time term, and stability comes from under-relaxation
folded implicitly into the momentum coefficients. Ours had a false
transient *and* an explicit post-hoc blend of the whole field, which is
why the converged cavity solution varied with `time_step` -- something a
steady state cannot legitimately do.

Four changes, in the order they mattered:

1. The convergence measure was `|u - u_old|`, the change between
   successive iterates. That is not a residual: it reports how far the
   iteration moved, which depends on how heavily it is damped, and the
   damping was set by `dt`. Replaced with the imbalance of the discretised
   momentum equations, `|a_p u_P - sum a_nb u_nb - b|`, normalised by
   `sum |a_p u_P|` as CFD solvers conventionally report it. An
   unnormalised sum grows with the cell count and with `dt` through
   `a_p0`, so the same numeric tolerance meant a different thing on every
   grid.

   The residual is measured against the *unrelaxed* equation. Relaxation
   inflates the diagonal by 1/alpha and adds a matching source; reporting
   the relaxed system's residual makes one tolerance correspond to a
   different true error for each alpha.

2. Steady by default: `a_p0 = 0`, and Patankar's implicit under-relaxation
   -- `a_p / alpha` with `(1-alpha)/alpha * a_p * u_prev` added to the
   source. At a fixed point the two cancel exactly, so the converged
   solution is independent of alpha by construction. The explicit velocity
   blend is removed; it relaxed a second time and undid part of the
   continuity the pressure correction had just enforced. `steady: false`
   restores the transient term for genuinely time-dependent problems.

   Result: dt = 0.001, 0.01 and 0.05 now give bit-identical fields.

3. Dropped the net convective flux from `a_p`. It vanishes identically
   once continuity holds, but during the iteration it does not, and it can
   exceed the sum of the neighbour coefficients -- driving `a_p` through
   zero and the solve to NaN, which is what the workflow tests hit once
   `a_p0` was no longer there to mask it. Omitting it is what makes
   `a_p = sum a_nb` positive unconditionally.

4. Anchored one cell of the pressure correction. With velocity prescribed
   on every boundary the pressure equation is pure Neumann and singular;
   `p'` is fixed only up to a constant and Gauss-Seidel lets it drift.
   Enforcing solvability by subtracting the mean source is the textbook
   remedy and is wrong here -- this source is assembled from face fluxes
   that include the boundaries, so it need not sum to zero, and
   subtracting its mean injects a spurious source everywhere. Tried; it
   diverged. Anchoring a reference cell changes no pressure gradient,
   which is all the momentum equation uses.

Also measured, and it settles the open question about Ghia: the
under-prediction is numerical diffusion, not a defect. First-order upwind
carries a numerical viscosity of about |u| dx / 2, which at 65^2 is 0.0078
against a physical 0.01 -- an effective Reynolds number near 56, not 100.
Refinement moves the centreline minimum monotonically toward the
reference: -0.068 at 17^2, -0.109 at 33^2, -0.142 at 65^2, -0.157 at 97^2,
against Ghia's -0.2109, with the vortex position tracking 0.375 -> 0.406
-> 0.469 -> 0.490 against Ghia's 0.4531.

The cavity test moves to 65^2 and asserts the vortex position tightly
(0.40..0.52, Ghia 0.4531) while bounding the strength to the band
first-order upwind can reach there. Its tolerance is 1e-4 rather than
1e-6: the two lid corners hold a velocity discontinuity whose discrete
imbalance does not reduce with iteration, so the normalised residual
floors near 7e-5. That is a property of the problem -- the same
singularity Botella & Peyret (1998) subtract analytically -- and the
physical assertions, not the stopping rule, are what establish
correctness.

Still open: converged solutions retain a dependence on the relaxation
factor that the implicit formulation should have removed (-0.159 at
alpha=0.3 against -0.134 at alpha=0.9 on 65^2, each stable to six
decimals over 200k iterations). Recorded rather than papered over.

558 tests across the three crates, 0 failing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 09:47:46 -07:00
Omar SobhandClaude Opus 5 bfd9f4dfd2 rtx-cfd: repair the pressure-velocity coupling, LBM walls and mesh quality
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
Clears the rest of the quarantine. All three crates now run 558 tests
with 0 failures and no `#[ignore]` markers.

SIMPLE could not converge, and the reason was not slow convergence but
wrong physics.

The pressure correction equation used a bare Laplacian, 1/dx^2 and
1/dy^2, while the velocity correction divided by a_p = rho dx dy / dt.
SIMPLE requires these to be each other's inverse: substituting the
corrected velocities into continuity must reproduce the pressure
equation, which fixes a_E = rho d dy/dx with d = dV/a_p. The two
disagreed by roughly 1/(h^2 dt) -- about 2e4 on a 16x16 cavity -- so the
pressure correction was that many times too weak to enforce continuity.

The consequence was visible and specific. A lid-driven cavity at Re=100
produced a monotonic profile rising from 0 at the floor to 1 at the lid:
Couette flow, with no recirculation anywhere, and a peak pressure of
1.6e-4 against the rho U^2 scale of 1. The return flow in a cavity is
driven entirely by the pressure gradient, so with the pressure pinned
near zero there was nothing to turn the flow around. With the
coefficients made consistent the profile recirculates, the peak pressure
is 2.9, and the solver converges.

Also in SIMPLE:
  - `p'` was never reset between outer iterations. It is a correction
    that `pressure_update_step` folds into `p`, so carrying it forward
    applied the same correction twice.
  - The convergence measure was the inner Gauss-Seidel residual, which
    goes to zero whether or not the flow satisfies continuity. Now the
    mass imbalance.
  - The velocity correction used only the transient part of a_p,
    `rho dV/dt`, rather than the diagonal the momentum equation was
    actually solved with.
  - All four convective face fluxes were computed from a single
    cell-centred velocity, so `fe` and `fw` were the same number, as were
    `fn` and `fs`. Upwinding then picked the same direction on opposite
    faces of the control volume. Now interpolated per face on the
    staggered grid.

Not claimed: agreement with Ghia, Ghia & Shin (1982). The vortex centre
moves toward their y = 0.4531 under refinement (0.400 at 16^2, 0.419 at
32^2, 0.460 at 64^2) but the minimum centreline velocity reaches only
-0.130 against their -0.2109, and the converged field still depends
slightly on the pseudo-time step, which a true steady state cannot. The
cavity test therefore asserts what is established -- convergence,
recirculation, vortex position, and an O(1) pressure field -- and the
remaining gap is recorded in omni-cortex/docs/solver_status.md rather
than papered over with a loose tolerance.

LBM bounce-back was doing neither of the things its name claims. It was
written as assignment (`f[2] = f[4]`) rather than a swap, discarding the
population being reflected -- bounce-back is a permutation and conserves
mass exactly, so the domain leaked 0.013% of its mass every 100 steps and
would have kept draining. And the pairs used were 5<->8 and 6<->7, which
reverse only the wall-normal component: that is specular reflection, a
free-slip wall, so the no-slip condition the walls were supposed to
impose never held.

Mesh quality:
  - Quadrilateral aspect ratio included the diagonals in the maximum but
    not the minimum, so it could never return 1: a unit square reported
    sqrt(2) and a 2:1 rectangle sqrt(5).
  - Triangle aspect ratio used longest-over-shortest edge, which does not
    detect the failure mode that matters. A sliver with vertices (0,0),
    (10,0), (5,0.1) scores 2.0 -- indistinguishable from a healthy 2:1
    triangle -- while its area is a twentieth of what its edges suggest.
    Now the radius ratio R/2r, which is 1 for equilateral and 1250 for
    that sliver, and which also fixes the quality histogram.
  - StructuredMesh aspect ratio took bounding-box extents and guarded the
    z-extent with `.max(1e-10)`. On a 2-D mesh the depth is exactly zero,
    so the guard became the minimum and a unit square reported 2e10.

Mesh refinement produced meshes that failed their own validation.
`subdivide_triangle` reserved midpoint ids as `next_node_id + k`, then
advanced the counter by 3, after which `refine_cells` called `add_node`
and advanced it three more -- so every refined cell referenced vertices
three ids away from the ones actually created. Separately, the position
lookup selected by slot rather than by id ("This is simplified, should
look up correct midpoint"), so three of four sub-triangles had their
areas computed from the wrong points; the quadrilateral version mapped
every new id to the cell centre.

Fixtures corrected rather than tolerances loosened: a structured mesh
test asserted 0.16 for the average cell volume while the comment beside
it computed 0.25 from the node-count convention the code actually uses;
the Zou-He pressure test built a *velocity* boundary at u = 1.2, far
above the lattice speed of sound, making the density negative; and the
cavity-setup test required the lid to influence the domain centre 16
rows away in 10 steps, which exceeds the lattice propagation speed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 08:40:09 -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
Omar SobhandClaude Opus 5 9be5f4a68f rtx-fsi: partitioned fluid-structure coupling
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
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
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
rtx-cfd (18,715 lines) and rtx-fea (36,576 lines) both exist and nothing
connects them -- rtx-fea is commented out of rtx-cfd's dependencies. This
is the coupling layer, and it is the piece Prof. Charbel Farhat's 2026
Guggenheim Medal citation is actually about.

It depends on NEITHER solver. The properties that make a partitioned
coupling correct -- conservation of force, moment and interface work --
are statements about the transfer operators alone, so they can be
validated now, on solvers whose canonical-benchmark validation is still
outstanding. Adapters to the concrete solvers belong above this.

TRANSFER (transfer.rs). Weights satisfy two constraints:
  sum(w_i) = 1            partition of unity  -> force conserved
  sum(w_i x_i) = x_face   linear reproduction -> MOMENT conserved

The second is the one that gets skipped. Inverse-distance weighting
satisfies the first and generally violates the second, conserving force
while corrupting moment -- which shows up as slow spurious rotation rather
than as an obvious error. Underdetermined for >4 nodes, so it takes the
minimum-norm solution w = A^T (A A^T)^+ b.

That is a PSEUDO-inverse, and not for defensiveness. A wetted surface is a
surface, so its nodes are usually planar, and for a planar patch the z
constraint row is an affine multiple of the ones row -- A A^T is genuinely
rank-deficient. The constraint is redundant there, not unsatisfiable. An
ordinary inverse rejects the most ordinary interface there is; I found
this because my first test fixture was collinear and the code correctly
refused it. Constraints are then verified against the weights actually
obtained, since a pseudo-inverse returns a least-squares answer whether or
not the system was consistent.

Motion transfer uses the TRANSPOSE of the load operator, which makes
interface work conserved identically: (Hf).v = f.(H^T v). Any other
pairing leaks energy every step, and the leak looks like physics until it
destabilises.

COUPLING (coupling.rs). Staggered and Aitken-relaxed subiteration. The
decisive tests reproduce the added-mass effect: at a gain of 2.5 the
fixed-relaxation scheme DIVERGES and is reported as CouplingDiverged
rather than as an exhausted budget, and Aitken recovers the same case. A
partitioned coupling that cannot reproduce its own classic failure mode is
not being tested hard enough. Aitken is exact for a linear fixed point, so
convergence is asserted at <=4 iterations -- pinning that this is the real
delta-squared formula and not an under-relaxation that happens to work.

SCOPE, stated up front in the crate docs: small-displacement transpiration
coupling on a fixed mesh. Deliberately not ALE and not embedded-boundary,
so the Discrete Geometric Conservation Law does not yet apply -- the mesh
does not move. Large motion needs an embedded boundary treatment; that is
the next phase, not an oversight.

External comparator named at entry: Turek-Hron FSI2/FSI3, not yet reached.

26 tests written red-first; cargo test/fmt/clippy -D warnings clean.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 06:49:36 -07:00
osobhandClaude Sonnet 5 5155c081ca feat(mamba): GPU-accelerated backward pass (backward_cuda)
GPU Tests / Check GPU Availability (push) Successful in 0s
GPU Tests / CUDA Tests (11.8) (push) Skipped
GPU Tests / CUDA Tests (12.1) (push) Skipped
GPU Tests / Metal Tests (push) Skipped
CI / Clippy Check (push) Failing after 7s
CI / Build (ubuntu-latest) (push) Failing after 8s
Documentation / Build User Guide (push) Successful in 9s
CI / Format Check (push) Failing after 15s
CI / Build (macos-latest) (push) Failing after 20s
CI / Test (macos-latest) (push) Skipped
CI / Test (ubuntu-latest) (push) Skipped
CI / Python Bindings (maturin) (macos-latest) (push) Skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Skipped
CI / WASM Build + Size Check (push) Skipped
CI / Distributed Training Tests (push) Skipped
CI / Build CPU-Only (Explicit) (push) Failing after 1m48s
CI / CI Success (push) Failing after 1s
Documentation / Build API Documentation (push) Failing after 2m29s
Performance Benchmarks / Run Benchmarks (push) Successful in 5m56s
MambaBlock::forward already had a working, tested CUDA dispatch
(forward_cuda: cuBLAS matmuls for projections, CPU for the scan).
backward() had none — it silently ran entirely CPU-serial on GPU
tensors via to_vec()/from_vec() D2H/H2D round-trips. This adds the
missing acceleration, mirroring forward_cuda's hybrid split: the
four large projection-parameter gradients (in_proj, x_proj, dt_proj,
out_proj) now go through batched GPU matmuls; the inherently
sequential scan reverse-pass and small per-channel grads stay CPU.

Extracted CpuWeights::pull and recompute_forward_cpu out of the old
inline per-batch forward-recompute block inside backward() (pure
refactor, gradient-checked unchanged by real_selective_scan.rs's
existing 6 tests) so CPU backward and the new CUDA backward share
identical forward math and can never numerically diverge on it.

New CUDA-vs-CPU gradient-check test (mamba_cuda_backward_matches_cpu,
#[ignore]-gated, GPU-only) caught a real bug during development:
Tensor::contiguous() is a no-op stub in this rtx-tensor version, and
cuda_matmul reads raw GPU storage by shape.dims() ignoring
strides/offset, so .transpose(..).matmul(..) on a GPU tensor silently
computed garbage (80-200x relative error on 3 of 4 accelerated
gradients). Fixed by building already-transposed [dim, b*l] buffers
on CPU before upload instead of transposing GPU-side. All 9 gradients
now match CPU backward within ~2.2e-5 max relative error (tolerance
1e-4).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-10 07:11:20 -07:00