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
This commit is contained in:
Omar Sobh
2026-08-24 19:14:13 -05:00
co-authored by Claude Fable 5
parent 555a72cbc0
commit d2c82af91b
5 changed files with 516 additions and 11 deletions
@@ -182,7 +182,8 @@ fn fsi2_flapping_flag() {
" FSI2 (fluid ny = {ny}, flag {flag_nx}x2 Quad8, dt = {:.2e}, {}): coupled {} \
steps in {:.0} s wall total; {:.1} subit/step (max {}); {} stalled steps, {} \
history-reset retries (worst residual {:.2e}); worst conservation {:.2e}; \
skipped samples {} (of which {} spike-clamped)\n measured over [{:.1}, {t_end:.1}] s: \
skipped samples {} (of which {} spike-clamped); Newton rescues {:?}\n \
measured over [{:.1}, {t_end:.1}] s: \
uy(A) = {:.4} ± {:.4} mm (ref {:.2} ± {:.1}), ux(A) = {:.4} ± {:.4} mm (ref {:.2} ± \
{:.2}), f = {} Hz (ref {REF_UY_FREQ}); onset amp {:.3e} -> {:.3e} m",
result.dt,
@@ -197,6 +198,7 @@ fn fsi2_flapping_flag() {
result.worst_conservation,
result.skipped,
result.spiked,
result.newton_rescues,
w.t_start,
w.uy_mid * 1e3,
w.uy_amp * 1e3,
@@ -245,6 +247,7 @@ fn fsi2_flapping_flag() {
// is pinned nowhere (measured to change nothing that matters).
let default_coupling = smooth_in_h == 0.0 && config.coupler == "aitken";
let mode2_coupling = smooth_in_h == 0.0 && config.coupler == "iqn" && subcycle == 2;
let s1_coupling = smooth_in_h == 0.0 && config.coupler == "iqn" && subcycle == 1;
if default_coupling
&& ny == 62
&& flag_nx == 35
@@ -304,5 +307,30 @@ fn fsi2_flapping_flag() {
(benchmark reference {REF_UY_AMP})",
w.uy_amp
);
} else if s1_coupling && ny == 62 && flag_nx == 35 && t_end >= 16.0 {
// Study horizons, subcycle 1: THE BENCHMARK CYCLE, hit 2026-08-24
// (run 3, floor 4e-6, MAXSUB 30; measured over [13, 16] s:
// uy 3.17 ± 81.68 mm at 1.925 Hz vs reference 1.30 ± 81.6
// [1.93] — 0.1% in amplitude, 0.26% in frequency; full record in
// omni-cortex solver_status.md §"C2 closed in displacement").
// The bands are deliberately generous: the s = 1 trajectory
// shifts legitimately whenever a structural-Newton rescue
// engages mid-march (runs 1 and 2 DIED where run 3's coupler
// fixes carried it), and the mode-2 s = 2 cycle (±57.6 mm at
// 2.23 Hz) still fails both bands — losing the benchmark cycle
// stays loud.
if let Some(f) = w.frequency {
assert!(
(1.85..2.0).contains(&f),
"uy frequency {f:.3} left the benchmark-cycle band [1.85, 2.0] Hz \
(measured 1.925, reference {REF_UY_FREQ})"
);
}
assert!(
(70e-3..92e-3).contains(&w.uy_amp),
"uy amplitude {:.4e} left the benchmark-cycle band [70e-3, 92e-3] \
(measured 81.68e-3, reference {REF_UY_AMP})",
w.uy_amp
);
}
}