Files
rustytorch/crates/specialized/rtx-cfd/tests/three_d_mms.rs
T
Omar SobhandClaude Fable 5.1 616d2a3394
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
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 / Format Check (push) Failing after 5s
CI / Clippy Check (push) Failing after 4s
CI / Build (ubuntu-latest) (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 9s
CI / Build CPU-Only (Explicit) (push) Failing after 1m36s
Documentation / Build API Documentation (push) Failing after 1m41s
rtx-cfd 3D Stage 1 item 4: three_d::{FlowField3D, piso_host::Piso3Solver} — the 2D embedded predictor/projection transcribed with the z terms appended (u/v/w predictors, six sides incl. periodic z, TVD, the apertured-ready projection on PoissonProblem3D); gates 4/5/6 HELD: MMS + Poiseuille marches value-identical to the 2D embedded solver (multigrid) over 200 steps at nz=1; 3D MMS orders 0.88 upwind / 1.61 TVD, div 1e-9; Beltrami orders 1.08/1.25 with face-averaged data (box compatible to 1e-12); Poiseuille |u−û| ≤ 2e-10, |v|,|w| ≤ 4e-10, p spread ≤ 5e-8 at nz 1 and periodic nz 4
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 11:31:30 -05:00

348 lines
11 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.
//! 3D Stage 1, gate 4: (a) z-invariance identity — the 2D manufactured
//! problem (`embedded_mms.rs`, no body) on the 2D embedded solver with the
//! multigrid Poisson vs the 3D solver at nz = 1 (dz = 1, z slip): the same
//! values over 200 steps; (b) a three-dimensional manufactured solution
//! marched to steady state on `n³` cubes: errors monotone under
//! refinement, orders in [0.75, 2.3], TVD's error below upwind's, and the
//! field divergence-free on every cell.
use rtx_cfd::CfdConfig;
use rtx_cfd::solvers::incompressible::three_d::{
Boundaries3, FlowField3D, Fluid3, Grid3, Piso3Parameters, Piso3Solver, SideBoundary3,
};
use rtx_cfd::solvers::incompressible::{
ConvectionScheme, EmbeddedParameters, EmbeddedPisoSolver, FlowField, PoissonSolverKind,
};
use std::f64::consts::PI;
const RHO: f64 = 1.0;
const MU: f64 = 0.05;
// ---- the 2D manufactured problem (embedded_mms.rs) ----
fn u2(x: f64, y: f64) -> f64 {
(PI * x).sin() * (PI * y).cos()
}
fn v2(x: f64, y: f64) -> f64 {
-(PI * x).cos() * (PI * y).sin()
}
fn source2(x: f64, y: f64) -> (f64, f64) {
let fx = RHO * 0.5 * PI * (2.0 * PI * x).sin()
+ 2.0 * PI * PI * MU * u2(x, y)
+ PI * (PI * x).cos() * (PI * y).sin();
let fy = RHO * 0.5 * PI * (2.0 * PI * y).sin()
+ 2.0 * PI * PI * MU * v2(x, y)
+ PI * (PI * x).sin() * (PI * y).cos();
(fx, fy)
}
fn boundary2(x: f64, y: f64) -> (f64, f64) {
let u = if x <= 0.0 || x >= 1.0 { 0.0 } else { u2(x, y) };
let v = if y <= 0.0 || y >= 1.0 { 0.0 } else { v2(x, y) };
(u, v)
}
fn fluid() -> Fluid3 {
Fluid3 {
density: RHO,
viscosity: MU,
reference_velocity: 1.0,
reference_length: 1.0,
}
}
fn time_step(n: usize) -> f64 {
let h = 1.0 / n as f64;
0.4 * (h * h / (4.0 * MU / RHO)).min(h)
}
#[tokio::test]
async fn nz_one_reproduces_the_two_d_embedded_mms_march() {
let n = 16;
let h = 1.0 / n as f64;
let dt = time_step(n);
let config = CfdConfig::new()
.with_density(RHO)
.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");
two.set_momentum_source(|x, y, _| source2(x, y));
two.set_boundary_velocity(|x, y, _| boundary2(x, y));
let mut three = Piso3Solver::new(
fluid(),
Piso3Parameters {
corrector_steps: 2,
tolerance: 1e-8,
boundaries: Boundaries3 {
z0: SideBoundary3::SlipWall,
z1: SideBoundary3::SlipWall,
..Boundaries3::default()
},
..Piso3Parameters::default()
},
);
three.set_momentum_source(|x, y, _z, _t| {
let (fx, fy) = source2(x, y);
(fx, fy, 0.0)
});
three.set_boundary_velocity(|x, y, _z, _t| {
let (u, v) = boundary2(x, y);
(u, v, 0.0)
});
let mut a = FlowField::new(n, n, h, h).expect("field");
let g = Grid3 {
nx: n,
ny: n,
nz: 1,
dx: h,
dy: h,
dz: 1.0,
};
let mut b = FlowField3D::new(g);
for j in 0..n {
let y = (j as f64 + 0.5) * h;
a.u[(j, 0)] = boundary2(0.0, y).0;
a.u[(j, n)] = boundary2(1.0, y).0;
b.u[g.uface(0, j, 0)] = boundary2(0.0, y).0;
b.u[g.uface(0, j, n)] = boundary2(1.0, y).0;
}
for i in 0..n {
let x = (i as f64 + 0.5) * h;
a.v[(0, i)] = boundary2(x, 0.0).1;
a.v[(n, i)] = boundary2(x, 1.0).1;
b.v[g.vface(0, 0, i)] = boundary2(x, 0.0).1;
b.v[g.vface(0, n, i)] = boundary2(x, 1.0).1;
}
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 {
worst = worst.max((a.u[(j, i)] - b.u[g.uface(0, j, i)]).abs());
}
}
for j in 0..=n {
for i in 0..n {
worst = worst.max((a.v[(j, i)] - b.v[g.vface(0, j, i)]).abs());
}
}
for j in 0..n {
for i in 0..n {
worst = worst.max((a.p[(j, i)] - b.p[g.cell(0, j, i)]).abs());
}
}
assert!(
worst == 0.0,
"step {step}: 3D departs from the 2D embedded MMS march by {worst:.3e}"
);
}
println!(" 200 steps of the manufactured problem value-identical to the 2D embedded solver");
}
// ---- the 3D manufactured solution ----
// u = sin πx cos πy cos πz, v = cos πx sin πy cos πz, w = 2 cos πx cos πy sin πz
// (divergence-free), p = sin πx sin πy sin πz; source = ρ(u·∇)u + ∇p μ∇²u.
fn u3(x: f64, y: f64, z: f64) -> f64 {
(PI * x).sin() * (PI * y).cos() * (PI * z).cos()
}
fn v3(x: f64, y: f64, z: f64) -> f64 {
(PI * x).cos() * (PI * y).sin() * (PI * z).cos()
}
fn w3(x: f64, y: f64, z: f64) -> f64 {
-2.0 * (PI * x).cos() * (PI * y).cos() * (PI * z).sin()
}
fn p3(x: f64, y: f64, z: f64) -> f64 {
(PI * x).sin() * (PI * y).sin() * (PI * z).sin()
}
fn source3(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
let (sx, cx) = (PI * x).sin_cos();
let (sy, cy) = (PI * y).sin_cos();
let (sz, cz) = (PI * z).sin_cos();
let (u, v, w) = (u3(x, y, z), v3(x, y, z), w3(x, y, z));
// Gradients.
let (ux, uy, uz) = (PI * cx * cy * cz, -PI * sx * sy * cz, -PI * sx * cy * sz);
let (vx, vy, vz) = (-PI * sx * sy * cz, PI * cx * cy * cz, -PI * cx * sy * sz);
let (wx, wy, wz) = (
2.0 * PI * sx * cy * sz,
2.0 * PI * cx * sy * sz,
-2.0 * PI * cx * cy * cz,
);
let (px, py, pz) = (PI * cx * sy * sz, PI * sx * cy * sz, PI * sx * sy * cz);
// ∇²(product of three π-trig functions) = 3π² (itself).
let lap = -3.0 * PI * PI;
let fx = RHO * (u * ux + v * uy + w * uz) + px - MU * lap * u;
let fy = RHO * (u * vx + v * vy + w * vz) + py - MU * lap * v;
let fz = RHO * (u * wx + v * wy + w * wz) + pz - MU * lap * w;
(fx, fy, fz)
}
/// The exact field on the cube's boundary with the normal components
/// snapped to their analytic zero.
fn boundary3(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
let u = if x <= 0.0 || x >= 1.0 {
0.0
} else {
u3(x, y, z)
};
let v = if y <= 0.0 || y >= 1.0 {
0.0
} else {
v3(x, y, z)
};
let w = if z <= 0.0 || z >= 1.0 {
0.0
} else {
w3(x, y, z)
};
(u, v, w)
}
struct Measurement {
l2_velocity: f64,
max_div: f64,
steps: usize,
}
fn measure3(n: usize, scheme: ConvectionScheme) -> Measurement {
let h = 1.0 / n as f64;
let dt = time_step(n);
let mut solver = Piso3Solver::new(
fluid(),
Piso3Parameters {
corrector_steps: 2,
tolerance: 1e-8,
convection_scheme: scheme,
..Piso3Parameters::default()
},
);
solver.set_momentum_source(|x, y, z, _t| source3(x, y, z));
solver.set_boundary_velocity(|x, y, z, _t| boundary3(x, y, z));
let g = Grid3 {
nx: n,
ny: n,
nz: n,
dx: h,
dy: h,
dz: h,
};
let mut f = FlowField3D::new(g);
solver.initialize(&mut f);
let mut steps = 0;
for step in 0..200_000 {
let (bu, bv, bw) = (f.u.clone(), f.v.clone(), f.w.clone());
solver.advance(&mut f, dt);
steps = step + 1;
let mut change = 0.0_f64;
for (a, b) in
f.u.iter()
.zip(&bu)
.chain(f.v.iter().zip(&bv))
.chain(f.w.iter().zip(&bw))
{
change = change.max((a - b).abs());
}
if change / dt < 1e-6 {
break;
}
}
let (mut sq, mut vol) = (0.0, 0.0);
let dv = h * h * h;
for k in 0..n {
for j in 0..n {
for i in 1..n {
let e = f.u[g.uface(k, j, i)]
- u3(i as f64 * h, (j as f64 + 0.5) * h, (k as f64 + 0.5) * h);
sq += e * e * dv;
vol += dv;
}
}
}
for k in 0..n {
for j in 1..n {
for i in 0..n {
let e = f.v[g.vface(k, j, i)]
- v3((i as f64 + 0.5) * h, j as f64 * h, (k as f64 + 0.5) * h);
sq += e * e * dv;
vol += dv;
}
}
}
for k in 1..n {
for j in 0..n {
for i in 0..n {
let e = f.w[g.wface(k, j, i)]
- w3((i as f64 + 0.5) * h, (j as f64 + 0.5) * h, k as f64 * h);
sq += e * e * dv;
vol += dv;
}
}
}
Measurement {
l2_velocity: (sq / vol).sqrt(),
max_div: f.max_divergence(),
steps,
}
}
fn ladder(resolutions: &[usize], scheme: ConvectionScheme) -> Vec<Measurement> {
let ms: Vec<Measurement> = resolutions.iter().map(|&n| measure3(n, scheme)).collect();
let errors: Vec<f64> = ms.iter().map(|m| m.l2_velocity).collect();
for (i, &n) in resolutions.iter().enumerate() {
let rate = if i == 0 {
" -".to_string()
} else {
format!("{:5.2}", (errors[i - 1] / errors[i]).log2())
};
println!(
" {scheme:?} n = {n:3} ({:5} steps) L2 velocity {:.6e} order {rate} max |div| {:.3e}",
ms[i].steps, errors[i], ms[i].max_div
);
}
assert!(
errors.windows(2).all(|w| w[1] < w[0]),
"{scheme:?}: errors not monotone {errors:?}"
);
for w in errors.windows(2) {
let rate = (w[0] / w[1]).log2();
assert!(
rate > 0.75 && rate < 2.3,
"{scheme:?}: observed order {rate:.3} outside [0.75, 2.3]; errors {errors:?}"
);
}
for m in &ms {
assert!(m.max_div < 1e-5, "{scheme:?}: max |div| {:.3e}", m.max_div);
}
ms
}
#[test]
fn three_d_mms_orders() {
let resolutions = [12usize, 24];
let up = ladder(&resolutions, ConvectionScheme::Upwind);
let tvd = ladder(&resolutions, ConvectionScheme::TvdVanAlbada);
let tvd_rate = (tvd[0].l2_velocity / tvd[1].l2_velocity).log2();
assert!(tvd_rate > 1.1, "TVD order {tvd_rate:.3} not above 1.1");
for (a, b) in up.iter().zip(&tvd) {
assert!(
b.l2_velocity < a.l2_velocity,
"TVD error not below upwind's"
);
}
}
#[test]
#[ignore = "the three-rung ladder to n = 48 (minutes on the host)"]
fn three_d_mms_orders_three_rungs() {
let resolutions = [12usize, 24, 48];
ladder(&resolutions, ConvectionScheme::Upwind);
ladder(&resolutions, ConvectionScheme::TvdVanAlbada);
}