Files
rustytorch/crates/specialized/rtx-cfd/tests/curvilinear_poiseuille.rs
T
Omar SobhandClaude Fable 5.1 c63d79c300
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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
rtx-cfd/rtx-fsi: overset A-P0 GATED + M1 precision probe — curvilinear collocated PISO: relative-reduction pressure stop (the absolute stop floored |du/dt| at 2e-4 on 64²), line-implicit-n sign fix, adjustPhi; gates: Cartesian reduction 1.37–1.40x the staggered error at orders 0.83/0.90; skewed stretched periodic annulus Stokes orders 2.30/2.06 (explicit and line-implicit), upwind 1.08/0.80; Poiseuille exact to 1e-9 on Cartesian and affine-sheared periodic channels (both diffusion variants), varying-skew channel order 2.02 (v 1.9), cell mass 1e-14; divergence ≤ 1e-11 relative every step; snapshot/restore bit-identical. M1: poisson.rs multigrid hierarchy generic over MgScalar (f32/f64), f64 CG keeps its own fine level; MgPrecision on MultigridParameters/EmbeddedParameters/PisoParameters, set_poisson_precision, harness RTX_FSI2_POISSON_F32 (march + noise probe, printed marker); f64 arm bit-identical in vivo (FSI2 default line-for-line with 08-31), f32 arm holds the noise floor and stall pins and the FSI2 band; poisson_equivalence f32 arm
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
2026-09-04 12:40:43 -07:00

213 lines
7.3 KiB
Rust

