Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_embedded_mms.rs
T
Omar SobhandClaude Fable 5.1 0e4c97ed24
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-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 / Build CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 4s
CI / Format Check (push) Failing after 12s
Documentation / Build User Guide (push) Successful in 5s
CI / Build (ubuntu-latest) (push) Failing after 1m50s
CI / Clippy Check (push) Failing after 2m5s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m40s
embedded3 item 10: the apertured cut-cell wall (AM-wall) — cutwall.rs classification, apertured projection with the compatible wall flux, cut predictor (V_u = αhA, averaged mass fluxes, implicit wall shear, inertia floor), cut load route; sphere MMS CutCell ≤ GhostBinary at n 12/24 (ratio 0.96), loads 9.3/10.7 % at n 24
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 15:34:07 -05:00

390 lines
13 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 gates 9a and 10: the manufactured solution with an embedded
//! sphere (centre (0.6, 0.45, 0.5), r 0.2, off-centre so the exact force is
//! not zero by symmetry) carrying the exact field as its surface velocity,
//! on the binary ghost wall (item 9) and the apertured cut-cell wall (item
//! 10). The velocity error falls at the scheme's order, every fluid cell
//! is divergence-free (apertured, with the porous surface's flux, on the
//! cut wall), the compatibility correction shrinks, and both load routes
//! converge to the exact surface integral of the manufactured stress (the
//! control-volume route measures F M with M the momentum flux through
//! the porous manufactured surface). Item 10's gate: the cut wall's errors
//! are at most the binary wall's at every n, its loads within 10 % at the
//! finest rung.
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::{
Body, FaceKind, Field, Fluid, Grid, Parameters, Solver, WallScheme,
};
use std::f64::consts::PI;
const RHO: f64 = 1.0;
const MU: f64 = 0.05;
const C: (f64, f64, f64) = (0.6, 0.45, 0.5);
const R: f64 = 0.2;
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()
}
/// The velocity gradient ∂u_i/∂x_j and the pressure gradient.
fn grads(x: f64, y: f64, z: f64) -> ([[f64; 3]; 3], [f64; 3]) {
let (sx, cx) = (PI * x).sin_cos();
let (sy, cy) = (PI * y).sin_cos();
let (sz, cz) = (PI * z).sin_cos();
(
[
[PI * cx * cy * cz, -PI * sx * sy * cz, -PI * sx * cy * sz],
[-PI * sx * sy * cz, PI * cx * cy * cz, -PI * cx * sy * sz],
[
2.0 * PI * sx * cy * sz,
2.0 * PI * cx * sy * sz,
-2.0 * PI * cx * cy * cz,
],
],
[PI * cx * sy * sz, PI * sx * cy * sz, PI * sx * sy * cz],
)
}
fn source3(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
let (g, gp) = grads(x, y, z);
let u = [u3(x, y, z), v3(x, y, z), w3(x, y, z)];
let lap = -3.0 * PI * PI;
let conv = |i: usize| u[0] * g[i][0] + u[1] * g[i][1] + u[2] * g[i][2];
(
RHO * conv(0) + gp[0] - MU * lap * u[0],
RHO * conv(1) + gp[1] - MU * lap * u[1],
RHO * conv(2) + gp[2] - MU * lap * u[2],
)
}
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)
}
/// Exact force `∮ (p I + μ(∇u + ∇uᵀ)) n dA` and momentum flux `∮ ρ u (u·n) dA`
/// over the sphere by a fine Fibonacci quadrature.
fn exact_force_and_flux() -> ([f64; 3], [f64; 3]) {
let n = 200_000;
let golden = PI * (3.0 - 5.0_f64.sqrt());
let (mut f, mut m) = ([0.0; 3], [0.0; 3]);
let da = 4.0 * PI * R * R / n as f64;
for k in 0..n {
let zz = 1.0 - 2.0 * (k as f64 + 0.5) / n as f64;
let rr = (1.0 - zz * zz).sqrt();
let th = golden * k as f64;
let nrm = [rr * th.cos(), rr * th.sin(), zz];
let (x, y, z) = (C.0 + R * nrm[0], C.1 + R * nrm[1], C.2 + R * nrm[2]);
let (g, _) = grads(x, y, z);
let p = p3(x, y, z);
let u = [u3(x, y, z), v3(x, y, z), w3(x, y, z)];
let un = u[0] * nrm[0] + u[1] * nrm[1] + u[2] * nrm[2];
for i in 0..3 {
let mut t = -p * nrm[i];
for j in 0..3 {
t += MU * (g[i][j] + g[j][i]) * nrm[j];
}
f[i] += t * da;
m[i] += RHO * u[i] * un * da;
}
}
(f, m)
}
struct Measurement {
l2_velocity: f64,
max_div: f64,
ghost_correction: f64,
force_surface: [f64; 3],
skipped: usize,
force_cv: [f64; 3],
}
fn measure(n: usize, scheme: WallScheme) -> Measurement {
let h = 1.0 / n as f64;
let dt = 0.4 * (h * h / (4.0 * MU / RHO)).min(h);
let mut solver = Solver::new(
Fluid {
density: RHO,
viscosity: MU,
reference_velocity: 1.0,
reference_length: 1.0,
},
Parameters {
corrector_steps: 2,
tolerance: 1e-8,
convection_scheme: ConvectionScheme::Upwind,
wall_scheme: scheme,
..Parameters::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));
solver.set_body(
Body::sphere(|_t| C, R)
.with_surface_velocity(|x, y, z, _t| (u3(x, y, z), v3(x, y, z), w3(x, y, z))),
);
let g = Grid::cubic(n, n, n, h);
let mut f = Field::new(g);
solver.initialize(&mut f);
let mut last = solver.advance(&mut f, dt);
for _ in 0..200_000 {
let (bu, bv, bw) = (f.u.clone(), f.v.clone(), f.w.clone());
last = solver.advance(&mut f, dt);
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 mask = solver.mask().expect("mask");
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 {
if mask.u_kind(g.uface(k, j, i)) == FaceKind::Fluid {
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 j in 1..n {
for i in 0..n {
if mask.v_kind(g.vface(k, j, i)) == FaceKind::Fluid {
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 {
if mask.w_kind(g.wface(k, j, i)) == FaceKind::Fluid {
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;
}
}
}
}
let body = solver.body().expect("body");
let t = solver.time();
// The apertured divergence per unit volume, the porous surface's flux
// through the wall included (the plain divergence on the binary wall).
let mut max_div = 0.0_f64;
let mut at_vol = 1.0;
let mut sum_flux = 0.0;
let (wall_fluxes, _) = mask.wall_flux_table(body, t);
for k in 0..n {
for j in 0..n {
for i in 0..n {
let idx = g.cell(k, j, i);
if mask.is_fluid_cell(idx) {
let flux = (mask.a_u(g.uface(k, j, i + 1)) * f.u[g.uface(k, j, i + 1)]
- mask.a_u(g.uface(k, j, i)) * f.u[g.uface(k, j, i)])
* h
* h
+ (mask.a_v(g.vface(k, j + 1, i)) * f.v[g.vface(k, j + 1, i)]
- mask.a_v(g.vface(k, j, i)) * f.v[g.vface(k, j, i)])
* h
* h
+ (mask.a_w(g.wface(k + 1, j, i)) * f.w[g.wface(k + 1, j, i)]
- mask.a_w(g.wface(k, j, i)) * f.w[g.wface(k, j, i)])
* h
* h
+ wall_fluxes[idx];
sum_flux += flux.abs();
if (flux / (h * h * h)).abs() > max_div {
max_div = (flux / (h * h * h)).abs();
at_vol = mask.vol(idx);
}
}
}
}
}
println!(
" [{scheme:?} n {n}] max div {max_div:.2e} in a cell of fluid fraction {at_vol:.3e}; Σ|flux| {sum_flux:.2e}; last step residual {:.2e}",
last.final_residual
);
let surface = match scheme {
WallScheme::GhostBinary => mask.surface_force(body, &f, MU, t, 0.5 * h),
WallScheme::CutCell => rtx_cfd::solvers::incompressible::embedded3::SurfaceForce {
f: mask.cut_wall_force(body, &f, MU, t).expect("cut wall"),
samples: 0,
skipped: 0,
},
};
let (i0, i1) = (n / 8, n - n / 8);
let src = |x: f64, y: f64, z: f64| source3(x, y, z);
let force_cv = mask.control_volume_force(&f, dt, RHO, MU, Some(&src), (i0, i1, i0, i1, i0, i1));
Measurement {
l2_velocity: (sq / vol).sqrt(),
max_div,
ghost_correction: solver.ghost_correction().abs(),
force_surface: surface.f,
skipped: surface.skipped,
force_cv,
}
}
fn norm(a: [f64; 3]) -> f64 {
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
}
/// The velocity errors and the two routes' relative force errors per rung.
struct Ladder {
errors: Vec<f64>,
surface: Vec<f64>,
cv: Vec<f64>,
}
fn ladder(resolutions: &[usize], scheme: WallScheme) -> Ladder {
let (fe, m) = exact_force_and_flux();
let f_scale = norm(fe);
let fcv = [fe[0] - m[0], fe[1] - m[1], fe[2] - m[2]];
println!(
" {scheme:?}: exact force {fe:.5?}; momentum flux {m:.5?}; the control-volume route measures {fcv:.5?}"
);
let ms: Vec<Measurement> = resolutions.iter().map(|&n| measure(n, scheme)).collect();
let errors: Vec<f64> = ms.iter().map(|x| x.l2_velocity).collect();
let mut se = Vec::new();
let mut ce = Vec::new();
for (k, (mm, &n)) in ms.iter().zip(resolutions).enumerate() {
let rate = if k == 0 {
" -".to_string()
} else {
format!("{:5.2}", (errors[k - 1] / errors[k]).log2())
};
let s = norm([
mm.force_surface[0] - fe[0],
mm.force_surface[1] - fe[1],
mm.force_surface[2] - fe[2],
]) / f_scale;
let c = norm([
mm.force_cv[0] - fcv[0],
mm.force_cv[1] - fcv[1],
mm.force_cv[2] - fcv[2],
]) / f_scale;
println!(
" n = {n:3} L2 u {:.4e} (order {rate}) max div {:.2e} ghost corr {:.2e} F_surface {:.4?} rel {s:.3e} (skipped {}) F_cv {:.4?} rel {c:.3e}",
mm.l2_velocity,
mm.max_div,
mm.ghost_correction,
mm.force_surface,
mm.skipped,
mm.force_cv
);
se.push(s);
ce.push(c);
}
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 && rate < 2.3,
"order {rate:.3} outside [0.75, 2.3]"
);
}
for mm in &ms {
assert!(mm.max_div < 1e-5, "max div {:.3e}", mm.max_div);
}
assert!(
se.windows(2).all(|w| w[1] < w[0]),
"surface-route error not falling {se:?}"
);
assert!(
ce.windows(2).all(|w| w[1] < w[0]),
"control-volume-route error not falling {ce:?}"
);
Ladder {
errors,
surface: se,
cv: ce,
}
}
/// Item 10's comparison: the cut wall's velocity error at most the binary
/// wall's at every rung; both routes within `load_bound` at the finest.
fn compare(resolutions: &[usize], load_bound: f64) {
let ghost = ladder(resolutions, WallScheme::GhostBinary);
let cut = ladder(resolutions, WallScheme::CutCell);
for (k, &n) in resolutions.iter().enumerate() {
println!(
" n = {n:3} L2 u ghost {:.4e} cut {:.4e} (ratio {:.3})",
ghost.errors[k],
cut.errors[k],
cut.errors[k] / ghost.errors[k]
);
assert!(
cut.errors[k] <= ghost.errors[k],
"cut-cell error above the binary wall's at n = {n}"
);
}
let last = resolutions.len() - 1;
assert!(
cut.surface[last] < load_bound && cut.cv[last] < load_bound,
"cut-cell loads at the finest rung: surface {:.3e}, control volume {:.3e} (bound {load_bound})",
cut.surface[last],
cut.cv[last]
);
}
#[test]
fn embedded_sphere_recovers_the_manufactured_solution() {
ladder(&[12, 24], WallScheme::GhostBinary);
}
#[test]
fn cut_cell_wall_recovers_the_manufactured_solution() {
compare(&[12, 24], 0.2);
}
#[test]
#[ignore = "the three-rung ladder to n = 48 (minutes on the host)"]
fn embedded_sphere_three_rungs() {
ladder(&[12, 24, 48], WallScheme::GhostBinary);
}
#[test]
#[ignore = "item 10's finest rung: the cut wall's loads within 10 % at n = 48"]
fn cut_cell_three_rungs() {
compare(&[12, 24, 48], 0.1);
}