Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_poiseuille.rs
T
Omar SobhandClaude Fable 5.1 8821e18520
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Build CPU-Only (Explicit) (push) Failing after 5s
Documentation / Build API Documentation (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 5s
CI / Format Check (push) Failing after 12s
CI / Build (ubuntu-latest) (push) Failing after 2m10s
CI / Clippy Check (push) Failing after 2m25s
Performance Benchmarks / Run Benchmarks (push) Successful in 4m13s
rtx-cfd embedded3 items 4–6: field.rs + step/{mod, predictor, projection} (364/700/419 lines) — the host PISO step re-laid from the verified three_d code; gates HELD: MMS + Poiseuille marches value-identical to the 2D embedded solver at nz=1 over 200 steps; 3D MMS orders 0.88 upwind / 1.61 TVD, div ≤ 5e-9; Beltrami 1.08 / 1.25 with face-averaged data, div−mean ≤ 7e-8; Poiseuille |u−û| ≤ 8e-10 at nz 1 and periodic nz 4
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 14:55:18 -05:00

272 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! embedded3 gate 6: plane Poiseuille flow on the 3D solver. (a) At
//! `nz = 1` (`dz = 1`, z sides slip) the 3D host step reproduces the 2D
//! embedded solver (no body, multigrid Poisson) to the value over 200
//! steps; (b) the 2D `poiseuille.rs` gates on the 3D field at nz = 1 and on
//! a periodic-z extrusion: `|u û| < 1e-7`, `max |v|, |w| < 1e-7`,
//! `p spread < 1e-6`, with û the discrete channel profile.
use rtx_cfd::CfdConfig;
use rtx_cfd::solvers::incompressible::embedded3::{
Boundaries, Field, Fluid, Grid, Parameters, Side, Solver,
};
use rtx_cfd::solvers::incompressible::{
EmbeddedParameters, EmbeddedPisoSolver, FlowField, PoissonSolverKind,
};
const MU: f64 = 0.1;
const G: f64 = 0.8;
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
}
fn fluid() -> Fluid {
Fluid {
density: 1.0,
viscosity: MU,
reference_velocity: 1.0,
reference_length: 1.0,
}
}
/// The inlet/outlet carry the discrete profile (the embedded solvers
/// re-stamp every Velocity side from the boundary function each step);
/// the walls are no-slip.
fn profile_boundary(n: usize) -> impl Fn(f64, f64) -> f64 + Clone {
let u_hat = discrete_profile(n);
let h = 1.0 / n as f64;
move |x: f64, y: f64| {
if x <= 0.0 || x >= 1.0 {
let j = ((y / h - 0.5).round().max(0.0) as usize).min(n - 1);
u_hat[j]
} else {
0.0
}
}
}
fn solver3(n: usize, nz_periodic: bool) -> Solver {
let z = if nz_periodic {
Side::Periodic
} else {
Side::SlipWall
};
let mut s = Solver::new(
fluid(),
Parameters {
corrector_steps: 2,
tolerance: 1e-8,
boundaries: Boundaries {
z0: z,
z1: z,
..Boundaries::default()
},
..Parameters::default()
},
);
s.set_momentum_source(|_x, _y, _z, _t| (G, 0.0, 0.0));
let ub = profile_boundary(n);
s.set_boundary_velocity(move |x, y, _z, _t| (ub(x, y), 0.0, 0.0));
s
}
fn field3(n: usize, nz: usize, dz: f64) -> Field {
let h = 1.0 / n as f64;
let g = Grid {
nx: n,
ny: n,
nz,
dx: h,
dy: h,
dz,
};
let mut f = Field::new(g);
let u_hat = discrete_profile(n);
for k in 0..nz {
for (j, &uj) in u_hat.iter().enumerate() {
f.u[g.uface(k, j, 0)] = uj;
f.u[g.uface(k, j, n)] = uj;
}
}
f
}
fn field2(n: usize) -> FlowField {
let h = 1.0 / n as f64;
let mut f = FlowField::new(n, n, h, h).expect("field");
let u_hat = discrete_profile(n);
for (j, &uj) in u_hat.iter().enumerate() {
f.u[(j, 0)] = uj;
f.u[(j, n)] = uj;
}
f
}
fn dt_for(n: usize) -> f64 {
let h = 1.0 / n as f64;
0.4 * (h * h / (4.0 * MU)).min(h)
}
/// (a) the value identity at nz = 1.
#[tokio::test]
async fn nz_one_is_the_two_d_embedded_solver() {
let n = 16;
let dt = dt_for(n);
let config = CfdConfig::new()
.with_density(1.0)
.with_viscosity(MU)
.with_reference_velocity(1.0)
.with_reference_length(1.0);
let mut two = EmbeddedPisoSolver::new(
config,
EmbeddedParameters {
corrector_steps: 2,
tolerance: 1e-8,
poisson_solver: PoissonSolverKind::Multigrid,
..EmbeddedParameters::default()
},
)
.expect("2D solver");
two.set_momentum_source(|_x, _y, _t| (G, 0.0));
let ub = profile_boundary(n);
two.set_boundary_velocity(move |x, y, _t| (ub(x, y), 0.0));
let mut three = solver3(n, false);
let mut a = field2(n);
let mut b = field3(n, 1, 1.0);
let g = b.grid;
let mut signed_zero = 0usize;
for step in 0..200 {
two.advance(&mut a, dt).await.expect("2D step");
three.advance(&mut b, dt);
let mut worst = 0.0_f64;
for j in 0..n {
for i in 0..=n {
let (x, y) = (a.u[(j, i)], b.u[g.uface(0, j, i)]);
if x != y {
worst = worst.max((x - y).abs());
} else if x.to_bits() != y.to_bits() {
signed_zero += 1;
}
}
}
for j in 0..=n {
for i in 0..n {
let (x, y) = (a.v[(j, i)], b.v[g.vface(0, j, i)]);
if x != y {
worst = worst.max((x - y).abs());
} else if x.to_bits() != y.to_bits() {
signed_zero += 1;
}
}
}
for j in 0..n {
for i in 0..n {
let (x, y) = (a.p[(j, i)], b.p[g.cell(0, j, i)]);
if x != y {
worst = worst.max((x - y).abs());
}
}
}
assert!(
worst == 0.0,
"step {step}: the 3D field departs from the 2D embedded solver by {worst:.3e}"
);
}
println!(
" 200 steps value-identical to the 2D embedded solver ({signed_zero} ±0 sign differences)"
);
}
struct Measurement {
max_u_vs_discrete: f64,
max_v: f64,
max_w: f64,
p_spread: f64,
steps: usize,
}
fn measure(n: usize, nz: usize, dz: f64, periodic: bool) -> Measurement {
let dt = dt_for(n);
let mut solver = solver3(n, periodic);
let mut f = field3(n, nz, dz);
let g = f.grid;
let u_hat = discrete_profile(n);
let mut steps = 0;
for step in 0..200_000 {
let before = f.u.clone();
solver.advance(&mut f, dt);
steps = step + 1;
let change =
f.u.iter()
.zip(&before)
.fold(0.0_f64, |m, (a, b)| m.max((a - b).abs()))
/ dt;
if change < 1e-8 {
break;
}
}
let mut max_u_vs_discrete = 0.0_f64;
for k in 0..nz {
for (j, &uj) in u_hat.iter().enumerate() {
for i in 1..n {
max_u_vs_discrete = max_u_vs_discrete.max((f.u[g.uface(k, j, i)] - uj).abs());
}
}
}
let max_v = f.v.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
let max_w = f.w.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
let (mut p_min, mut p_max) = (f64::INFINITY, f64::NEG_INFINITY);
for &p in &f.p {
p_min = p_min.min(p);
p_max = p_max.max(p);
}
Measurement {
max_u_vs_discrete,
max_v,
max_w,
p_spread: p_max - p_min,
steps,
}
}
/// (b) the 2D gates on the 3D field.
#[test]
fn poiseuille_is_the_discrete_profile_in_three_d() {
for (n, nz, dz, periodic) in [
(16usize, 1usize, 1.0, false),
(16, 4, 1.0 / 16.0, true),
(32, 1, 1.0, false),
] {
let m = measure(n, nz, dz, periodic);
println!(
" n {n} nz {nz} periodic {periodic}: {} steps; |u û| {:.3e}, max |v| {:.3e}, max |w| {:.3e}, p spread {:.3e}",
m.steps, m.max_u_vs_discrete, m.max_v, m.max_w, m.p_spread
);
assert!(
m.max_u_vs_discrete < 1e-7,
"u departs from the discrete profile by {:.3e}",
m.max_u_vs_discrete
);
assert!(m.max_v < 1e-7, "spurious transverse flow {:.3e}", m.max_v);
assert!(m.max_w < 1e-7, "spurious spanwise flow {:.3e}", m.max_w);
assert!(m.p_spread < 1e-6, "spurious pressure {:.3e}", m.p_spread);
}
}