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
This commit is contained in:
Omar Sobh
2026-08-23 16:54:12 -05:00
co-authored by Claude Fable 5
parent a342e703a4
commit be04e0e233
@@ -58,22 +58,41 @@
//! 49 ± 508 (ref 0.61 ± 237.8) — consistent with the small-amplitude
//! state.
//!
//! Why mode 2 does not win here, measured stepwise: (a) at the default
//! coupling the motion-load staggered phase lag (~omega dt_c) starves the
//! resonant channel — a subcycle = 2 probe (lag / 4) redirected early
//! growth into 1.9 / 2.85 Hz exactly as that predicts; (b) but the probe
//! then destabilised: at a fixed interface-DISPLACEMENT tolerance the
//! no-slip closure's wall-velocity noise is tol / dt_c (~0.3 m/s at
//! 2e-4 / 6.5e-4 — 30% of the mean inflow), and the fluid pumped up and
//! blew through the coupling. The displacement tolerance a smaller dt_c
//! needs (~1e-5) sits BELOW the discrete interface noise floor (~1e-4 at
//! full inflow, from mask flips through Newmark's beta dt^2/m). **The
//! route to the benchmark cycle is lowering the interface noise floor**
//! (smoother load sampling / mask transitions, or a vector quasi-Newton
//! interface solver in place of scalar Aitken), not more iterations
//! against it.
//! Why mode 2 does not win at the default coupling: the motion-load
//! staggered phase lag (~omega dt_c) starves the resonant channel. The
//! tenth session's attempt at tighter coupling (subcycle 2 at the SAME
//! 2e-4 tolerance) blew up — the accepted interface scatter feeds the
//! no-slip closure a wall-velocity noise of tol / dt_c, 30% of the mean
//! inflow there — and was read as an impassable interface noise floor.
//!
//! Robustness findings, both measured: rare wild tractions from
//! # What the eleventh session (2026-08-21) measured: the mode-2 cycle
//!
//! The noise probe (`fsi2_interface_noise.rs`) showed the floor was a
//! tolerance mis-budgeting: the real step map converges to ~1e-9 at
//! subcycle 2. With the `IqnIls` coupler and the tolerance budgeted
//! from that measurement (`RTX_FSI2_COUPLER=iqn RTX_FSI2_SUBCYCLE=2
//! RTX_FSI2_TOL=1.5e-5`), the march self-excites through resonant
//! growth (2.34 → 2.23 Hz, drifting toward mode 2) into **the
//! benchmark's mode-2 flapping cycle: uy = 3.7 ± 57.6 mm at 2.227 Hz
//! at ny = 62 (t_end = 16), 1.99 ± 57.1 mm at 2.236 Hz at ny = 82
//! (t_end = 20) — grid-converged to 0.9% / 0.4% — and stable over a
//! 24 s record (3.68 ± 55.9 mm at 2.224 Hz over [27, 30] s)**. The
//! ladder localizes mode selection in dt_c: subcycle 4 opens the
//! resonant channel for ~4 s (2.5 Hz) before the wake recaptures it
//! (3.67 Hz, ±15.5 mm); subcycle 1 reaches 1.955 Hz / ±73 mm mid-growth
//! (89% of the reference amplitude at essentially its frequency), so
//! the remaining gap to the reference is coupling lag, not grid. Cost:
//! 1.4 (subcycle 2) to 2.8 (subcycle 1) subiterations per step.
//!
//! Coupler failure modes met on the way, each measured and handled
//! (`rtx_fsi::coupling` docs): stale secant history overshooting during
//! rapid amplitude growth (history reset + one retry from the
//! predictor), an ill-conditioned secant step sweeping a candidate
//! interface to the domain wall (trust-region cap at 50x the
//! residual), and a rare violent step near peak motion stalling an
//! order below its own increment (accepted, counted, bounded).
//!
//! Robustness findings from the tenth session, both measured: rare wild tractions from
//! near-degenerate reconstructions (19 samples in 2.4 million) killed a
//! t = 25.8 s march through the flag's Newton until the spike CLAMP (20x
//! the sample median, direction kept — clamping, not dropping: a hard
@@ -85,8 +104,10 @@
//! (partition of unity) at 1e-10 (measured 8e-12 over 9,263 steps),
//! coupled convergence bookkeeping, finite fields. The committed default
//! (t_end = 7) pins the deterministic release response; study horizons
//! (t_end >= 25) pin the measured attractor so any material change is
//! loud. Full trajectories: the session scratchpad study logs.
//! pin the measured attractor of each configuration (the wake state at
//! t_end >= 25 under the default; the mode-2 cycle at t_end >= 16 under
//! IQN / subcycle 2) so any material change is loud. Full trajectories:
//! the session scratchpad study logs.
//!
//! Environment knobs: `RTX_FSI2_NY` (fluid resolution, default 62),
//! `RTX_FSI2_T_RELEASE` (default 6 s), `RTX_FSI2_T_END` (default 7 s —
@@ -498,17 +519,18 @@ fn fsi2_flapping_flag() {
let _ = (amp_early, amp_late);
// Physics bands, by horizon. The march is deterministic, so short
// horizons carry tight regression bands; long horizons pin the
// MEASURED loosely-coupled attractor — not benchmark agreement (see
// the module docs: the reference's mode-2 resonant cycle at 1.93 Hz /
// 81.6 mm is not reached by this coupling; the measured state is the
// wake-forced 3.73 Hz / ±17.3 mm cycle at BOTH grids). If a change
// moves these numbers, that is a finding either way and must be loud.
// The bands pin the UNSMOOTHED, Aitken-coupled sampling (the
// defaults): smoothing or the IQN coupler change the load path and
// re-pin deliberately.
// Physics bands, by horizon and configuration. The march is
// deterministic, so short horizons carry tight regression bands;
// long horizons pin MEASURED attractorswhich attractor depends on
// the coupling (see the module docs): the loosely-coupled default
// (Aitken, subcycle 8) locks the wake-forced 3.73 Hz / ±17.3 mm
// state at BOTH grids; the IQN / subcycle-2 configuration locks the
// benchmark's mode-2 flapping cycle, grid-converged at ±57 mm /
// 2.23 Hz. If a change moves any of these numbers, that is a finding
// either way and must be loud. Smoothing changes the load path and
// is pinned nowhere (measured to change nothing that matters).
let default_coupling = smooth_in_h == 0.0 && coupler_kind == "aitken";
let mode2_coupling = smooth_in_h == 0.0 && coupler_kind == "iqn" && subcycle == 2;
if default_coupling
&& ny == 62
&& flag_nx == 35
@@ -545,5 +567,27 @@ fn fsi2_flapping_flag() {
"uy amplitude {uy_amp:.4e} left the measured ±17.3 mm attractor \
band [12e-3, 24e-3]"
);
} else if mode2_coupling && flag_nx == 35 && t_end >= 16.0 {
// Study horizons, the mode-2 configuration: the flapping cycle
// measured 2026-08-21 at t_end = 16 (ny = 62: uy 3.71 ± 57.6 mm
// at 2.227 Hz), t_end = 20 (ny = 82: 1.99 ± 57.1 mm at 2.236 Hz)
// and t_end = 30 (ny = 62: 3.68 ± 55.9 mm at 2.224 Hz) — two-grid
// agreement 0.9% in amplitude, 0.4% in frequency, stable over 24 s.
// The bands are wide enough for a settled cycle at either grid
// and narrow enough that the wake attractor (3.73 Hz, ±17 mm)
// or the reference itself (1.93 Hz, ±82 mm) would both fail:
// either would be a material change to report.
if let Some(f) = frequency {
assert!(
(2.05..2.45).contains(&f),
"uy frequency {f:.3} left the measured mode-2 cycle band \
[2.05, 2.45] Hz (benchmark reference {REF_UY_FREQ})"
);
}
assert!(
(45e-3..70e-3).contains(&uy_amp),
"uy amplitude {uy_amp:.4e} left the measured mode-2 cycle band \
[45e-3, 70e-3] (benchmark reference {REF_UY_AMP})"
);
}
}