Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_beltrami.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

270 lines
8.3 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 5: the EthierSteinman (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::embedded3::{Field, Fluid, Grid, Parameters, Solver};
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 = Solver::new(
Fluid {
density: RHO,
viscosity: NU * RHO,
reference_velocity: 1.0,
reference_length: 1.0,
},
Parameters {
corrector_steps: 2,
tolerance: 1e-8,
..Parameters::default()
},
);
solver.set_boundary_velocity(face_averaged(h));
let g = Grid {
nx: n,
ny: n,
nz: n,
dx: h,
dy: h,
dz: h,
};
let mut f = Field::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();
}