P5 audit: the overset march saves the composite at every N-th committed step (RTX_FSI2O_SAVE_EVERY: background, the deformed patch's nodes, its vectors, d, ddot, time), and fsi2_overset_audit_of_saved_instants (RTX_FSI2O_AUDIT=dir) rebuilds the composite around each saved patch (build_case_with an initial mesh, so the overlap is the instant's) and prints the solver-metric chain — the CFD23 instrument on the moving patch
CI / Build CPU-Only (Explicit) (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 / CI Success (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 / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0116sg1Qz1gMv9hdcKP1XUam
This commit is contained in:
Omar Sobh
2026-09-07 09:13:07 -07:00
co-authored by Claude Fable 5.1
parent 3a68048934
commit 3c2add3f32
3 changed files with 241 additions and 1 deletions
@@ -152,6 +152,19 @@ impl OversetFluid {
flag_nx: usize,
sweeps: usize,
max_rounds: usize,
) -> CfdResult<Self> {
Self::build_case_with(case, ny, flag_nx, sweeps, max_rounds, None)
}
/// [`Self::build_case`] with an initial patch mesh given (a saved
/// instant's deformed patch: the overlap is built around it).
pub fn build_case_with(
case: BenchmarkCase,
ny: usize,
flag_nx: usize,
sweeps: usize,
max_rounds: usize,
initial: Option<rtx_cfd::mesh::PatchMesh>,
) -> CfdResult<Self> {
let h = H / ny as f64;
let nx = (L / h).round() as usize;
@@ -220,7 +233,9 @@ impl OversetFluid {
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let patch_mesh = if warm_sweeps > 0 {
let patch_mesh = if let Some(m) = initial {
m
} else if warm_sweeps > 0 {
let t0 = std::time::Instant::now();
let (m, (it, last)) = rtx_cfd::mesh::patch_gen::cylinder_flag_patch_deformed_from(
Some(&cold_mesh),
@@ -362,6 +377,176 @@ impl OversetFluid {
Ok(())
}
/// Save the composite at an instant of the coupled march for the
/// offline chain audit: the background, the patch's node coordinates
/// (the deformed mesh), its vectors, the time and the interface `d`
/// (`dir/inst_<tag>_<step>/`).
pub fn save_instant(&self, dir: &str, step: usize, d: &[f64], ddot: &[f64]) -> CfdResult<()> {
let tag = format!("fsi2o_ny{}", self.ny);
let dir = std::path::Path::new(dir).join(format!("inst_{tag}_{step:06}"));
std::fs::create_dir_all(&dir).expect("instant dir");
self.field.background.save(&dir.join("bg.bin"))?;
let mesh = self.solver.patch().mesh();
let (ns, nn) = (mesh.ns(), mesh.nn());
let mut nodes: Vec<f64> = Vec::with_capacity(2 * (ns + 1) * (nn + 1));
for k in 0..=nn {
for i in 0..=ns {
let p = mesh.node_xy(mesh.node(k, i));
nodes.push(p[0]);
nodes.push(p[1]);
}
}
for (name, vals) in [
("nodes", &nodes),
("u", &self.field.patch.u),
("v", &self.field.patch.v),
("p", &self.field.patch.p),
("flux", &self.field.patch.flux),
("d", &d.to_vec()),
("dd", &ddot.to_vec()),
] {
let bytes: Vec<u8> = vals.iter().flat_map(|x| x.to_le_bytes()).collect();
std::fs::write(dir.join(format!("patch_{name}.bin")), bytes).expect("save");
}
std::fs::write(
dir.join("meta.txt"),
format!("t {:.17e}\nns {ns}\nnn {nn}\n", self.solver.time()),
)
.expect("meta");
Ok(())
}
/// Rebuild the composite on a saved instant (`save_instant`) — the
/// deformed patch from its node coordinates, the fields, the wall
/// motion — and return it with the interface and the time; the chain
/// then runs on it as on any settled state.
pub fn from_instant(
case: BenchmarkCase,
ny: usize,
flag_nx: usize,
dir: &std::path::Path,
) -> CfdResult<(Self, Vec<f64>, f64)> {
let read = |name: &str| -> Vec<f64> {
let bytes = std::fs::read(dir.join(format!("patch_{name}.bin")))
.unwrap_or_else(|e| panic!("instant {name}: {e}"));
bytes
.chunks_exact(8)
.map(|c| f64::from_le_bytes(c.try_into().expect("8 bytes")))
.collect()
};
let meta = std::fs::read_to_string(dir.join("meta.txt")).expect("meta");
let mut t = 0.0;
let (mut ns, mut nn) = (0usize, 0usize);
for line in meta.lines() {
let mut it = line.split_whitespace();
match (it.next(), it.next()) {
(Some("t"), Some(v)) => t = v.parse().expect("t"),
(Some("ns"), Some(v)) => ns = v.parse().expect("ns"),
(Some("nn"), Some(v)) => nn = v.parse().expect("nn"),
_ => {}
}
}
let nodes = read("nodes");
let (xs, ys): (Vec<f64>, Vec<f64>) = nodes.chunks_exact(2).map(|c| (c[0], c[1])).unzip();
let mesh = rtx_cfd::mesh::PatchMesh::from_nodes(ns, nn, xs, ys, Some([0.0, 0.0]))?;
// The composite around the deformed patch: built from scratch so
// the overlap is the instant's.
let mut fluid = Self::build_case_with(case, ny, flag_nx, 100, 3, Some(mesh))?;
let d = read("d");
let dd = read("dd");
{
let mut w = fluid.shared.write().unwrap();
w.polygon = fluid
.interface
.polygon(&d)
.iter()
.map(|&(x, y)| [x, y])
.collect();
w.velocity = fluid
.interface
.walk_velocities(&dd)
.iter()
.map(|&(u, v)| [u, v])
.collect();
}
fluid.field.background = FlowField::load(&dir.join("bg.bin"))?;
fluid.field.patch.u = read("u");
fluid.field.patch.v = read("v");
fluid.field.patch.p = read("p");
fluid.field.patch.flux = read("flux");
fluid.solver.set_time(t);
fluid.last_d = d.clone();
Ok((fluid, d, t))
}
/// The solver-metric momentum chain at the current state (§5.11's
/// instrument on the moving patch): the solved-face pin, the ring, the
/// box in the solver's flux form, the patch's balance, the wall.
pub fn chain_line(&self) -> String {
let dt = self.dt_fluid;
let h = self.h;
let (nx, ny) = (self.nx, self.ny);
let cv = (
(0.10 / h).round() as usize,
(0.75 / h).round() as usize,
(0.05 / h).round() as usize,
(0.36 / h).round() as usize,
);
let _ = (nx, ny);
let wall = self.measure_force();
let mr = self.solver.momentum_residual(&self.field, dt);
let ring = mr.fringe_fringe.fx + mr.fringe_hole.fx;
let bx = self.solver.solver_metric_force(&self.field, dt, cv).0;
let bx2 = self
.solver
.solver_metric_force(
&self.field,
dt,
(
(0.09 / h).round() as usize,
(0.70 / h).round() as usize,
(0.07 / h).round() as usize,
(0.34 / h).round() as usize,
),
)
.0;
let pb = self
.solver
.patch()
.momentum_balance(&self.field.patch, self.solver.time());
let ff = pb.flux_force()[0];
let fw = pb.wall_force()[0];
format!(
"solved far Σ|r| ({:.2e}, {:.2e}) {}/{} | near ring Σr ({:+.2e}, {:+.2e}) | box (solver flux form) {:.3} [2 boxes spread {:.1e}] → ring Σr {:+.3} ({} + {} faces of {} + {}) → hole flux {:.3} → band {:+.3} → patch interface {:.3} → interior {:+.3} (δP {:+.3}, balance residual {:+.3}) → wall, scheme fluxes {:.3} → wall formula {:+.3} → wall ({:.3}, {:.3}); total wall box {:+.3} ({:+.2} %); reclassified so far {}",
mr.solved_far.abs_x,
mr.solved_far.abs_y,
mr.solved_far.evaluated,
mr.solved_far.total,
mr.solved_near.fx,
mr.solved_near.fy,
bx,
(bx - bx2).abs(),
ring,
mr.fringe_fringe.evaluated,
mr.fringe_hole.evaluated,
mr.fringe_fringe.total,
mr.fringe_hole.total,
bx + ring,
ff - (bx + ring),
ff,
fw - ff,
pb.pressure_defect()[0],
pb.balance()[0],
fw,
wall.0 - fw,
wall.0,
wall.1,
wall.0 - bx,
100.0 * (wall.0 - bx) / wall.0,
self.reclassified_total.get()
)
}
/// Load a state saved by [`Self::save`] (same ny, undeformed patch);
/// returns its time.
pub fn load(&mut self, dir: &str) -> CfdResult<f64> {
@@ -235,6 +235,10 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
});
let (t_fluid, t_structure) = (std::cell::Cell::new(0.0_f64), std::cell::Cell::new(0.0_f64));
let prev_area = std::cell::Cell::new(fluid.borrow().shared.read().unwrap().area());
let save_every: usize = std::env::var("RTX_FSI2O_SAVE_EVERY")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let phase_start = std::time::Instant::now();
for step in 0..coupled_steps {
@@ -364,6 +368,18 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
committed_nodal = nodal;
prev_area.set(fluid.borrow().shared.read().unwrap().area());
fluid.borrow_mut().commit_base();
// `RTX_FSI2O_SAVE_EVERY=N` (+ `RTX_FSI2O_SAVE`): the composite at
// every N-th committed step, for the offline chain audit.
if save_every > 0 && (step + 1) % save_every == 0 {
if let Ok(dir) = std::env::var("RTX_FSI2O_SAVE") {
let d_now = extract(&flag_state);
let dd_now = extract_velocity(&flag_state);
fluid
.borrow()
.save_instant(&dir, step + 1, &d_now, &dd_now)
.expect("save instant");
}
}
worst_conservation = worst_conservation.max(conservation);
faces_used = faces;
let ux = flag_state.displacement[a_dofs[0]];