rtx-cfd: mask hysteresis — sticky cell classification against a reference mask
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 measured FSI3 killer is the bistable mask: one geometry (|d|
identical to 4 digits) samples two load branches (60 vs 120 kN), and
the traced s=1 death is the secant walking a 20x load cliff
(68,886 -> 1,307,938 N over a 1e-3 candidate change). A sticky band
makes the load map single-valued at the crossing: formerly-fluid
cells flip only at phi < -band, formerly-solid at phi > band,
classified against the mask held at rebuild time — in a coupling
loop, the restored committed step-start mask.

Band 0 is structurally bit-identical (phi > -0.0 <=> phi > 0.0) and
verified digit-for-digit on both committed defaults (FSI2 and FSI3,
every physics digit). Measured cost on the translating-circle MMS at
band 0.25h: +0.5% field error (u/p ratios 1.17/2.12 vs the
no-hysteresis moving levels 1.16/2.11); flip delay = band/(v dt),
deterministic. Exposed as RTX_FSI{2,3}_HYST in multiples of h_min.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
Omar Sobh
2026-08-26 15:27:09 -05:00
co-authored by Claude Fable 5
parent 3207f2d4c6
commit bff84ccdcc
6 changed files with 340 additions and 4 deletions
@@ -49,6 +49,17 @@ pub struct MarchConfig {
/// only the rare-event window (FSI2's s = 1 benchmark run accepted
/// a worst stall of 5.4e-4 the same way and measured 0.1%).
pub stall_accept: f64,
/// Mask hysteresis band in multiples of the min cell size (0 = off,
/// bit-identical). With a band, a cell within `band * h` of the
/// interface keeps the classification of the committed step-start
/// mask (the snapshot every pass restores), so candidate geometries
/// within the band all see the SAME mask — the pass map stops
/// flipping cells on sub-band candidate differences (the measured
/// FSI3 killer: bistable load branches 60 vs 120 kN at one geometry,
/// and a 20x load cliff over a 1e-3 candidate change). Cost: the
/// effective wall lags the true surface by up to the band (measured
/// on the translating-circle MMS at 0.25h: +0.5% field error).
pub mask_hysteresis: f64,
pub max_subiterations: usize,
/// `"aitken"` (per-step scalar Aitken) or `"iqn"` (a persistent
/// IQN-ILS whose secant history carries across steps).
@@ -93,8 +104,8 @@ pub struct MarchConfig {
}
impl MarchConfig {
/// Read `RTX_{prefix}_{NY,T_RELEASE,T_END,SUBCYCLE,TOL,RTOL,MAXSUB,
/// FLAG_NX,SMOOTH,COUPLER,REUSE,CSV}` over `defaults`.
/// Read `RTX_{prefix}_{NY,T_RELEASE,T_END,SUBCYCLE,TOL,RTOL,STALLX,
/// HYST,MAXSUB,FLAG_NX,SMOOTH,COUPLER,REUSE,CSV}` over `defaults`.
pub fn from_env(prefix: &str, defaults: MarchConfig) -> MarchConfig {
let key = |name: &str| format!("RTX_{prefix}_{name}");
let num = |name: &str, default: f64| env_or(&key(name), default);
@@ -107,6 +118,7 @@ impl MarchConfig {
tol_floor: num("TOL", defaults.tol_floor),
rtol: num("RTOL", defaults.rtol),
stall_accept: num("STALLX", defaults.stall_accept),
mask_hysteresis: num("HYST", defaults.mask_hysteresis),
max_subiterations: num("MAXSUB", defaults.max_subiterations as f64) as usize,
coupler: std::env::var(key("COUPLER")).unwrap_or(defaults.coupler),
reuse: num("REUSE", defaults.reuse as f64) as usize,
@@ -234,6 +246,7 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
tol_floor,
rtol,
stall_accept,
mask_hysteresis,
max_subiterations: max_subiterations_budget,
ref coupler,
reuse,
@@ -247,6 +260,7 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
} = *config;
let (harness, mut solver, mut field) = Fsi2Harness::build_case(case, ny, flag_nx, smooth_in_h);
solver.set_mask_hysteresis(mask_hysteresis);
let dt_fluid = harness.dt_fluid;
let dt = dt_fluid * subcycle as f64;
let interface = &harness.interface;