rtx-cfd: CurvilinearPisoSolver::momentum_balance — the patch's own momentum balance on its solved cells in the scheme's fluxes (outward ρFu_f with the predictor's face value, Laplacian-form μ∇u·S on the solved/acceptor interface and the wall, the least-squares pressure volume sum vs the face-pressure integrals); flux_force, wall_force, pressure_defect δP; overset_cfd1 prints it and the acceptor band's mismatch, saves the patch flux, and RTX_OVERSET_CFD1_LOAD=dir runs the diagnostics offline on saved fields
Performance Benchmarks / Run Benchmarks (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
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

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
Omar Sobh
2026-09-06 14:13:04 -07:00
co-authored by Claude Fable 5.1
parent e31576d543
commit 0215c7d6a5
5 changed files with 267 additions and 49 deletions
+107 -46
View File
@@ -177,6 +177,40 @@ async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
)
};
// `RTX_OVERSET_CFD1_LOAD=dir`: settled fields saved by a previous run
// (`RTX_OVERSET_CFD1_SAVE`) replace the march — the diagnostics below
// run offline in seconds instead of the 2050 min settle.
let loaded = match std::env::var("RTX_OVERSET_CFD1_LOAD") {
Ok(dir) => {
let tag = format!(
"ny{ny}_{}",
if std::env::var("RTX_OVERSET_CFD1_TVD").is_ok() {
"tvd"
} else {
"upwind"
}
);
let dir = std::path::Path::new(&dir);
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()
};
field.patch.u = read("u");
field.patch.v = read("v");
field.patch.p = read("p");
field.patch.flux = read("flux");
assert_eq!(field.patch.u.len(), solver.patch().mesh().cell_count());
assert_eq!(field.patch.flux.len(), solver.patch().mesh().faces().len());
println!(" loaded settled fields {tag} from {}", dir.display());
true
}
Err(_) => false,
};
let start = std::time::Instant::now();
let flow_through = L / U_MEAN;
let min_steps = (flow_through / dt).ceil() as usize;
@@ -185,22 +219,23 @@ async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
let mut rounds_total = 0usize;
let mut correctors_total = 0usize;
let trace_first = std::env::var("RTX_OVERSET_CFD1_TRACE").is_ok();
loop {
let r = solver.advance(&mut field, dt).await?;
steps += 1;
let every: usize = std::env::var("RTX_OVERSET_CFD1_TRACE_EVERY")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
if (trace_first && steps <= 6) || (every > 0 && steps % every == 0) {
let pmax = field
.background
.p
.iter()
.fold(0.0_f64, |m, v| m.max(v.abs()));
let ppmax = field.patch.p.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
let upmax = field.patch.u.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
println!(
if !loaded {
loop {
let r = solver.advance(&mut field, dt).await?;
steps += 1;
let every: usize = std::env::var("RTX_OVERSET_CFD1_TRACE_EVERY")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
if (trace_first && steps <= 6) || (every > 0 && steps % every == 0) {
let pmax = field
.background
.p
.iter()
.fold(0.0_f64, |m, v| m.max(v.abs()));
let ppmax = field.patch.p.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
let upmax = field.patch.u.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
println!(
" step {steps}: rounds {:?} converged {} stalled {} bg res {:.2e} patch div {:.2e} patch iters {} conv {} | max|p| bg {pmax:.3e} patch {ppmax:.3e} max|u| patch {upmax:.3e} | defect bg {:.2e} patch {:.2e}",
r.rounds,
r.schwarz_converged,
@@ -212,25 +247,26 @@ async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
r.background_mass_defect,
r.patch_mass_defect
);
}
if steps >= max_steps {
break;
}
rounds_total += r.rounds.iter().sum::<usize>();
correctors_total += r.rounds.len();
if steps % 50 == 0 {
let (fx, _) = cv_force(&field, &solver);
history.push(fx);
let load = solver
.patch()
.surface_force(&field.patch, PatchSide::Inner, solver.time());
let umax = field
.background
.u
.iter()
.fold(0.0_f64, |m, v| m.max(v.abs()));
if steps % 500 == 0 || !umax.is_finite() {
println!(
}
if steps >= max_steps {
break;
}
rounds_total += r.rounds.iter().sum::<usize>();
correctors_total += r.rounds.len();
if steps % 50 == 0 {
let (fx, _) = cv_force(&field, &solver);
history.push(fx);
let load =
solver
.patch()
.surface_force(&field.patch, PatchSide::Inner, solver.time());
let umax = field
.background
.u
.iter()
.fold(0.0_f64, |m, v| m.max(v.abs()));
if steps % 500 == 0 || !umax.is_finite() {
println!(
" ny = {ny}: step {steps} t = {:.2} s drag_cv {fx:.4} drag_wall {:.4} lift_wall {:.4} max|u| {umax:.3} rounds {:?} bg res {:.1e} patch div {:.1e} [{:.0} s]",
solver.time(),
load.total()[0],
@@ -240,20 +276,21 @@ async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
r.patch_max_divergence,
start.elapsed().as_secs_f64()
);
}
assert!(
umax.is_finite(),
"velocity became non-finite at step {steps}"
);
if steps >= min_steps && history.len() > 4 {
let now = history[history.len() - 1];
let then = history[history.len() - 5];
if ((now - then) / now).abs() < 1e-4 {
break;
}
assert!(
umax.is_finite(),
"velocity became non-finite at step {steps}"
);
if steps >= min_steps && history.len() > 4 {
let now = history[history.len() - 1];
let then = history[history.len() - 5];
if ((now - then) / now).abs() < 1e-4 {
break;
}
}
}
assert!(steps < 2_000_000, "CFD1 at ny = {ny} did not settle");
}
assert!(steps < 2_000_000, "CFD1 at ny = {ny} did not settle");
}
let seconds = start.elapsed().as_secs_f64();
let load = solver
@@ -373,6 +410,29 @@ async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
wall[0] - sf.0 - ring_sum,
100.0 * (wall[0] - sf.0 - ring_sum) / wall[0],
);
// The patch's own momentum balance on its solved cells (scheme
// fluxes; the unsteady term is omitted — settled state): the balance
// residual is the gate; flux_force wall_force = the least-squares
// pressure's non-conservation δP; the acceptor band's mismatch is
// then (box + ring Σr) flux_force, all in N/m.
let pb = solver.patch().momentum_balance(&field.patch, solver.time());
let ff = pb.flux_force();
let fw = pb.wall_force();
let dp = pb.pressure_defect();
let bal = pb.balance();
let hole_flux = sf.0 + ring_sum;
println!(
" patch momentum balance ny = {ny} [N/m x / y; {} solved cells, {} interface faces, {} wall faces]: balance residual ({:+.3e}, {:+.3e}) | flux-form force through the interface ({:.4}, {:.4}) | wall force, scheme fluxes ({:.4}, {:.4}) | wall force, surface formula ({:.4}, {:.4}) | pressure defect δP = p_ls p_face ({:+.4}, {:+.4}) [{:+.2}% of wall drag] | pieces: conv_acc ({:+.4}, {:+.4}) visc_acc ({:+.4}, {:+.4}) p_face_acc ({:+.4}, {:+.4}) visc_wall ({:+.4}, {:+.4}) p_face_wall ({:+.4}, {:+.4}) p_ls ({:+.4}, {:+.4}) | acceptor band: background hole flux {:.4} patch interface {:.4} = {:+.4} ({:+.2}%)",
pb.cells, pb.acc_faces, pb.wall_faces,
bal[0], bal[1],
ff[0], ff[1],
fw[0], fw[1],
wall[0], wall[1],
dp[0], dp[1], 100.0 * dp[0] / wall[0],
pb.conv_acc[0], pb.conv_acc[1], pb.visc_acc[0], pb.visc_acc[1], pb.p_face_acc[0], pb.p_face_acc[1],
pb.visc_wall[0], pb.visc_wall[1], pb.p_face_wall[0], pb.p_face_wall[1], pb.p_ls[0], pb.p_ls[1],
hole_flux, ff[0], hole_flux - ff[0], 100.0 * (hole_flux - ff[0]) / wall[0],
);
// `RTX_OVERSET_CFD1_SAVE=dir`: the settled fields, for offline
// diagnostics without the march (background in `FlowField::save`'s
// format; patch u, v, p as raw little-endian f64 vectors).
@@ -392,6 +452,7 @@ async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
("u", &field.patch.u),
("v", &field.patch.v),
("p", &field.patch.p),
("flux", &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");