Files
rustytorch/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/cut.rs
T
Omar SobhandClaude Fable 5.1 d4ffac9ac7
CI / Build (ubuntu-latest) (push) Failing after 3s
Performance Benchmarks / Run Benchmarks (push) Failing after 4s
CI / Format Check (push) Failing after 5s
CI / Clippy Check (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 3s
Documentation / Build User Guide (push) Successful in 3s
CI / Build CPU-Only (Explicit) (push) Failing after 1m7s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
rtx-cfd embedded3 item 8: body.rs (SDF, surface velocity, normal, samplers: sphere Fibonacci lattice, z-cylinder, extruded 2D body) + cut.rs (Kuhn six-tet apertures/volumes, closure, wall distances); gate 8 HELD: volume/area orders 1.94–2.01, closure ≤ 3e-17, Lipschitz ratio 0.100, Σ ΔV per step 6.2e-4 of the swept volume
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 14:58:06 -05:00

262 lines
10 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 cut geometry of an embedded body on the grid: φ at the cell
//! corners; inside every cell the interface is the LINEAR interpolant on a
//! fixed Kuhn split into six tetrahedra (each face into two triangles along
//! the same diagonal from both sides), so face apertures and cell volumes
//! are exact for the interpolant, continuous in the corner values, and
//! consistent across shared faces — the wall polygon's vector area by
//! closure (`A_w n_w = −Σ_f A_f n_f`) telescopes exactly over a closed body.
use super::Grid;
use super::body::Body;
/// The cut data of one instant.
#[derive(Debug, Clone)]
pub struct CutGeometry {
pub grid: Grid,
/// φ at the corners, `(nx + 1) × (ny + 1) × (nz + 1)`.
pub phi: Vec<f64>,
/// Fluid area fraction of every u / v / w face (the staggered layouts).
pub a_u: Vec<f64>,
pub a_v: Vec<f64>,
pub a_w: Vec<f64>,
/// Fluid volume fraction of every cell.
pub vol: Vec<f64>,
/// The wall polygon's vector area per cell (outward from the fluid),
/// `−Σ_f A_f n_f · face area`, in area units.
pub wall: Vec<[f64; 3]>,
/// φ at the face centres (the wall distance of a near-wall face).
pub d_u: Vec<f64>,
pub d_v: Vec<f64>,
pub d_w: Vec<f64>,
}
impl CutGeometry {
#[inline]
fn node(g: Grid, k: usize, j: usize, i: usize) -> usize {
(k * (g.ny + 1) + j) * (g.nx + 1) + i
}
/// Build the cut data of `body` at time `t`.
pub fn build(body: &Body, grid: Grid, t: f64) -> Self {
let g = grid;
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
let mut phi = vec![0.0; (nx + 1) * (ny + 1) * (nz + 1)];
for k in 0..=nz {
for j in 0..=ny {
for i in 0..=nx {
phi[Self::node(g, k, j, i)] =
body.phi(i as f64 * dx, j as f64 * dy, k as f64 * dz, t);
}
}
}
let corner = |k: usize, j: usize, i: usize| phi[Self::node(g, k, j, i)];
// Face apertures: a face's two triangles along the diagonal from its
// (0, 0) to its (1, 1) corner in the face's own (a, b) order — the
// Kuhn split's diagonals: for an x-face (y, z), a y-face (x, z), a
// z-face (x, y); the same triangles seen from either cell.
let tri_area_fraction = |p0: f64, p1: f64, p2: f64| -> f64 {
let v = [p0, p1, p2];
let pos = v.iter().filter(|&&q| q >= 0.0).count();
match pos {
0 => 0.0,
3 => 1.0,
1 => {
let a = v.iter().position(|&q| q >= 0.0).unwrap();
let (b, c) = ((a + 1) % 3, (a + 2) % 3);
(v[a] / (v[a] - v[b])) * (v[a] / (v[a] - v[c]))
}
_ => {
let a = v.iter().position(|&q| q < 0.0).unwrap();
let (b, c) = ((a + 1) % 3, (a + 2) % 3);
1.0 - (v[a] / (v[a] - v[b])) * (v[a] / (v[a] - v[c]))
}
}
};
// Quad corners in (a, b) order: q00, q10, q01, q11; triangles
// (q00, q10, q11) and (q00, q11, q01).
let quad_fraction = |q00: f64, q10: f64, q01: f64, q11: f64| -> f64 {
0.5 * (tri_area_fraction(q00, q10, q11) + tri_area_fraction(q00, q11, q01))
};
let mut a_u = vec![0.0; (nx + 1) * ny * nz];
let mut a_v = vec![0.0; nx * (ny + 1) * nz];
let mut a_w = vec![0.0; nx * ny * (nz + 1)];
let mut d_u = vec![0.0; (nx + 1) * ny * nz];
let mut d_v = vec![0.0; nx * (ny + 1) * nz];
let mut d_w = vec![0.0; nx * ny * (nz + 1)];
for k in 0..nz {
for j in 0..ny {
for i in 0..=nx {
// x-face at i: corners (j, k), (j+1, k), (j, k+1), (j+1, k+1)
let (q00, q10, q01, q11) = (
corner(k, j, i),
corner(k, j + 1, i),
corner(k + 1, j, i),
corner(k + 1, j + 1, i),
);
a_u[g.uface(k, j, i)] = quad_fraction(q00, q10, q01, q11);
d_u[g.uface(k, j, i)] = 0.25 * (q00 + q10 + q01 + q11);
}
}
}
for k in 0..nz {
for j in 0..=ny {
for i in 0..nx {
// y-face at j: corners (i, k), (i+1, k), (i, k+1), (i+1, k+1)
let (q00, q10, q01, q11) = (
corner(k, j, i),
corner(k, j, i + 1),
corner(k + 1, j, i),
corner(k + 1, j, i + 1),
);
a_v[g.vface(k, j, i)] = quad_fraction(q00, q10, q01, q11);
d_v[g.vface(k, j, i)] = 0.25 * (q00 + q10 + q01 + q11);
}
}
}
for k in 0..=nz {
for j in 0..ny {
for i in 0..nx {
// z-face at k: corners (i, j), (i+1, j), (i, j+1), (i+1, j+1)
let (q00, q10, q01, q11) = (
corner(k, j, i),
corner(k, j, i + 1),
corner(k, j + 1, i),
corner(k, j + 1, i + 1),
);
a_w[g.wface(k, j, i)] = quad_fraction(q00, q10, q01, q11);
d_w[g.wface(k, j, i)] = 0.25 * (q00 + q10 + q01 + q11);
}
}
}
// Cell volumes by the Kuhn split: the six tetrahedra around the
// diagonal (0,0,0)(1,1,1) in unit-cube coordinates.
let mut vol = vec![0.0; g.cells()];
let mut wall = vec![[0.0; 3]; g.cells()];
const KUHN: [[[usize; 3]; 4]; 6] = [
[[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]],
[[0, 0, 0], [1, 0, 0], [1, 0, 1], [1, 1, 1]],
[[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 1, 1]],
[[0, 0, 0], [0, 1, 0], [0, 1, 1], [1, 1, 1]],
[[0, 0, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1]],
[[0, 0, 0], [0, 0, 1], [0, 1, 1], [1, 1, 1]],
];
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let mut fluid = 0.0;
for tet in &KUHN {
let pts: Vec<[f64; 3]> = tet
.iter()
.map(|c| [c[0] as f64, c[1] as f64, c[2] as f64])
.collect();
let vals: Vec<f64> = tet
.iter()
.map(|c| corner(k + c[2], j + c[1], i + c[0]))
.collect();
fluid += tet_fluid_volume(&pts, &vals);
}
// The six tets fill the unit cube (volume 1).
let idx = g.cell(k, j, i);
vol[idx] = fluid;
let ax = dy * dz;
let ay = dx * dz;
let az = dx * dy;
// Outward normals of the cell's faces times their fluid area,
// summed; the wall closes the fluid part of the cell.
let sx = (a_u[g.uface(k, j, i + 1)] - a_u[g.uface(k, j, i)]) * ax;
let sy = (a_v[g.vface(k, j + 1, i)] - a_v[g.vface(k, j, i)]) * ay;
let sz = (a_w[g.wface(k + 1, j, i)] - a_w[g.wface(k, j, i)]) * az;
wall[idx] = [-sx, -sy, -sz];
}
}
}
Self {
grid,
phi,
a_u,
a_v,
a_w,
vol,
wall,
d_u,
d_v,
d_w,
}
}
/// Total fluid volume.
#[must_use]
pub fn fluid_volume(&self) -> f64 {
let g = self.grid;
self.vol.iter().sum::<f64>() * g.dx * g.dy * g.dz
}
/// Σ over cells of |wall vector area| (the wall's area up to the
/// non-planarity of the per-cell polygon) and the closure vector Σ wall.
#[must_use]
pub fn wall_area_and_closure(&self) -> (f64, [f64; 3]) {
let mut area = 0.0;
let mut sum = [0.0; 3];
for w in &self.wall {
area += (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt();
sum[0] += w[0];
sum[1] += w[1];
sum[2] += w[2];
}
(area, sum)
}
}
fn det3(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
+ a[2] * (b[0] * c[1] - b[1] * c[0])
}
fn tet_volume(p: [[f64; 3]; 4]) -> f64 {
let e = |a: [f64; 3], b: [f64; 3]| [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
det3(e(p[0], p[1]), e(p[0], p[2]), e(p[0], p[3])).abs() / 6.0
}
fn lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
[
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
]
}
/// The volume of `{φ ≥ 0}` in a tetrahedron with the linear interpolant of
/// the corner values `v` (φ = 0 at a corner counts as fluid). Continuous in
/// `v`: every case's cut points move continuously and the cases agree on
/// their boundaries.
fn tet_fluid_volume(pts: &[[f64; 3]], v: &[f64]) -> f64 {
let total = tet_volume([pts[0], pts[1], pts[2], pts[3]]);
let pos: Vec<usize> = (0..4).filter(|&q| v[q] >= 0.0).collect();
let neg: Vec<usize> = (0..4).filter(|&q| v[q] < 0.0).collect();
let cut = |a: usize, b: usize| lerp(pts[a], pts[b], v[a] / (v[a] - v[b]));
match pos.len() {
0 => 0.0,
4 => total,
1 => {
let a = pos[0];
let (b, c, d) = (neg[0], neg[1], neg[2]);
tet_volume([pts[a], cut(a, b), cut(a, c), cut(a, d)])
}
3 => {
let a = neg[0];
let (b, c, d) = (pos[0], pos[1], pos[2]);
total - tet_volume([pts[a], cut(a, b), cut(a, c), cut(a, d)])
}
_ => {
// Two fluid corners A, B; the fluid wedge {A, B, P_AC, P_AD, P_BC, P_BD}
// as three tetrahedra.
let (a, b) = (pos[0], pos[1]);
let (c, d) = (neg[0], neg[1]);
let (pac, pad, pbc, pbd) = (cut(a, c), cut(a, d), cut(b, c), cut(b, d));
tet_volume([pts[a], pts[b], pbc, pbd])
+ tet_volume([pts[a], pac, pbc, pbd])
+ tet_volume([pts[a], pac, pad, pbd])
}
}
}