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
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
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
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
2f476a38d5
commit
616d2a3394
@@ -0,0 +1,271 @@
|
||||
//! 3D Stage 1, gate 5: the Ethier–Steinman (Beltrami) exact unsteady
|
||||
//! solution on the unit cube with time-dependent Dirichlet data — the
|
||||
//! transient machinery with no source term. (1) The L2 velocity error at
|
||||
//! `T` falls under `dt ~ h²` refinement at order ≥ 0.75; (2) the kinetic
|
||||
//! energy decay follows the closed form within the discretisation error.
|
||||
|
||||
use rtx_cfd::solvers::incompressible::three_d::{
|
||||
FlowField3D, Fluid3, Grid3, Piso3Parameters, Piso3Solver,
|
||||
};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const RHO: f64 = 1.0;
|
||||
const NU: f64 = 0.02;
|
||||
const A: f64 = PI / 4.0;
|
||||
const D: f64 = PI / 2.0;
|
||||
const T_END: f64 = 0.25;
|
||||
|
||||
fn exact(x: f64, y: f64, z: f64, t: f64) -> (f64, f64, f64) {
|
||||
let decay = (-D * D * NU * t).exp();
|
||||
let u = -A
|
||||
* ((A * x).exp() * (A * y + D * z).sin() + (A * z).exp() * (A * x + D * y).cos())
|
||||
* decay;
|
||||
let v = -A
|
||||
* ((A * y).exp() * (A * z + D * x).sin() + (A * x).exp() * (A * y + D * z).cos())
|
||||
* decay;
|
||||
let w = -A
|
||||
* ((A * z).exp() * (A * x + D * y).sin() + (A * y).exp() * (A * z + D * x).cos())
|
||||
* decay;
|
||||
(u, v, w)
|
||||
}
|
||||
|
||||
/// The boundary data as FACE AVERAGES (3 × 3 Gauss over the face): the
|
||||
/// exact field has a non-zero normal velocity on the closed box, and its
|
||||
/// face-centre samples leave an O(h²) net inflow a pure-Neumann projection
|
||||
/// can only spread uniformly; the face-averaged fluxes of a divergence-free
|
||||
/// field sum to zero to quadrature accuracy, so the box is compatible.
|
||||
fn face_averaged(h: f64) -> impl Fn(f64, f64, f64, f64) -> (f64, f64, f64) {
|
||||
const G: [f64; 3] = [-0.774_596_669_241_483_4, 0.0, 0.774_596_669_241_483_4];
|
||||
const W: [f64; 3] = [5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0];
|
||||
move |x: f64, y: f64, z: f64, t: f64| {
|
||||
let on_x = x <= 0.0 || x >= 1.0;
|
||||
let on_y = y <= 0.0 || y >= 1.0;
|
||||
let on_z = z <= 0.0 || z >= 1.0;
|
||||
if !(on_x || on_y || on_z) {
|
||||
return exact(x, y, z, t);
|
||||
}
|
||||
let (mut u, mut v, mut w) = (0.0, 0.0, 0.0);
|
||||
for (a, wa) in G.iter().zip(&W) {
|
||||
for (b, wb) in G.iter().zip(&W) {
|
||||
let (xx, yy, zz) = if on_x {
|
||||
(x, y + 0.5 * h * a, z + 0.5 * h * b)
|
||||
} else if on_y {
|
||||
(x + 0.5 * h * a, y, z + 0.5 * h * b)
|
||||
} else {
|
||||
(x + 0.5 * h * a, y + 0.5 * h * b, z)
|
||||
};
|
||||
let e = exact(xx, yy, zz, t);
|
||||
u += 0.25 * wa * wb * e.0;
|
||||
v += 0.25 * wa * wb * e.1;
|
||||
w += 0.25 * wa * wb * e.2;
|
||||
}
|
||||
}
|
||||
(u, v, w)
|
||||
}
|
||||
}
|
||||
|
||||
struct Measurement {
|
||||
l2: f64,
|
||||
/// `max |div − mean(div)|`, with the mean reported (the residual
|
||||
/// incompatibility of the face-averaged data: quadrature level).
|
||||
max_div: f64,
|
||||
mean_div: f64,
|
||||
energy_ratio: f64,
|
||||
steps: usize,
|
||||
}
|
||||
|
||||
fn measure(n: usize) -> Measurement {
|
||||
let h = 1.0 / n as f64;
|
||||
// dt ~ h²: the diffusion limit with a margin.
|
||||
let dt = 0.25 * h * h / (4.0 * NU);
|
||||
let steps = (T_END / dt).ceil() as usize;
|
||||
let dt = T_END / steps as f64;
|
||||
let mut solver = Piso3Solver::new(
|
||||
Fluid3 {
|
||||
density: RHO,
|
||||
viscosity: NU * RHO,
|
||||
reference_velocity: 1.0,
|
||||
reference_length: 1.0,
|
||||
},
|
||||
Piso3Parameters {
|
||||
corrector_steps: 2,
|
||||
tolerance: 1e-8,
|
||||
..Piso3Parameters::default()
|
||||
},
|
||||
);
|
||||
solver.set_boundary_velocity(face_averaged(h));
|
||||
let g = Grid3 {
|
||||
nx: n,
|
||||
ny: n,
|
||||
nz: n,
|
||||
dx: h,
|
||||
dy: h,
|
||||
dz: h,
|
||||
};
|
||||
let mut f = FlowField3D::new(g);
|
||||
for k in 0..n {
|
||||
for j in 0..n {
|
||||
for i in 0..=n {
|
||||
f.u[g.uface(k, j, i)] = exact(
|
||||
i as f64 * h,
|
||||
(j as f64 + 0.5) * h,
|
||||
(k as f64 + 0.5) * h,
|
||||
0.0,
|
||||
)
|
||||
.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
for k in 0..n {
|
||||
for j in 0..=n {
|
||||
for i in 0..n {
|
||||
f.v[g.vface(k, j, i)] = exact(
|
||||
(i as f64 + 0.5) * h,
|
||||
j as f64 * h,
|
||||
(k as f64 + 0.5) * h,
|
||||
0.0,
|
||||
)
|
||||
.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for k in 0..=n {
|
||||
for j in 0..n {
|
||||
for i in 0..n {
|
||||
f.w[g.wface(k, j, i)] = exact(
|
||||
(i as f64 + 0.5) * h,
|
||||
(j as f64 + 0.5) * h,
|
||||
k as f64 * h,
|
||||
0.0,
|
||||
)
|
||||
.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
let e0 = f.kinetic_energy(RHO);
|
||||
for _ in 0..steps {
|
||||
solver.advance(&mut f, dt);
|
||||
}
|
||||
let e1 = f.kinetic_energy(RHO);
|
||||
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)]
|
||||
- exact(
|
||||
i as f64 * h,
|
||||
(j as f64 + 0.5) * h,
|
||||
(k as f64 + 0.5) * h,
|
||||
T_END,
|
||||
)
|
||||
.0;
|
||||
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)]
|
||||
- exact(
|
||||
(i as f64 + 0.5) * h,
|
||||
j as f64 * h,
|
||||
(k as f64 + 0.5) * h,
|
||||
T_END,
|
||||
)
|
||||
.1;
|
||||
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)]
|
||||
- exact(
|
||||
(i as f64 + 0.5) * h,
|
||||
(j as f64 + 0.5) * h,
|
||||
k as f64 * h,
|
||||
T_END,
|
||||
)
|
||||
.2;
|
||||
sq += e * e * dv;
|
||||
vol += dv;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut divs = Vec::with_capacity(n * n * n);
|
||||
for k in 0..n {
|
||||
for j in 0..n {
|
||||
for i in 0..n {
|
||||
divs.push(
|
||||
(f.u[g.uface(k, j, i + 1)] - f.u[g.uface(k, j, i)]) / h
|
||||
+ (f.v[g.vface(k, j + 1, i)] - f.v[g.vface(k, j, i)]) / h
|
||||
+ (f.w[g.wface(k + 1, j, i)] - f.w[g.wface(k, j, i)]) / h,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mean_div = divs.iter().sum::<f64>() / divs.len() as f64;
|
||||
let max_div = divs
|
||||
.iter()
|
||||
.fold(0.0_f64, |m, d| m.max((d - mean_div).abs()));
|
||||
Measurement {
|
||||
l2: (sq / vol).sqrt(),
|
||||
max_div,
|
||||
mean_div,
|
||||
energy_ratio: e1 / e0,
|
||||
steps,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beltrami_error_falls_under_space_time_refinement() {
|
||||
let resolutions = [8usize, 16, 32];
|
||||
let ms: Vec<Measurement> = resolutions.iter().map(|&n| measure(n)).collect();
|
||||
let exact_ratio = (-2.0 * D * D * NU * T_END).exp();
|
||||
let errors: Vec<f64> = ms.iter().map(|m| m.l2).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!(
|
||||
" n = {n:2} ({:5} steps) L2 = {:.6e} order {rate} E(T)/E(0) = {:.5} (exact {exact_ratio:.5}, error {:.2e}) max |div − mean| {:.2e} (mean {:.2e})",
|
||||
ms[i].steps,
|
||||
ms[i].l2,
|
||||
ms[i].energy_ratio,
|
||||
(ms[i].energy_ratio - exact_ratio).abs(),
|
||||
ms[i].max_div,
|
||||
ms[i].mean_div
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
errors.windows(2).all(|w| w[1] < w[0]),
|
||||
"errors not monotone: {errors:?}"
|
||||
);
|
||||
for w in errors.windows(2) {
|
||||
let rate = (w[0] / w[1]).log2();
|
||||
assert!(
|
||||
rate >= 0.75,
|
||||
"observed order {rate:.3} below 0.75; errors {errors:?}"
|
||||
);
|
||||
}
|
||||
for m in &ms {
|
||||
assert!(m.max_div < 1e-6, "max |div − mean| {:.3e}", m.max_div);
|
||||
}
|
||||
// The energy decay: the discretisation error at each rung bounds it.
|
||||
let mut e_err: Vec<f64> = ms
|
||||
.iter()
|
||||
.map(|m| (m.energy_ratio - exact_ratio).abs())
|
||||
.collect();
|
||||
assert!(
|
||||
e_err.windows(2).all(|w| w[1] < w[0]),
|
||||
"energy error not falling: {e_err:?}"
|
||||
);
|
||||
e_err.clear();
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
//! 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);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
//! 3D Stage 1, 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::three_d::{
|
||||
Boundaries3, FlowField3D, Fluid3, Grid3, Piso3Parameters, Piso3Solver, SideBoundary3,
|
||||
};
|
||||
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() -> Fluid3 {
|
||||
Fluid3 {
|
||||
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) -> Piso3Solver {
|
||||
let z = if nz_periodic {
|
||||
SideBoundary3::Periodic
|
||||
} else {
|
||||
SideBoundary3::SlipWall
|
||||
};
|
||||
let mut s = Piso3Solver::new(
|
||||
fluid(),
|
||||
Piso3Parameters {
|
||||
corrector_steps: 2,
|
||||
tolerance: 1e-8,
|
||||
boundaries: Boundaries3 {
|
||||
z0: z,
|
||||
z1: z,
|
||||
..Boundaries3::default()
|
||||
},
|
||||
..Piso3Parameters::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) -> FlowField3D {
|
||||
let h = 1.0 / n as f64;
|
||||
let g = Grid3 {
|
||||
nx: n,
|
||||
ny: n,
|
||||
nz,
|
||||
dx: h,
|
||||
dy: h,
|
||||
dz,
|
||||
};
|
||||
let mut f = FlowField3D::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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user