P5-1 instruments: the overset fluid saves/loads its rigid-phase state (RTX_FSI2O_SAVE / _LOAD — a coupling experiment then costs seconds, not the 12-minute rigid march), dumps the interface edges when the composite refuses a patch (RTX_FSI2O_DUMP_DIR, at the advance where the overlap is rebuilt), the release time is the fluid's clock; patch_cylinder_flag_deformed: cells inside the flag are holes at small bends and both sweep counts (the cantilever shape does not reproduce the P5-1 death — the CSVs show a coupling runaway), and a dumped-edges reproduction test
Documentation / Build User Guide (push) Canceled after 0s
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

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 08:09:56 -07:00
co-authored by Claude Fable 5.1
parent d74571e21a
commit 2ed36c3a8c
3 changed files with 309 additions and 6 deletions
@@ -94,6 +94,8 @@ pub struct OversetFluid {
pub fresh_total: Cell<usize>,
pub rounds_total: Cell<usize>,
pub correctors_total: Cell<usize>,
/// The interface of the last `set_geometry`.
pub last_d: Vec<f64>,
}
impl OversetFluid {
@@ -236,9 +238,73 @@ impl OversetFluid {
fresh_total: Cell::new(0),
rounds_total: Cell::new(0),
correctors_total: Cell::new(0),
last_d: zero_d,
})
}
/// Save the fluid state (`RTX_FSI2O_SAVE=dir`, tag `fsi2o_ny{ny}`):
/// the background as `FlowField::save`, the patch vectors raw, the
/// time.
pub fn save(&self, dir: &str) -> CfdResult<()> {
let dir = std::path::Path::new(dir);
std::fs::create_dir_all(dir).expect("save dir");
let tag = format!("fsi2o_ny{}", self.ny);
self.field
.background
.save(&dir.join(format!("bg_{tag}.bin")))?;
for (name, vals) in [
("u", &self.field.patch.u),
("v", &self.field.patch.v),
("p", &self.field.patch.p),
("flux", &self.field.patch.flux),
] {
let bytes: Vec<u8> = vals.iter().flat_map(|x| x.to_le_bytes()).collect();
std::fs::write(dir.join(format!("patch_{tag}_{name}.bin")), bytes).expect("save patch");
}
std::fs::write(
dir.join(format!("time_{tag}.txt")),
format!("{:.17e}", self.solver.time()),
)
.expect("save time");
println!(
" saved the fluid state {tag} at t = {:.4} to {}",
self.solver.time(),
dir.display()
);
Ok(())
}
/// Load a state saved by [`Self::save`] (same ny, undeformed patch);
/// returns its time.
pub fn load(&mut self, dir: &str) -> CfdResult<f64> {
let dir = std::path::Path::new(dir);
let tag = format!("fsi2o_ny{}", self.ny);
self.field.background = FlowField::load(&dir.join(format!("bg_{tag}.bin")))?;
let read = |name: &str| -> Vec<f64> {
let bytes = std::fs::read(dir.join(format!("patch_{tag}_{name}.bin")))
.unwrap_or_else(|e| panic!("load patch {name}: {e}"));
bytes
.chunks_exact(8)
.map(|c| f64::from_le_bytes(c.try_into().expect("8 bytes")))
.collect()
};
self.field.patch.u = read("u");
self.field.patch.v = read("v");
self.field.patch.p = read("p");
self.field.patch.flux = read("flux");
let t: f64 = std::fs::read_to_string(dir.join(format!("time_{tag}.txt")))
.expect("time")
.trim()
.parse()
.expect("time value");
self.solver.set_time(t);
println!(
" loaded the fluid state {tag} at t = {t:.4} from {}",
dir.display()
);
Ok(t)
}
/// The patch around the interface `d`.
pub fn patch_for(&self, d: &[f64]) -> CfdResult<rtx_cfd::mesh::PatchMesh> {
let start = std::time::Instant::now();
@@ -278,13 +344,51 @@ impl OversetFluid {
.map(|&(u, v)| [u, v])
.collect();
}
let mesh = self.patch_for(d)?;
let mesh = match self.patch_for(d) {
Ok(m) => m,
Err(e) => {
self.dump_edges(d, &format!("generator refused ({e:?})"));
return Err(e);
}
};
self.last_d = d.to_vec();
self.solver.set_patch_mesh(mesh)
}
/// 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.
fn dump_edges(&self, d: &[f64], why: &str) {
let Ok(dir) = std::env::var("RTX_FSI2O_DUMP_DIR") else {
return;
};
let e = self.interface.edges(d);
let mut out = String::new();
for (name, pts) in [("bottom", &e.bottom), ("tip", &e.tip), ("top", &e.top)] {
out.push_str(&format!("# {name} {}\n", pts.len()));
for p in pts {
out.push_str(&format!("{:.17e} {:.17e}\n", p[0], p[1]));
}
}
let path = std::path::Path::new(&dir).join("p5_death_edges.txt");
std::fs::write(&path, out).expect("dump edges");
println!(" {why}; edges dumped to {}", path.display());
}
/// One fluid step at the current wall.
pub fn step(&mut self) -> CfdResult<OversetResult> {
let r = futures::executor::block_on(self.solver.advance(&mut self.field, self.dt_fluid))?;
let r = match futures::executor::block_on(
self.solver.advance(&mut self.field, self.dt_fluid),
) {
Ok(r) => r,
Err(e) => {
// The overlap is rebuilt inside `advance`: a refused patch
// surfaces here, with the last geometry.
let d = self.last_d.clone();
self.dump_edges(&d, &format!("advance refused ({e:?})"));
return Err(e);
}
};
self.reclassified_total
.set(self.reclassified_total.get() + r.reclassified_cells);
self.fresh_total.set(self.fresh_total.get() + r.fresh_cells);