rtx-cfd: OversetPisoSolver::momentum_residual — the background predictor's own staggered stencil (u_rhs/v_rhs, factored out of the predictor bit-identically) evaluated on every face of a NaN-masked field; solved faces read rounding, the active–fringe interface reads the composite's pressure level offset δ·h (cancels in the sum), prescribed fringe–fringe / fringe–hole faces read the stamping's momentum injection; hole ghosts (p, u, v) from a band widened three rows into the hole make every ring face evaluable; overset_cfd1 prints the buckets, the ring x-bands and δ at the settled state; pin: residual vanishes on the solved faces
CI / Format Check (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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:
co-authored by
Claude Fable 5.1
parent
cbec40b999
commit
6f9b0d43b2
@@ -7,12 +7,13 @@
|
||||
//! Reference (FEATFLOW level 6): drag 14.2929, lift 1.11905. The embedded
|
||||
//! staircase measured drag 15.71 (surface) / 15.62 (CV) at ny = 41 (+10%).
|
||||
|
||||
use rtx_cfd::mesh::PatchSide;
|
||||
use rtx_cfd::mesh::patch_gen::cylinder_flag_patch;
|
||||
use rtx_cfd::mesh::PatchSide;
|
||||
use rtx_cfd::solvers::incompressible::{
|
||||
AleBoundaries, CellClass, CurvilinearParameters, CurvilinearPisoSolver, EmbeddedParameters,
|
||||
EmbeddedPisoSolver, FlowField, NormalDiffusion, OversetField, OversetParameters,
|
||||
OversetPisoSolver, PatchConvection, PatchField, PoissonSolverKind, SideBoundary,
|
||||
EmbeddedPisoSolver, FlowField, MomentumResidual, NormalDiffusion, OversetField,
|
||||
OversetParameters, OversetPisoSolver, PatchConvection, PatchField, PoissonSolverKind,
|
||||
SideBoundary,
|
||||
};
|
||||
use rtx_cfd::{CfdConfig, CfdResult};
|
||||
|
||||
@@ -37,9 +38,10 @@ struct Cfd1 {
|
||||
seconds: f64,
|
||||
rounds_mean: f64,
|
||||
dt: f64,
|
||||
residual: MomentumResidual,
|
||||
}
|
||||
|
||||
async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
||||
async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
|
||||
let h = H / ny as f64;
|
||||
let nx = (L / h).round() as usize;
|
||||
let mu = RHO * NU;
|
||||
@@ -182,10 +184,6 @@ async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
||||
let mut steps = 0;
|
||||
let mut rounds_total = 0usize;
|
||||
let mut correctors_total = 0usize;
|
||||
let max_steps: usize = std::env::var("RTX_OVERSET_CFD1_MAX_STEPS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(2_000_000);
|
||||
let trace_first = std::env::var("RTX_OVERSET_CFD1_TRACE").is_ok();
|
||||
loop {
|
||||
let r = solver.advance(&mut field, dt).await?;
|
||||
@@ -285,6 +283,52 @@ async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
||||
100.0 * (ring.0 - hole.0) / wall[0],
|
||||
100.0 * (hole.0 - wall[0]) / wall[0],
|
||||
);
|
||||
// P4 option B: the momentum residual of the solver's OWN staggered
|
||||
// upwind stencil on every background face at the settled state. Solved
|
||||
// faces read zero by construction (the pin below); the prescribed
|
||||
// faces' sum is the momentum the stamping injects, in the solver's
|
||||
// metric and without the staircase curves' face-formula error.
|
||||
let mr = solver.momentum_residual(&field, dt);
|
||||
let pct = |b: &rtx_cfd::solvers::incompressible::ResidualBucket| 100.0 * b.fx / wall[0];
|
||||
println!(
|
||||
" momentum residual ny = {ny} [N/m, x / y; % of wall drag; faces evaluated/total]: solved far Σr ({:+.3e}, {:+.3e}) Σ|r| ({:.3e}, {:.3e}) {}/{} | solved near ring Σr ({:+.3e}, {:+.3e}) Σ|r| ({:.4}, {:.4}) max|r| ({:.3e}, {:.3e}) {}/{} | fringe–fringe ({:+.4}, {:+.4}) {:+.2}% {}/{} | fringe–hole ({:+.4}, {:+.4}) {:+.2}% {}/{} | hole–hole skipped {} (ghosts: {} cells, {} faces) | Σ|r| fringe–fringe ({:.4}, {:.4}) fringe–hole ({:.4}, {:.4}); ring total ({:+.4}, {:+.4}) {:+.2}% vs routes' ring defect {:+.4} ({:+.2}%)",
|
||||
mr.solved_far.fx, mr.solved_far.fy, 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, mr.solved_near.abs_x, mr.solved_near.abs_y, mr.solved_near.max_abs_x, mr.solved_near.max_abs_y, mr.solved_near.evaluated, mr.solved_near.total,
|
||||
mr.fringe_fringe.fx, mr.fringe_fringe.fy, pct(&mr.fringe_fringe), mr.fringe_fringe.evaluated, mr.fringe_fringe.total,
|
||||
mr.fringe_hole.fx, mr.fringe_hole.fy, pct(&mr.fringe_hole), mr.fringe_hole.evaluated, mr.fringe_hole.total,
|
||||
mr.hole_hole_skipped, mr.hole_ghosts, mr.ghost_faces,
|
||||
mr.fringe_fringe.abs_x, mr.fringe_fringe.abs_y, mr.fringe_hole.abs_x, mr.fringe_hole.abs_y,
|
||||
mr.fringe_fringe.fx + mr.fringe_hole.fx, mr.fringe_fringe.fy + mr.fringe_hole.fy,
|
||||
pct(&mr.fringe_fringe) + pct(&mr.fringe_hole),
|
||||
hole.0 - ring.0,
|
||||
100.0 * (hole.0 - ring.0) / wall[0],
|
||||
);
|
||||
// Where along the ring: the prescribed u faces' x-momentum residual in
|
||||
// x-bands (cylinder front, cylinder–flag junction, flag, trailing edge).
|
||||
let mut bands = [
|
||||
(0.0_f64, 0.20, 0.0_f64, 0usize),
|
||||
(0.20, 0.30, 0.0, 0),
|
||||
(0.30, 0.55, 0.0, 0),
|
||||
(0.55, 1.0, 0.0, 0),
|
||||
];
|
||||
for f in mr.prescribed.iter().filter(|f| f.is_u && f.r.is_finite()) {
|
||||
let x = f.i as f64 * h;
|
||||
if let Some(b) = bands.iter_mut().find(|b| x >= b.0 && x < b.1) {
|
||||
b.2 += f.r;
|
||||
b.3 += 1;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
" ring x-momentum residual by x-band ny = {ny} (N/m, u faces): {}; level offset δ = {:.3e} Pa on {} + {} interface faces",
|
||||
bands
|
||||
.iter()
|
||||
.map(|b| format!("x {:.2}–{:.2}: {:+.4} ({} faces)", b.0, b.1, b.2, b.3))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | "),
|
||||
mr.level_offset(h),
|
||||
mr.interface_u,
|
||||
mr.interface_v
|
||||
);
|
||||
// The wall load split and the fringe ring's extent (the tight box in
|
||||
// the sensitivity list must stay outside it).
|
||||
let (mut jmin, mut jmax, mut imin, mut imax) = (usize::MAX, 0, usize::MAX, 0);
|
||||
@@ -353,6 +397,7 @@ async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
||||
seconds,
|
||||
rounds_mean: rounds_total as f64 / correctors_total.max(1) as f64,
|
||||
dt,
|
||||
residual: mr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -367,7 +412,11 @@ async fn cfd1_on_the_overset_against_the_featflow_reference() -> CfdResult<()> {
|
||||
},
|
||||
);
|
||||
for &ny in &resolutions {
|
||||
let r = run_cfd1(ny).await?;
|
||||
let max_steps: usize = std::env::var("RTX_OVERSET_CFD1_MAX_STEPS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(2_000_000);
|
||||
let r = run_cfd1(ny, max_steps).await?;
|
||||
let rel = |a: f64, b: f64| 100.0 * (a - b) / b;
|
||||
println!(
|
||||
" CFD1 overset ny = {ny} (h = {:.4}, dt = {:.2e}, patch {}): wall drag {:.4} ({:+.2}%) lift {:.4} ({:+.2}%); control volume drag {:.4} ({:+.2}%) lift {:.4}; routes differ {:.2}%; [{} steps, {:.0} s, Schwarz rounds mean {:.2}] reference {REF_DRAG} / {REF_LIFT}; embedded staircase at ny=41: 15.71 / 15.62 (+10%)",
|
||||
@@ -394,3 +443,67 @@ async fn cfd1_on_the_overset_against_the_featflow_reference() -> CfdResult<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The residual diagnostic is the solver's own operator: on every SOLVED
|
||||
/// background face away from the ring the momentum residual (time term
|
||||
/// included) is zero to rounding; on the solved faces NEXT to the ring it
|
||||
/// is a constant per face that cancels in the sum — the composite's
|
||||
/// pressure LEVEL offset between the active cells (whose `p'` had its mean
|
||||
/// removed) and the fringe cells (re-stamped from the patch, which never
|
||||
/// saw that shift); and the ring buckets are populated. This is what makes
|
||||
/// the prescribed faces' sum readable as the stamping's momentum injection
|
||||
/// in the solver's metric (§5.11, option B).
|
||||
#[tokio::test]
|
||||
async fn momentum_residual_vanishes_on_the_solved_faces() -> CfdResult<()> {
|
||||
let r = run_cfd1(41, 5).await?;
|
||||
let mr = &r.residual;
|
||||
let scale = r.drag_surface.abs().max(1.0);
|
||||
let far = &mr.solved_far;
|
||||
assert_eq!(
|
||||
far.evaluated, far.total,
|
||||
"every far solved face is evaluable"
|
||||
);
|
||||
assert!(
|
||||
far.abs_x <= 1e-9 * scale && far.abs_y <= 1e-9 * scale,
|
||||
"solved far: Σ|r| = ({:.3e}, {:.3e}) is not rounding against {scale:.3}",
|
||||
far.abs_x,
|
||||
far.abs_y
|
||||
);
|
||||
let near = &mr.solved_near;
|
||||
assert_eq!(
|
||||
near.evaluated, near.total,
|
||||
"every near solved face is evaluable"
|
||||
);
|
||||
assert!(
|
||||
near.fx.abs() <= 1e-9 * scale && near.fy.abs() <= 1e-9 * scale,
|
||||
"solved near: Σr = ({:.3e}, {:.3e}) does not cancel against {scale:.3}",
|
||||
near.fx,
|
||||
near.fy
|
||||
);
|
||||
// A pure level offset: every active–fringe INTERFACE face carries the
|
||||
// same |r| = δ·h and every other near face (one that only reads a
|
||||
// prescribed velocity) reads zero, so Σ|r| = N_interface · max|r| on
|
||||
// each lattice.
|
||||
assert!(
|
||||
(near.abs_x - mr.interface_u as f64 * near.max_abs_x).abs() <= 1e-6 * near.abs_x.max(1e-300)
|
||||
&& (near.abs_y - mr.interface_v as f64 * near.max_abs_y).abs()
|
||||
<= 1e-6 * near.abs_y.max(1e-300),
|
||||
"solved near: not a uniform level offset on the interface — Σ|r| ({:.4e}, {:.4e}) vs N·max|r| ({:.4e}, {:.4e}) with N = ({}, {})",
|
||||
near.abs_x,
|
||||
near.abs_y,
|
||||
mr.interface_u as f64 * near.max_abs_x,
|
||||
mr.interface_v as f64 * near.max_abs_y,
|
||||
mr.interface_u,
|
||||
mr.interface_v
|
||||
);
|
||||
assert!(mr.fringe_fringe.evaluated > 0 && mr.fringe_hole.evaluated > 0);
|
||||
assert_eq!(
|
||||
mr.fringe_fringe.evaluated, mr.fringe_fringe.total,
|
||||
"every fringe–fringe face has a fully valid stencil"
|
||||
);
|
||||
assert_eq!(
|
||||
mr.fringe_hole.evaluated, mr.fringe_hole.total,
|
||||
"every fringe–hole face has a fully valid stencil with the ghost band"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user