Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_sphere/mod.rs
T
Omar SobhandClaude Fable 5.1 5b1621e6ad
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
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)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 16:20:35 -05:00

256 lines
8.9 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.
//! 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,
}
}