R4-i + R5 design pass: RTX_FSI2O_REGEN_ONCE (one patch regeneration per coupled step; dies at step 0 — the registered test is void), the step CSV's subit/dres_y/dres_norm/fx_nodal/fy_nodal columns; the cut predictor's per-face term probe (enable_term_probe) and the curved instrument's exact side/wall viscous integrals — the static convex wall's flat residual is the viscous closure's first-order relative accuracy on O(1/h) fluxes
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / Build CPU-Only (Explicit) (push) Failing after 4s
CI / Clippy Check (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 4s
CI / Format Check (push) Failing after 12s
CI / Build (ubuntu-latest) (push) Failing after 1m26s
Documentation / Build API Documentation (push) Failing after 1m28s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m7s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-22 07:39:54 -05:00
co-authored by Claude Fable 5.1
parent f5735fdeaa
commit 501b45f9d0
5 changed files with 316 additions and 31 deletions
@@ -240,6 +240,15 @@ pub struct OversetFluid {
/// The last fluid step's mass defects (patch acceptor ring, background
/// fringe) and the patch's max divergence.
pub last_defects: Cell<(f64, f64, f64)>,
/// R4-i `RTX_FSI2O_REGEN_ONCE`: one patch regeneration per coupled
/// step — the first pass's meshes (one per substep) are reused by every
/// later subiteration of the same step (the wall polygon and velocity
/// still follow the candidate). Active between `begin_coupled_step`
/// calls only; `restore` rewinds the cursor to the step's first substep.
pub regen_once: bool,
pub regen_hold: bool,
pub regen_cache: Vec<rtx_cfd::mesh::PatchMesh>,
pub regen_cursor: usize,
}
impl OversetFluid {
@@ -503,6 +512,10 @@ impl OversetFluid {
correctors_total: Cell::new(0),
last_d: zero_d,
last_defects: Cell::new((0.0, 0.0, 0.0)),
regen_once: std::env::var("RTX_FSI2O_REGEN_ONCE").is_ok(),
regen_hold: false,
regen_cache: Vec::new(),
regen_cursor: 0,
warm_base: None,
warm_sweeps,
})
@@ -918,6 +931,12 @@ impl OversetFluid {
self.last_d = d.to_vec();
return Ok(());
}
if self.regen_once && self.regen_hold && self.regen_cursor < self.regen_cache.len() {
let mesh = self.regen_cache[self.regen_cursor].clone();
self.regen_cursor += 1;
self.last_d = d.to_vec();
return self.solver.set_patch_mesh(mesh);
}
let mesh = match self.patch_for(d) {
Ok(m) => m,
Err(e) => {
@@ -925,10 +944,22 @@ impl OversetFluid {
return Err(e);
}
};
if self.regen_once && self.regen_hold {
self.regen_cache.push(mesh.clone());
self.regen_cursor += 1;
}
self.last_d = d.to_vec();
self.solver.set_patch_mesh(mesh)
}
/// R4-i: a new coupled step begins — the first pass regenerates, the
/// later passes reuse (`RTX_FSI2O_REGEN_ONCE`; a no-op otherwise).
pub fn begin_coupled_step(&mut self) {
self.regen_hold = self.regen_once;
self.regen_cache.clear();
self.regen_cursor = 0;
}
/// The last geometry, for the offline reproduction
/// (`patch_cylinder_flag_deformed.rs`, `RTX_CF_EDGES_FILE`): one edge
/// per block, `x y` per line, when `RTX_FSI2O_DUMP_DIR` is set.
@@ -1198,6 +1229,7 @@ impl OversetFluid {
let t0 = std::time::Instant::now();
self.solver.restore(&saved.0);
self.field = saved.1.clone();
self.regen_cursor = 0;
self.t_restore
.set(self.t_restore.get() + t0.elapsed().as_secs_f64());
}
@@ -137,6 +137,11 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
std::env::var("RTX_FSI2O_FICT_MASS").unwrap_or_else(|_| "0".into()),
std::env::var("RTX_FSI2O_ROBIN_ALPHA").unwrap_or_else(|_| "0".into()),
);
if std::env::var("RTX_FSI2O_REGEN_ONCE").is_ok() {
println!(
" patch regeneration: ONCE per coupled step (R4-i, RTX_FSI2O_REGEN_ONCE): the first pass's mesh is reused by the later subiterations"
);
}
// Phase 1: rigid flag to t_release (`RTX_FSI2O_LOAD=dir` replaces the
// march with the saved state; `RTX_FSI2O_SAVE=dir` saves it).
@@ -399,7 +404,11 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
// cell imbalance.
let mut step_csv = std::env::var("RTX_FSI2O_STEP_CSV").ok().map(|p| {
let mut f = std::fs::File::create(p).expect("step csv");
writeln!(f, "t,ux,uy,drag,lift,defect_patch,defect_bg,patch_div").unwrap();
// R4-i columns: the step's subiterations, the last pass's signed
// y-residual Σ_k (d_new d_candidate)_y and its norm, and the
// nodal load the structure was given (Σ F_x, Σ F_y) — against the
// wall-integral force of the same state (drag, lift).
writeln!(f, "t,ux,uy,drag,lift,defect_patch,defect_bg,patch_div,subit,dres_y,dres_norm,fx_nodal,fy_nodal").unwrap();
f
});
let (t_fluid, t_structure) = (std::cell::Cell::new(0.0_f64), std::cell::Cell::new(0.0_f64));
@@ -433,6 +442,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
if robin_alpha > 0.0 {
robin_datum.replace(fluid.borrow().inner_tractions());
}
fluid.borrow_mut().begin_coupled_step();
let saved = fluid.borrow().snapshot();
type PassResult = (DynamicState, Vec<(NodeId, Vector3<f64>)>, f64, usize);
let latest: RefCell<Option<PassResult>> = RefCell::new(None);
@@ -498,6 +508,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
let tol_step = cfg.tol_floor.max(cfg.rtol * increment);
let retry_at = (5.0 * tol_step).max(0.1 * increment);
let acceptable = (cfg.stall_accept * tol_step).max(0.1 * increment);
let mut step_iterations = 0usize;
let mut outcome = if let Some(iqn) = iqn.as_mut() {
iqn.set_tolerance(tol_step).unwrap();
iqn.solve(&d_predicted, pass)
@@ -533,6 +544,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
Ok(c) => {
total_subiterations += c.iterations;
max_subiterations = max_subiterations.max(c.iterations);
step_iterations = c.iterations;
}
Err(
rtx_fsi::FsiError::CouplingNotConverged {
@@ -549,6 +561,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
worst_stall = worst_stall.max(residual);
total_subiterations += iterations;
max_subiterations = max_subiterations.max(iterations);
step_iterations = iterations;
}
Err(e) => {
println!(
@@ -560,6 +573,25 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
}
}
let (new_state, nodal, conservation, faces) = latest.borrow_mut().take().expect("pass ran");
// R4-i: the last pass's residual (the structure's answer against the
// candidate the fluid's committed state sits at) and the nodal load.
let (dres_y, dres_norm, fx_nodal, fy_nodal) = {
let d_new = extract(&new_state);
let d_cand = &fluid.borrow().last_d;
let mut sy = 0.0_f64;
let mut n2 = 0.0_f64;
for k in 0..d_new.len() / 2 {
sy += d_new[2 * k + 1] - d_cand[2 * k + 1];
n2 += (d_new[2 * k] - d_cand[2 * k]).powi(2)
+ (d_new[2 * k + 1] - d_cand[2 * k + 1]).powi(2);
}
let (mut fx, mut fy) = (0.0_f64, 0.0_f64);
for (_, f) in &nodal {
fx += f.x;
fy += f.y;
}
(sy, n2.sqrt(), fx, fy)
};
flag_state = new_state;
committed_nodal = nodal;
prev_area.set(fluid.borrow().shared.read().unwrap().area());
@@ -590,7 +622,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
let (dp, db, pd) = fluid.borrow().last_defects.get();
writeln!(
f,
"{t_now:.6},{ux:.6e},{uy:.6e},{drag_now:.6e},{lift_now:.6e},{dp:.3e},{db:.3e},{pd:.3e}"
"{t_now:.6},{ux:.6e},{uy:.6e},{drag_now:.6e},{lift_now:.6e},{dp:.3e},{db:.3e},{pd:.3e},{step_iterations},{dres_y:.3e},{dres_norm:.3e},{fx_nodal:.6e},{fy_nodal:.6e}"
)
.unwrap();
}