rtx-cfd/rtx-fsi: the FlowField->clawview exporter — real FSI fields through the viewer, end to end
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
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s

The march gains RTX_FSI{2,3}_FFLD (MarchConfig::ffld_dir, off by
default): every snap_every committed steps (10 when SNAPEVERY is 0),
dump the committed FlowField (FlowField::save, bit-exact), the solver's
own fluid-cell mask and the interface polygon as text sidecars, plus an
index.csv. Reporting-only after acceptance; the FSI2 default with the
knob off reproduces every printed digit of the warm-start baseline
(uy 3.4921 +- 3.5109, conservation 8.25e-12).

The exporter (rtx-cfd examples/ffld_to_vtk): a dump directory ->
clawview-readable legacy VTK — per-snapshot 2D triangle meshes (one
selectable point scalar --field p|umag|vort, the 0/1 fluid mask as
integer CELL_DATA) and an optional space-time volume (--spacetime:
frames stacked along z = time, prisms split to tets, POINT_DATA phi),
which clawview's slice-plane animation plays as a transient movie.

Verified end to end with REAL fields, not synthetic: a 29-frame FSI3
release-transient dump (t 4.0 -> 4.1, 64 MB) exported to 29 snapshots
+ a 984,312-tet space-time volume; the clawview server loaded both
(176,320 nodes volume; 23,877-node snapshots) and served live
cross-sections (time slices), vorticity isosurfaces and contours from
them. The check caught two real viewer-contract constraints now
encoded in the exporter: clawview's legacy-VTK path parses CELL_DATA
scalars as INTEGER markers only, and supports exactly ONE point scalar
(all POINT_DATA blocks append into `phi`) — multi-field snapshots need
the .clwv route, out of scope here.

Closes the top open thread of the fifteenth-session handoff (the
FSNP/FlowField->viewer exporter); the mesh-repo push and the claw-gds
cargo feature remain clawview-side items.

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-30 14:45:54 -05:00
co-authored by Claude Fable 5
parent 4d2cede7bc
commit 4aec4e589d
4 changed files with 450 additions and 2 deletions
@@ -80,6 +80,17 @@ pub struct MarchConfig {
/// acceptance, no float ops on the solver path.
pub snap_path: Option<String>,
pub snap_every: usize,
/// Fluid-field dump directory (`RTX_{prefix}_FFLD`, off by default):
/// every `snap_every` committed steps (10 if `snap_every` is 0),
/// write the committed `FlowField` (`f_STEP.ffld`,
/// `FlowField::save` — bit-exact), the mask's fluid-cell map
/// (`mask_STEP.txt`, `ny` rows of `nx` `0`/`1` chars, row 0 first)
/// and the interface polygon (`poly_STEP.txt`, `x y` per line), and
/// append `step,t,f_STEP.ffld` to `index.csv` in the directory.
/// Reporting-only: reads committed state after acceptance, no float
/// ops on the solver path. The `ffld_to_vtk` example in `rtx-cfd`
/// turns a dump directory into clawview-readable VTK.
pub ffld_dir: Option<String>,
/// IQN's relaxation on the very first pass, before any secant
/// information exists. Must CONTRACT a repulsive added-mass map: for
/// a per-pass gain `-g` the first update multiplies the residual by
@@ -115,8 +126,8 @@ pub struct MarchConfig {
impl MarchConfig {
/// Read `RTX_{prefix}_{NY,T_RELEASE,T_END,SUBCYCLE,TOL,RTOL,STALLX,
/// HYST,MAXSUB,FLAG_NX,SMOOTH,COUPLER,REUSE,CSV,SNAP,SNAPEVERY}`
/// over `defaults`.
/// HYST,MAXSUB,FLAG_NX,SMOOTH,COUPLER,REUSE,CSV,SNAP,SNAPEVERY,
/// FFLD}` 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);
@@ -137,6 +148,7 @@ impl MarchConfig {
csv_path: std::env::var(key("CSV")).ok().or(defaults.csv_path),
snap_path: std::env::var(key("SNAP")).ok().or(defaults.snap_path),
snap_every: num("SNAPEVERY", defaults.snap_every as f64) as usize,
ffld_dir: std::env::var(key("FFLD")).ok().or(defaults.ffld_dir),
initial_relaxation: num("OMEGA0", defaults.initial_relaxation),
trace_steps: num("TRACE", defaults.trace_steps as f64) as usize,
c1_interface: num("C1", f64::from(u8::from(defaults.c1_interface))) != 0.0,
@@ -278,6 +290,7 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
ref csv_path,
ref snap_path,
snap_every,
ref ffld_dir,
initial_relaxation,
trace_steps,
c1_interface,
@@ -622,6 +635,19 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
write_snapshot(w, t, &flag_state, &committed_nodal);
}
}
if let Some(dir) = ffld_dir {
let cadence = if snap_every > 0 { snap_every } else { 10 };
if (step + 1) % cadence == 0 {
write_ffld_dump(
dir,
step + 1,
t,
&solver.borrow(),
&field.borrow(),
&harness,
);
}
}
let (drag_now, lift_now) = harness.measure_force(&solver.borrow(), &field.borrow());
interval_drag.push(drag_now);
interval_lift.push(lift_now);
@@ -696,6 +722,49 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
/// `(node id, fx, fy, fz)` tuples — everything the ECSW phase needs
/// (POD/ECSW train on the displacement snapshots; the load records
/// drive the offline full-vs-reduced replay).
/// One fluid-field dump (see `MarchConfig::ffld_dir`): the committed
/// `FlowField` bit-exact, the mask's fluid-cell map and the interface
/// polygon as text sidecars, and an `index.csv` line. Reporting-only.
fn write_ffld_dump(
dir: &str,
step: usize,
t: f64,
solver: &rtx_cfd::solvers::incompressible::EmbeddedPisoSolver,
field: &rtx_cfd::solvers::incompressible::FlowField,
harness: &super::Fsi2Harness,
) {
use std::io::Write as _;
let dir = std::path::Path::new(dir);
std::fs::create_dir_all(dir).expect("ffld dir");
let ffld_name = format!("f_{step:06}.ffld");
field.save(&dir.join(&ffld_name)).expect("ffld save");
let (nx, ny, _, _) = field.grid_info();
let mask = solver.mask().expect("mask");
let mut mask_text = String::with_capacity((nx + 1) * ny);
for j in 0..ny {
for i in 0..nx {
mask_text.push(if mask.is_fluid_cell(j, i) { '1' } else { '0' });
}
mask_text.push('\n');
}
std::fs::write(dir.join(format!("mask_{step:06}.txt")), mask_text).expect("mask sidecar");
let mut poly_text = String::new();
for &(x, y) in harness.shared.read().unwrap().0.vertices() {
use std::fmt::Write as _;
writeln!(poly_text, "{x:.9e} {y:.9e}").unwrap();
}
std::fs::write(dir.join(format!("poly_{step:06}.txt")), poly_text).expect("poly sidecar");
let mut index = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(dir.join("index.csv"))
.expect("index.csv");
writeln!(index, "{step},{t:.9},{ffld_name}").expect("index line");
}
fn write_snapshot(
w: &mut std::io::BufWriter<std::fs::File>,
t: f64,