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
This commit is contained in:
Omar Sobh
2026-08-21 09:34:10 -07:00
co-authored by Claude Fable 5
parent 140310b223
commit d5f19ea497
2 changed files with 64 additions and 2 deletions
@@ -253,6 +253,7 @@ fn fsi2_flapping_flag() {
let mut max_subiterations = 0usize;
let mut total_skipped = 0usize;
let mut stalled_steps = 0usize;
let mut retried_steps = 0usize;
let mut worst_stall = 0.0f64;
let mut force_times: Vec<f64> = Vec::new();
let mut drag_series: Vec<f64> = Vec::new();
@@ -313,7 +314,7 @@ fn fsi2_flapping_flag() {
.sum::<f64>()
.sqrt();
let tol_step = tol_floor.max(rtol * increment);
let outcome = if let Some(iqn) = iqn.as_mut() {
let mut outcome = if let Some(iqn) = iqn.as_mut() {
iqn.set_tolerance(tol_step).unwrap();
iqn.solve(&d_predicted, pass)
} else {
@@ -321,6 +322,30 @@ fn fsi2_flapping_flag() {
.unwrap()
.solve(&d_predicted, pass)
};
// Stale-history recovery: cross-step secant columns assume the
// interface Jacobian drifts slowly, and during a rapid resonant
// growth they can steer the quasi-Newton update into an
// overshoot the divergence guard reads as added mass (measured:
// a first residual of 1e-4 driven to 1e-3 by the first update at
// 2.7x the previously seen amplitude, killing a t = 11.1 s
// march). The map itself converges deeply from a clean start
// (the noise probe's stall measurement), so: reset the history
// and retry the step ONCE from the predictor. Aitken carries no
// history — a retry would repeat the identical iteration, so
// this path is IQN-only.
if let (Err(e), Some(iqn_ref)) = (&outcome, iqn.as_mut()) {
let recoverable = matches!(
e,
rtx_fsi::FsiError::CouplingNotConverged { residual, .. }
| rtx_fsi::FsiError::CouplingDiverged { residual, .. }
if *residual >= 5.0 * tol_step
);
if recoverable {
iqn_ref.reset_history();
retried_steps += 1;
outcome = iqn_ref.solve(&d_predicted, pass);
}
}
match outcome {
Ok(converged) => {
total_subiterations += converged.iterations;
@@ -422,7 +447,8 @@ fn fsi2_flapping_flag() {
println!(
" FSI2 (fluid ny = {ny}, flag {flag_nx}x2 Quad8, dt = {dt:.2e}): coupled {coupled_steps} \
steps in {:.0} s wall total; {mean_subiterations:.1} subit/step (max \
{max_subiterations}); {stalled_steps} stalled steps (worst residual \
{max_subiterations}); {stalled_steps} stalled steps, {retried_steps} \
history-reset retries (worst residual \
{worst_stall:.2e}); worst conservation {worst_conservation:.2e}; skipped samples \
{total_skipped} (of which {} spike-rejected)\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 \