P5-3 instruments: per-region wall loads on saved instants (top / bottom / tip arc / fillets+cylinder — drag, lift, normal-traction mean/min/max; RTX_FSI2O_AUDIT_FACES, RTX_FSI2O_AUDIT_ONLY) and the patch-thickness knobs (RTX_FSI2O_PATCH_OFFSET / _PATCH_ROWS, RTX_OVERSET_PATCH_OFFSET / _PATCH_ROWS; save tags carry the offset) — the overlap band sat at a fixed number of cells (6 h) and moved inward in metres with refinement
CI / Build (macos-latest) (push) Failing after 9s
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
CI / Clippy Check (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-08 05:35:14 -07:00
co-authored by Claude Fable 5.1
parent 2c1143076e
commit 3e019d1491
3 changed files with 195 additions and 13 deletions
@@ -17,7 +17,12 @@
//! asserted, until the ladder is seen; `RTX_OVERSET_CFD2_NY` / //! asserted, until the ladder is seen; `RTX_OVERSET_CFD2_NY` /
//! `RTX_OVERSET_CFD3_NY` (default 41), `RTX_OVERSET_CFD23_T_END` (smoke), //! `RTX_OVERSET_CFD3_NY` (default 41), `RTX_OVERSET_CFD23_T_END` (smoke),
//! `RTX_OVERSET_MAX_ROUNDS` (3), `RTX_OVERSET_ROWS` (4), //! `RTX_OVERSET_MAX_ROUNDS` (3), `RTX_OVERSET_ROWS` (4),
//! `RTX_OVERSET_CFD1_SAVE` / `_LOAD` (fields, tagged by case). //! `RTX_OVERSET_CFD1_SAVE` / `_LOAD` (fields, tagged by case),
//! `RTX_OVERSET_PATCH_OFFSET` (the patch's thickness in units of h,
//! default 6 — so the overlap band sits at a fixed NUMBER of cells from
//! the wall and moves inward in metres with refinement; P5-3 holds it in
//! metres instead) and `RTX_OVERSET_PATCH_ROWS` (12; scale it with the
//! offset to keep the wall spacing).
use rtx_cfd::mesh::patch_gen::cylinder_flag_patch; use rtx_cfd::mesh::patch_gen::cylinder_flag_patch;
use rtx_cfd::mesh::PatchSide; use rtx_cfd::mesh::PatchSide;
@@ -80,10 +85,29 @@ fn overlap_rows() -> usize {
) )
} }
/// The patch's thickness in units of h (`RTX_OVERSET_PATCH_OFFSET`, 6).
fn patch_offset_h() -> f64 {
std::env::var("RTX_OVERSET_PATCH_OFFSET")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(6.0)
}
/// The patch's across-rows (`RTX_OVERSET_PATCH_ROWS`, 12).
fn patch_rows() -> usize {
env_usize("RTX_OVERSET_PATCH_ROWS", 12)
}
fn field_tag(case: &str, ny: usize) -> String { fn field_tag(case: &str, ny: usize) -> String {
let rows = overlap_rows(); let rows = overlap_rows();
let offset = patch_offset_h();
format!( format!(
"{case}_ny{ny}_tvd{}{}", "{case}_ny{ny}_tvd{}{}{}",
if (offset - 6.0).abs() < 1e-12 {
String::new()
} else {
format!("_off{offset}")
},
if rows == OversetParameters::default().overlap_rows { if rows == OversetParameters::default().overlap_rows {
String::new() String::new()
} else { } else {
@@ -150,8 +174,8 @@ impl Composite {
0.6, 0.6,
h, h,
0.5 * 0.41 / 41.0, 0.5 * 0.41 / 41.0,
6.0 * h, patch_offset_h() * h,
12, patch_rows(),
4.0, 4.0,
500, 500,
)?; )?;
@@ -34,6 +34,37 @@ const FILLET: f64 = 0.5 * 0.41 / 41.0;
const PATCH_ROWS: usize = 12; const PATCH_ROWS: usize = 12;
const PATCH_STRETCH: f64 = 4.0; const PATCH_STRETCH: f64 = 4.0;
/// The patch's thickness in units of h (`RTX_FSI2O_PATCH_OFFSET`, 6).
/// At the default the overlap band sits a fixed NUMBER of cells from the
/// wall and moves inward in metres with refinement (ny 41 / 62 / 82 →
/// 60 / 40 / 30 mm); P5-3 holds it in metres across the ladder instead.
fn patch_offset_h() -> f64 {
std::env::var("RTX_FSI2O_PATCH_OFFSET")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(6.0)
}
/// The patch's across-rows (`RTX_FSI2O_PATCH_ROWS`, 12; scale it with the
/// offset to keep the wall spacing).
fn patch_rows() -> usize {
std::env::var("RTX_FSI2O_PATCH_ROWS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(PATCH_ROWS)
}
/// The save tag: `fsi2o_ny{ny}`, plus `_off{offset}` off the default
/// patch thickness so a thicker patch never loads the default's state.
fn save_tag(ny: usize) -> String {
let offset = patch_offset_h();
if (offset - 6.0).abs() < 1e-12 {
format!("fsi2o_ny{ny}")
} else {
format!("fsi2o_ny{ny}_off{offset}")
}
}
/// The deforming wall as the patch sees it: the wetted polygon (the /// The deforming wall as the patch sees it: the wetted polygon (the
/// `Interface` walk, anchors included) and the velocity at each vertex. /// `Interface` walk, anchors included) and the velocity at each vertex.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@@ -221,8 +252,8 @@ impl OversetFluid {
FLAG_X1, FLAG_X1,
h, h,
FILLET, FILLET,
6.0 * h, patch_offset_h() * h,
PATCH_ROWS, patch_rows(),
PATCH_STRETCH, PATCH_STRETCH,
sweeps, sweeps,
)?; )?;
@@ -244,8 +275,8 @@ impl OversetFluid {
FLAG_X1, FLAG_X1,
h, h,
FILLET, FILLET,
6.0 * h, patch_offset_h() * h,
PATCH_ROWS, patch_rows(),
PATCH_STRETCH, PATCH_STRETCH,
20_000, 20_000,
)?; )?;
@@ -359,7 +390,7 @@ impl OversetFluid {
pub fn save(&self, dir: &str) -> CfdResult<()> { pub fn save(&self, dir: &str) -> CfdResult<()> {
let dir = std::path::Path::new(dir); let dir = std::path::Path::new(dir);
std::fs::create_dir_all(dir).expect("save dir"); std::fs::create_dir_all(dir).expect("save dir");
let tag = format!("fsi2o_ny{}", self.ny); let tag = save_tag(self.ny);
self.field self.field
.background .background
.save(&dir.join(format!("bg_{tag}.bin")))?; .save(&dir.join(format!("bg_{tag}.bin")))?;
@@ -390,7 +421,7 @@ impl OversetFluid {
/// (the deformed mesh), its vectors, the time and the interface `d` /// (the deformed mesh), its vectors, the time and the interface `d`
/// (`dir/inst_<tag>_<step>/`). /// (`dir/inst_<tag>_<step>/`).
pub fn save_instant(&self, dir: &str, step: usize, d: &[f64], ddot: &[f64]) -> CfdResult<()> { pub fn save_instant(&self, dir: &str, step: usize, d: &[f64], ddot: &[f64]) -> CfdResult<()> {
let tag = format!("fsi2o_ny{}", self.ny); let tag = save_tag(self.ny);
let dir = std::path::Path::new(dir).join(format!("inst_{tag}_{step:06}")); let dir = std::path::Path::new(dir).join(format!("inst_{tag}_{step:06}"));
std::fs::create_dir_all(&dir).expect("instant dir"); std::fs::create_dir_all(&dir).expect("instant dir");
self.field.background.save(&dir.join("bg.bin"))?; self.field.background.save(&dir.join("bg.bin"))?;
@@ -496,6 +527,110 @@ impl OversetFluid {
Ok((fluid, d, t)) Ok((fluid, d, t))
} }
/// The wall load by REGION at the current state (P5-3): for the top
/// face, the bottom face, the tip arc, and the fillets + cylinder —
/// `(name, faces, length, drag, lift, mean t_n, min t_n, max t_n)`
/// with `t_n` the normal traction (≈ −p at the wall), and the patch's
/// pressure level.
pub fn wall_regions(
&self,
d: &[f64],
) -> (
Vec<(&'static str, usize, f64, f64, f64, f64, f64, f64)>,
f64,
) {
let e = self.interface.edges(d);
let tip_mid = [
0.5 * (e.tip[0][0] + e.tip[e.tip.len() - 1][0]),
0.5 * (e.tip[0][1] + e.tip[e.tip.len() - 1][1]),
];
let nearest = |pts: &[[f64; 2]], q: [f64; 2]| -> f64 {
pts.iter()
.map(|p| (p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2))
.fold(f64::INFINITY, f64::min)
};
let mut acc: Vec<(&'static str, usize, f64, f64, f64, f64, f64, f64)> = vec![
(
"top",
0,
0.0,
0.0,
0.0,
0.0,
f64::INFINITY,
f64::NEG_INFINITY,
),
(
"bottom",
0,
0.0,
0.0,
0.0,
0.0,
f64::INFINITY,
f64::NEG_INFINITY,
),
(
"tip arc",
0,
0.0,
0.0,
0.0,
0.0,
f64::INFINITY,
f64::NEG_INFINITY,
),
(
"fillets+cyl",
0,
0.0,
0.0,
0.0,
0.0,
f64::INFINITY,
f64::NEG_INFINITY,
),
];
for (centre, normal, len, traction) in self.solver.patch().wall_tractions(
&self.field.patch,
PatchSide::Inner,
self.solver.time(),
) {
let on_cyl =
((centre[0] - CYL_CENTRE[0]).powi(2) + (centre[1] - CYL_CENTRE[1]).powi(2)).sqrt()
< CYL_R + 1.5 * FILLET;
let near_tip = ((centre[0] - tip_mid[0]).powi(2) + (centre[1] - tip_mid[1]).powi(2))
.sqrt()
< 2.5 * FLAG_T;
let k = if on_cyl {
3
} else if near_tip {
2
} else if nearest(&e.top, centre) <= nearest(&e.bottom, centre) {
0
} else {
1
};
let tn = traction[0] * normal[0] + traction[1] * normal[1];
let r = &mut acc[k];
r.1 += 1;
r.2 += len;
r.3 += traction[0] * len;
r.4 += traction[1] * len;
r.5 += tn * len;
r.6 = r.6.min(tn);
r.7 = r.7.max(tn);
}
for r in &mut acc {
if r.2 > 0.0 {
r.5 /= r.2;
}
}
let p = &self.field.patch.p;
let level = p.iter().sum::<f64>() / p.len().max(1) as f64;
(acc, level)
}
/// The solver-metric momentum chain at the current state (§5.11's /// The solver-metric momentum chain at the current state (§5.11's
/// instrument on the moving patch): the solved-face pin, the ring, the /// 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. /// box in the solver's flux form, the patch's balance, the wall.
@@ -568,7 +703,7 @@ impl OversetFluid {
/// returns its time. /// returns its time.
pub fn load(&mut self, dir: &str) -> CfdResult<f64> { pub fn load(&mut self, dir: &str) -> CfdResult<f64> {
let dir = std::path::Path::new(dir); let dir = std::path::Path::new(dir);
let tag = format!("fsi2o_ny{}", self.ny); let tag = save_tag(self.ny);
self.field.background = FlowField::load(&dir.join(format!("bg_{tag}.bin")))?; self.field.background = FlowField::load(&dir.join(format!("bg_{tag}.bin")))?;
let read = |name: &str| -> Vec<f64> { let read = |name: &str| -> Vec<f64> {
let bytes = std::fs::read(dir.join(format!("patch_{tag}_{name}.bin"))) let bytes = std::fs::read(dir.join(format!("patch_{tag}_{name}.bin")))
@@ -611,8 +746,8 @@ impl OversetFluid {
FLAG_X1, FLAG_X1,
self.h, self.h,
FILLET, FILLET,
6.0 * self.h, patch_offset_h() * self.h,
PATCH_ROWS, patch_rows(),
PATCH_STRETCH, PATCH_STRETCH,
if self.warm_sweeps > 0 && self.warm_base.is_some() { if self.warm_sweeps > 0 && self.warm_base.is_some() {
self.warm_sweeps self.warm_sweeps
@@ -44,7 +44,17 @@ fn fsi2_overset_audit_of_saved_instants() {
}) })
.collect(); .collect();
dirs.sort(); dirs.sort();
// `RTX_FSI2O_AUDIT_ONLY=a,b,c`: only the named instants.
let only: Option<Vec<String>> = std::env::var("RTX_FSI2O_AUDIT_ONLY")
.ok()
.map(|v| v.split(',').map(|s| s.trim().to_string()).collect());
for d in dirs { for d in dirs {
if let Some(only) = &only {
let name = d.file_name().unwrap().to_string_lossy().to_string();
if !only.iter().any(|o| name.contains(o.as_str())) {
continue;
}
}
let (fluid, dvec, t) = let (fluid, dvec, t) =
fsi2_harness::overset::OversetFluid::from_instant(case, ny, flag_nx, &d) fsi2_harness::overset::OversetFluid::from_instant(case, ny, flag_nx, &d)
.expect("instant"); .expect("instant");
@@ -54,6 +64,19 @@ fn fsi2_overset_audit_of_saved_instants() {
d.file_name().unwrap().to_string_lossy(), d.file_name().unwrap().to_string_lossy(),
fluid.chain_line() fluid.chain_line()
); );
// `RTX_FSI2O_AUDIT_FACES=1`: the wall load by region (P5-3).
if std::env::var("RTX_FSI2O_AUDIT_FACES").is_ok() {
let (regions, level) = fluid.wall_regions(&dvec);
let uy_tip = dvec[2 * fluid.interface.tip[fluid.interface.tip.len() / 2] + 1];
let mut line =
format!(" REGIONS t = {t:.4} tip uy {uy_tip:+.4}: p level {level:+.1} Pa |");
for (name, n, len, fx, fy, tn_mean, tn_min, tn_max) in regions {
line += &format!(
" {name}: {n} faces {len:.3} m, drag {fx:+.1}, lift {fy:+.1}, t_n mean {tn_mean:+.0} [{tn_min:+.0}, {tn_max:+.0}] Pa |"
);
}
println!("{line}");
}
} }
} }