embedded3 item 11: moving bodies (end-of-step mask, fresh-cell refill, space-time cut cell: step-averaged apertures, GCL wall flux, Reynolds-transport momentum), the 3D fresh-cell falsifier (plate / circle / stadium, wall + control-volume routes) and the Lipschitz sweep; ghost wall reproduces the 2D falsifier to the digit; cut wall 5–14× smoother on the circle, gates not met (fresh cell's first step); wall.rs split (impose.rs)
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 / Clippy Check (push) Failing after 3s
CI / Build (ubuntu-latest) (push) Failing after 4s
CI / Format Check (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
CI / Build CPU-Only (Explicit) (push) Failing after 1m6s
Documentation / Build API Documentation (push) Failing after 1m9s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-17 16:20:35 -05:00
co-authored by Claude Fable 5.1
parent 0e4c97ed24
commit 5b1621e6ad
12 changed files with 1406 additions and 419 deletions
@@ -0,0 +1,255 @@
//! The embedded-sphere manufactured solution shared by the embedded3
//! wall gates (items 911): the fields, the source, the boundary data,
//! the exact surface integrals, and one steady march measured.
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;
pub const RHO: f64 = 1.0;
pub const MU: f64 = 0.05;
/// The sphere's default centre (off-centre so the exact force is not zero by symmetry).
pub const C: (f64, f64, f64) = (0.6, 0.45, 0.5);
pub const R: f64 = 0.2;
pub fn u3(x: f64, y: f64, z: f64) -> f64 {
(PI * x).sin() * (PI * y).cos() * (PI * z).cos()
}
pub fn v3(x: f64, y: f64, z: f64) -> f64 {
(PI * x).cos() * (PI * y).sin() * (PI * z).cos()
}
pub fn w3(x: f64, y: f64, z: f64) -> f64 {
-2.0 * (PI * x).cos() * (PI * y).cos() * (PI * z).sin()
}
pub 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.
pub 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],
)
}
pub 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],
)
}
pub 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.
pub fn exact_force_and_flux(c: (f64, f64, f64)) -> ([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)
}
pub struct Measurement {
pub l2_velocity: f64,
pub max_div: f64,
pub ghost_correction: f64,
pub force_surface: [f64; 3],
pub skipped: usize,
pub force_cv: [f64; 3],
}
/// March the manufactured solution with the sphere at `c` to steady state on grid `n`.
pub fn measure(n: usize, scheme: WallScheme, c: (f64, f64, f64)) -> 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(move |_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,
}
}