Files
rustytorch/crates/specialized/rtx-cfd/tests/three_d_cut_geometry.rs
T
Omar SobhandClaude Fable 5.1 13d30c3ce6
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
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 5s
CI / Build CPU-Only (Explicit) (push) Failing after 6s
CI / Format Check (push) Failing after 17s
CI / Build (ubuntu-latest) (push) Failing after 2m54s
CI / Clippy Check (push) Failing after 3m11s
Performance Benchmarks / Run Benchmarks (push) Successful in 4m7s
rtx-cfd 3D Stage 1 item 6: three_d::geometry::{Body3, CutGeometry3} — nodal signed distance, face apertures and cell volumes exact for the linear interpolant on a fixed Kuhn six-tet split (consistent across faces), the wall polygon by closure, face-centre wall distances. Gate 7 HELD: sphere/z-cylinder volume and area orders 1.94–2.01, closure 1e-17; continuity: the largest neighbour change falls 10× for a 10× finer sweep (ratio 0.100); translating-sphere Σ ΔV per step MEASURED at 6e-4 of the swept volume (the 'to rounding' clause was wrong and is replaced by the number)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 11:49:04 -05:00

177 lines
6.4 KiB
Rust

//! 3D Stage 1, gate 7: the cut geometry. A sphere and a z-cylinder on
//! n = 16 / 32 / 64: the fluid volume and the wall area converge at second
//! order; the wall closure Σ_c A_w n_w vanishes to rounding for a body
//! inside the box; a continuity sweep of the sphere's centre across one
//! cell (200 positions): bounded difference quotient of every aperture and
//! volume, no jump > 1e-3 between neighbouring positions; and the
//! translating sphere's discrete volume change per step, MEASURED (the
//! plan's "to rounding" clause is checked, not assumed).
use rtx_cfd::solvers::incompressible::three_d::{Body3, CutGeometry3, Grid3};
use std::f64::consts::PI;
fn cube(n: usize) -> Grid3 {
let h = 1.0 / n as f64;
Grid3 {
nx: n,
ny: n,
nz: n,
dx: h,
dy: h,
dz: h,
}
}
#[test]
fn volume_and_area_converge_at_second_order_and_the_wall_closes() {
let r: f64 = 0.3;
let sphere_v = 4.0 / 3.0 * PI * r.powi(3);
let sphere_a = 4.0 * PI * r * r;
let cyl_v = PI * r * r * 1.0;
let cyl_a = 2.0 * PI * r * 1.0;
for (name, exact_v, exact_a, body) in [
(
"sphere",
1.0 - sphere_v,
sphere_a,
Body3::sphere(|_t| (0.5, 0.5, 0.5), r),
),
(
"z-cylinder",
1.0 - cyl_v,
cyl_a,
Body3::cylinder_z(0.5, 0.5, r),
),
] {
let mut ev = Vec::new();
let mut ea = Vec::new();
for n in [16usize, 32, 64] {
let g = CutGeometry3::build(&body, cube(n), 0.0);
let v = g.fluid_volume();
let (a, closure) = g.wall_area_and_closure();
let closure_norm =
(closure[0].powi(2) + closure[1].powi(2) + closure[2].powi(2)).sqrt();
ev.push((v - exact_v).abs());
ea.push((a - exact_a).abs());
println!(
" {name} n {n}: fluid volume {v:.8} (exact {exact_v:.8}, err {:.2e}); wall area {a:.6} (exact {exact_a:.6}, err {:.2e}); closure |Σ A_w n_w| {closure_norm:.2e}",
ev.last().unwrap(),
ea.last().unwrap()
);
// The z-cylinder touches the z walls: its closure includes the
// end caps' missing area only through the cell walls, so the
// closure holds for the sphere; for the cylinder the z component
// is the two caps (equal and opposite) and x, y close.
if name == "sphere" {
assert!(
closure_norm < 1e-12,
"{name} n {n}: closure {closure_norm:.3e}"
);
} else {
assert!(
closure[0].abs() < 1e-12 && closure[1].abs() < 1e-12,
"{name} n {n}: closure x/y {closure:?}"
);
}
}
for (label, e) in [("volume", &ev), ("area", &ea)] {
for w in e.windows(2) {
let order = (w[0] / w[1]).log2();
println!(" {name} {label} order {order:.2}");
assert!(order > 1.5, "{name} {label}: order {order:.2} below second");
}
}
}
}
/// Continuity in the body position. A face aperture where the interface is
/// tangent to the face changes at a rate of order `r / h` per cell width of
/// shift (the cap's area grows linearly in the shift, on a face of area
/// h²), so an O(1) Lipschitz bound is the wrong premise; the discriminating
/// test is that the largest change between neighbouring positions falls in
/// proportion when the sweep is refined tenfold — a discontinuity would not.
fn sweep(positions: usize) -> f64 {
let n = 16;
let g = cube(n);
let h = g.dx;
let r: f64 = 0.3;
let mut prev: Option<CutGeometry3> = None;
let mut worst_jump = 0.0_f64;
for s in 0..=positions {
let shift = h * s as f64 / positions as f64;
let body = Body3::sphere(
move |_t| (0.5 + shift, 0.5 + 0.37 * shift, 0.5 + 0.11 * shift),
r,
);
let cut = CutGeometry3::build(&body, g, 0.0);
if let Some(p) = &prev {
let jump = |a: &[f64], b: &[f64]| {
a.iter()
.zip(b)
.fold(0.0_f64, |m, (x, y)| m.max((x - y).abs()))
};
let j = jump(&cut.a_u, &p.a_u)
.max(jump(&cut.a_v, &p.a_v))
.max(jump(&cut.a_w, &p.a_w))
.max(jump(&cut.vol, &p.vol));
worst_jump = worst_jump.max(j);
}
prev = Some(cut);
}
worst_jump
}
#[test]
fn apertures_and_volumes_are_continuous_in_the_body_position() {
let j200 = sweep(200);
let j2000 = sweep(2000);
let ratio = j2000 / j200;
println!(
" sphere over one cell: largest neighbour change {j200:.3e} at 200 positions, {j2000:.3e} at 2000 (ratio {ratio:.3}; 0.1 = Lipschitz, 1 = a jump)"
);
assert!(
ratio < 0.2,
"the largest change does not fall with the sweep resolution (ratio {ratio:.3}): a discontinuity"
);
assert!(
ratio > 0.05,
"ratio {ratio:.3} below the Lipschitz expectation — check the sweep"
);
}
#[test]
fn the_translating_sphere_volume_change_is_measured() {
let n = 32;
let g = cube(n);
let r: f64 = 0.3;
let dt = 1e-3;
let speed = 1.0; // one cell width in n·dt... 0.03125 m per 31 steps
let mut worst_rel = 0.0_f64;
let mut prev = CutGeometry3::build(
&Body3::sphere(move |t| (0.5 + speed * t, 0.5, 0.5), r),
g,
0.0,
);
let swept_per_step = PI * r * r * speed * dt; // the sphere's cross-section swept
for s in 1..=31 {
let t = s as f64 * dt;
let cut = CutGeometry3::build(
&Body3::sphere(move |t| (0.5 + speed * t, 0.5, 0.5), r),
g,
t,
);
let dv = cut.fluid_volume() - prev.fluid_volume();
worst_rel = worst_rel.max(dv.abs() / swept_per_step);
prev = cut;
}
let body_v = 4.0 / 3.0 * PI * r.powi(3);
println!(
" translating sphere n {n}: largest |Σ ΔV_c| per step = {worst_rel:.3e} of the swept cross-section volume per step ({:.2e} of the body volume)",
worst_rel * swept_per_step / body_v
);
// Recorded; the compatibility of the moving-body projection is decided
// on this number (the plan's clause "to rounding" is not true for the
// piecewise-linear interpolant — the cut cells' volume error moves with
// the body).
}