//! P0 gate 3 (`docs/overset_metal_campaign.md` §5.3): plane Poiseuille
//! flow on the patch. On the Cartesian AND the affinely sheared periodic
//! channel the discrete fixed point is exactly the `poiseuille.rs`
//! profile (congruent parallelogram cells: the non-orthogonal corrections
//! cancel by translation invariance) — recovered to 1e-8 with `v` and the
//! pressure spread at the same level. On a channel of smoothly varying
//! skew and stretch the error against the parabola falls at second
//! order. Cell mass is conserved to 1e-12 everywhere.
use rtx_cfd::mesh::PatchMesh;
use rtx_cfd::mesh::patch_gen::{cartesian, channel_sheared, channel_varying_skew};
use rtx_cfd::solvers::incompressible::{
CurvilinearParameters, CurvilinearPisoSolver, NormalDiffusion, PatchField,
};
use rtx_cfd::{CfdConfig, CfdResult};
const MU: f64 = 0.1;
const G: f64 = 0.8;
fn u_exact(y: f64) -> f64 {
G / (2.0 * MU) * y * (1.0 - y)
}
/// The 1-D discrete channel profile with half-cell wall closures
/// (`tests/poiseuille.rs`).
fn discrete_profile(n: usize) -> Vec<f64> {
let h = 1.0 / n as f64;
let rhs_value = -G * h * h / MU;
let mut diag = vec![-2.0; n];
diag[0] = -3.0;
diag[n - 1] = -3.0;
let mut rhs = vec![rhs_value; n];
let upper = vec![1.0; n];
for j in 1..n {
let factor = 1.0 / diag[j - 1];
diag[j] -= factor * upper[j - 1];
rhs[j] -= factor * rhs[j - 1];
}
let mut u = vec![0.0; n];
u[n - 1] = rhs[n - 1] / diag[n - 1];
for j in (0..n - 1).rev() {
u[j] = (rhs[j] - upper[j] * u[j + 1]) / diag[j];
}
u
}
struct Steady {
field: PatchField,
mesh: PatchMesh,
worst_mass: f64,
}
async fn steady(mesh: PatchMesh, diffusion: NormalDiffusion, dt_factor: f64) -> CfdResult<Steady> {
let mut h = f64::INFINITY;
for c in 0..mesh.cell_count() {
for (f, _) in mesh.cell_faces(c) {
let d = mesh.faces()[f].d;
h = h.min((d[0] * d[0] + d[1] * d[1]).sqrt());
}
}
let dt = dt_factor * 0.4 * h * h / (4.0 * MU);
let config = CfdConfig::new().with_density(1.0).with_viscosity(MU);
let params = CurvilinearParameters {
tolerance: 1e-6,
normal_diffusion: diffusion,
..CurvilinearParameters::default()
};
let mut solver = CurvilinearPisoSolver::new(config, params, mesh)?;
solver.set_boundary_velocity(|_, _, _| (0.0, 0.0));
solver.set_momentum_source(|_, _, _| (G, 0.0));
let mut field = PatchField::new(solver.mesh());
solver.initialize(&mut field, |_, _| (0.0, 0.0));
// Mass defect at the steady state, relative to the largest face flux
// (during the transient it is the pressure solver's residual).
let env = |k: &str, d: f64| {
std::env::var(k)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(d)
};
// 1e-9: the rounding floor of |du/dt| sits at ~2e-11 on the 32² channels
// (measured: 0 pressure iterations, divergence 1e-16) and the gates are
// at 1e-8.
let steady_tol = env("RTX_CURV_STEADY", 1e-9);
let trace = std::env::var("RTX_CURV_TRACE").is_ok();
let mut worst_mass = 0.0_f64;
let mut converged = false;
for step in 0..2_000_000 {
let before = field.u.clone();
let r = solver.advance(&mut field, dt).await?;
assert!(r.poisson_converged, "{r:?}");
let scale: f64 = field.flux.iter().map(|f| f.abs()).fold(0.0, f64::max);
worst_mass = r.max_divergence / scale.max(1e-300);
let change = field
.u
.iter()
.zip(&before)
.map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max);
if trace && step % 50_000 == 0 {
println!(
" step {step} t={:.2} |du/dt| {:.3e} iters {} div {:.2e}",
solver.time(),
change / dt,
r.poisson_iterations,
r.max_divergence
);
}
if change / dt < steady_tol {
converged = true;
break;
}
}
assert!(converged, "no steady state");
let mesh = solver.mesh().clone();
Ok(Steady {
field,
mesh,
worst_mass,
})
}
fn max_vs_discrete(s: &Steady, n: usize) -> (f64, f64, f64) {
let u_hat = discrete_profile(n);
let (mut du, mut v, mut pmin, mut pmax) = (0.0_f64, 0.0_f64, f64::INFINITY, f64::NEG_INFINITY);
for c in 0..s.mesh.cell_count() {
let (k, _) = s.mesh.cell_ki(c);
du = du.max((s.field.u[c] - u_hat[k]).abs());
v = v.max(s.field.v[c].abs());
pmin = pmin.min(s.field.p[c]);
pmax = pmax.max(s.field.p[c]);
}
(du, v, pmax - pmin)
}
#[tokio::test]
async fn cartesian_and_sheared_channels_hit_the_discrete_profile_exactly() -> CfdResult<()> {
let n = 16;
let shapes: [(&str, Box<dyn Fn() -> CfdResult<PatchMesh>>); 3] = [
(
"cartesian",
Box::new(move || cartesian(n, n, 1.0, 1.0, true)),
),
(
"sheared 0.4",
Box::new(move || channel_sheared(1.0, 1.0, n, n, 0.4, true)),
),
(
"sheared -0.7",
Box::new(move || channel_sheared(1.0, 1.0, n, n, -0.7, true)),
),
];
for (name, mesh) in &shapes {
for diffusion in [NormalDiffusion::Explicit, NormalDiffusion::LineImplicit] {
let s = steady(mesh()?, diffusion, 1.0).await?;
let (du, v, dp) = max_vs_discrete(&s, n);
println!(
"{name} {diffusion:?}: |u - u_hat| {du:.3e}, |v| {v:.3e}, p spread {dp:.3e}, mass {:.3e}",
s.worst_mass
);
assert!(du < 1e-8, "{name} {diffusion:?}: |u - u_hat| = {du:.3e}");
assert!(v < 1e-8, "{name} {diffusion:?}: |v| = {v:.3e}");
assert!(dp < 1e-8, "{name} {diffusion:?}: p spread {dp:.3e}");
assert!(
s.worst_mass < 1e-12,
"{name} {diffusion:?}: mass defect {:.3e}",
s.worst_mass
);
}
}
Ok(())
}
#[tokio::test]
async fn varying_skew_channel_converges_to_the_parabola_at_second_order() -> CfdResult<()> {
let mut errs = Vec::new();
let mut vs = Vec::new();
let only: Option<usize> = std::env::var("RTX_CURV_N")
.ok()
.and_then(|v| v.parse().ok());
for n in [16usize, 32, 64] {
if only.is_some_and(|o| o != n) {
continue;
}
let s = steady(
channel_varying_skew(1.0, 1.0, n, n, 0.1, 2.0, true)?,
NormalDiffusion::Explicit,
1.0,
)
.await?;
let (mut e, mut v) = (0.0_f64, 0.0_f64);
for c in 0..s.mesh.cell_count() {
let y = s.mesh.centre(c)[1];
e = e.max((s.field.u[c] - u_exact(y)).abs());
v = v.max(s.field.v[c].abs());
}
println!(
"varying skew n={n}: |u - parabola| {e:.3e}, |v| {v:.3e}, mass {:.3e}",
s.worst_mass
);
assert!(s.worst_mass < 1e-12);
errs.push(e);
vs.push(v);
}
let o: Vec<f64> = errs.windows(2).map(|p| (p[0] / p[1]).log2()).collect();
let ov: Vec<f64> = vs.windows(2).map(|p| (p[0] / p[1]).log2()).collect();
println!("varying skew orders u {o:?}, v {ov:?}");
if only.is_none() {
assert!(o.iter().all(|&x| x >= 1.8), "orders {o:?}");
}
Ok(())
}