embedded3 S2-5: the cut wall sat ½(1−α)h inside the body — cross diffusion over the open-part centroid spacing (RTX_E3_DIFFUSION_CENTROID; host + e3_cut.cu, shift tables, point-implicit excess); flat-wall effective-position instrument; DFG 2D-1 ladder tests (device + host); knobs tried and refuted along the way (oblique distance, axis exchange, centroid pressure gradient)
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 / Build (macos-latest) (push) Waiting to run
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 4s
CI / Clippy Check (push) Failing after 2m24s
CI / Build CPU-Only (Explicit) (push) Failing after 3s
CI / Format Check (push) Failing after 11s
CI / Build (ubuntu-latest) (push) Failing after 1m58s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m44s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-18 10:54:52 -05:00
co-authored by Claude Fable 5.1
parent 4c3e58fa27
commit fdfb6da769
12 changed files with 667 additions and 22 deletions
@@ -117,6 +117,9 @@ impl Solver {
let mut mass_out = 0.0;
let mut conv = 0.0;
let mut diff = 0.0;
let shift0 = mask.face_shift(c, p);
// The implicit exchange with solid neighbour faces (S2-5).
let (mut wall_implicit, mut wall_rhs) = (0.0, 0.0);
for d in 0..3 {
let ed = e(d);
let a_d = area[d];
@@ -187,7 +190,32 @@ impl Solver {
conv += m_plus * (u_plus + delta_plus) - m_minus * (u_minus + delta_minus);
// Diffusion through the plus / minus faces.
let (g_minus, g_plus) = (cv.ap[d][0], cv.ap[d][1]);
// The cross-direction spacing between open-part centroids (S2-5).
let centroid = d != c && mask.diffusion_centroid;
let spacing = |q: [i64; 3], sign: f64| -> f64 {
let delta = h[d] + sign * (mask.face_shift(c, q)[d] - shift0[d]);
delta.clamp(0.25 * h[d], 2.0 * h[d])
};
let solid = |q: [i64; 3]| mask.wall_exchange_axis && ap(c, q) == Some(0.0);
diff += match up1 {
Some(un) if solid(add(p, ed, 1)) => {
let k = mu * g_plus * a_d / mask.exchange_delta(&cv, d);
wall_implicit += k;
wall_rhs += k * un;
0.0
}
Some(un) if centroid => {
// The part of the centroid coupling beyond `1/h` is taken
// point-implicitly (the explicit limit is `h`'s).
let k = mu * g_plus * a_d * (1.0 / spacing(add(p, ed, 1), 1.0) - 1.0 / h[d]);
if k > 0.0 {
wall_implicit += k;
wall_rhs += k * un;
mu * g_plus * a_d * (un - u0) / h[d]
} else {
(mu * g_plus * a_d / h[d] + k) * (un - u0)
}
}
Some(un) => mu * g_plus * a_d * (un - u0) / h[d],
None => {
if sides[d][1] == Side::Velocity {
@@ -198,6 +226,22 @@ impl Solver {
}
};
diff -= match dn1 {
Some(ud) if solid(add(p, ed, -1)) => {
let k = mu * g_minus * a_d / mask.exchange_delta(&cv, d);
wall_implicit += k;
wall_rhs += k * ud;
0.0
}
Some(ud) if centroid => {
let k = mu * g_minus * a_d * (1.0 / spacing(add(p, ed, -1), -1.0) - 1.0 / h[d]);
if k > 0.0 {
wall_implicit += k;
wall_rhs += k * ud;
mu * g_minus * a_d * (u0 - ud) / h[d]
} else {
(mu * g_minus * a_d / h[d] + k) * (u0 - ud)
}
}
Some(ud) => mu * g_minus * a_d * (u0 - ud) / h[d],
None => {
if sides[d][0] == Side::Velocity {
@@ -218,7 +262,8 @@ impl Solver {
let conv = rho * (conv - mass_out * u0);
let p_plus = lat.cell(cell_plus).map_or(0.0, |ci| field.p[ci]);
let p_minus = lat.cell(cell_minus).map_or(0.0, |ci| field.p[ci]);
let pressure = -(p_plus - p_minus) * cv.alpha * area[c];
let idx_f = lat.face(c, p).expect("the face");
let pressure = -(p_plus - p_minus) * cv.alpha * area[c] * mask.grad_weight(c, idx_f);
let v_u = cv.alpha * h[c] * area[c];
let source = self.momentum_source.as_ref().map_or(0.0, |f| {
let s = f(x[0], x[1], x[2], t_old);
@@ -238,8 +283,9 @@ impl Solver {
};
let v_eff = fraction.max(INERTIA_FLOOR) * h[c] * area[c];
let inertia = rho * v_eff / dt;
let u_star = (inertia * u0 - conv + diff + pressure + source + shear * ub - shear_explicit)
/ (inertia + shear);
let u_star = (inertia * u0 - conv + diff + pressure + source + shear * ub - shear_explicit
+ wall_rhs)
/ (inertia + shear + wall_implicit);
let v_alpha = fraction * h[c] * area[c];
(u_star, rho * (v_eff - v_alpha) * (u_star - u0) / dt)
}
@@ -163,6 +163,10 @@ impl DeviceStep {
/// Allocates the device fields for `grid`; the momentum source is
/// tabulated at `t = 0` (steady sources only in Stage 1).
pub fn new(solver: Solver, grid: Grid) -> Self {
assert!(
!solver.params.pressure_centroid,
"the centroid pressure gradient (S2-5) is a host prototype: the device kernels do not carry it"
);
let rt = runtime();
let nu = (grid.nx + 1) * grid.ny * grid.nz;
let nv = grid.nx * (grid.ny + 1) * grid.nz;
@@ -284,7 +288,11 @@ impl DeviceStep {
bz0: side_code(b.z0),
bz1: side_code(b.z1),
scheme: scheme_code(self.solver.params.convection_scheme),
wall_order: i32::from(self.solver.params.wall_order),
// Low 4 bits: the shear closure's order; bit 4: the oblique wall distance.
wall_order: i32::from(self.solver.params.wall_order)
+ 16 * i32::from(self.solver.params.wall_distance_oblique)
+ 32 * i32::from(self.solver.params.wall_exchange_axis)
+ 64 * i32::from(self.solver.params.diffusion_centroid),
dx: g.dx,
dy: g.dy,
dz: g.dz,
@@ -51,11 +51,11 @@ fn cut_kernels() -> &'static CutKernels {
})
}
/// `struct E3Cut` in e3_cut.cu: 18 device pointers.
/// `struct E3Cut` in e3_cut.cu: 21 device pointers.
#[repr(C)]
#[derive(Clone, Copy)]
struct E3CutPtrs {
ptrs: [u64; 18],
ptrs: [u64; 21],
}
unsafe impl DeviceRepr for E3CutPtrs {}
unsafe impl ValidAsZeroBits for E3CutPtrs {}
@@ -74,6 +74,8 @@ pub(super) struct DeviceCut {
fold_ptr: CudaSlice<u32>,
fold_idx: CudaSlice<u32>,
cell_flux: CudaSlice<f64>,
/// The open-part centroid shifts per face (S2-5; one dummy entry when off).
shift: [CudaSlice<f64>; 3],
pub(super) merged: usize,
}
@@ -228,6 +230,10 @@ impl DeviceCut {
fold_ptr: up_u(&fold_ptr),
fold_idx: up_u(&fold_idx),
cell_flux: rt.stream.alloc_zeros::<f64>(nc).expect("alloc"),
shift: match mask.face_shift_tables() {
Some(t) => [up_f(&t[0]), up_f(&t[1]), up_f(&t[2])],
None => [up_f(&[0.0]), up_f(&[0.0]), up_f(&[0.0])],
},
merged,
})
}
@@ -258,6 +264,9 @@ impl DeviceCut {
pu(&self.fold_ptr),
pu(&self.fold_idx),
pf(&self.cell_flux),
pf(&self.shift[0]),
pf(&self.shift[1]),
pf(&self.shift[2]),
],
}
}
@@ -96,6 +96,33 @@ pub struct Parameters {
/// next open face along the wall normal's dominant axis (S2-4).
/// `Parameters::default()` reads `RTX_E3_WALL_ORDER` (default 1).
pub wall_order: u8,
/// The cut face's wall distance with the wall's obliquity: `φ + ½h(1
/// α)|n_t|` (`n_t` the wall normal's part in the face plane) instead
/// of `φ + ½h(1 α)`, which over-reads the distance on oblique walls
/// by `½h(1 α)(1 |n_t|)` at every h (S2-5). `Parameters::default()`
/// reads `RTX_E3_WALL_DISTANCE=oblique` (default: the recorded form).
pub wall_distance_oblique: bool,
/// The diffusive exchange of a fluid face with a SOLID neighbour face
/// over the axis distance to the wall, `δ = min(h, d_f/|n_d|)`, and
/// implicit — instead of the full `h`, which places the no-slip value
/// deeper in the body than the wall (an effective radius ≈ 0.28 h short
/// on the DFG 2D-1 ladder; S2-5). `RTX_E3_WALL_EXCHANGE=axis`.
pub wall_exchange_axis: bool,
/// HOST PROTOTYPE (S2-5): the pressure gradient across cut cells over
/// the distance between the cells' FLUID CENTROIDS (estimated from the
/// volume fraction and the wall normal) instead of `h` — a symmetric
/// per-face weight `ω = h/δ` in the Poisson coefficient, the velocity
/// correction and the predictor's pressure force. The device path
/// refuses it. `RTX_E3_PRESSURE_CENTROID=1`.
pub pressure_centroid: bool,
/// S2-5: the cross-direction diffusion between two faces over the
/// distance between their OPEN-PART CENTROIDS (a cut face's velocity
/// is its open part's mean, ½h(1 α) off the face centre along the
/// wall's in-plane normal) instead of `h`. With `h` the coupling of a
/// cut face to its neighbour is weak by `(1 + α)/2` and the no-slip
/// surface sits `½(1 α) h` inside the body (the flat-wall instrument).
/// `RTX_E3_DIFFUSION_CENTROID=1`.
pub diffusion_centroid: bool,
}
impl Default for Parameters {
@@ -116,6 +143,11 @@ impl Default for Parameters {
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1),
wall_distance_oblique: std::env::var("RTX_E3_WALL_DISTANCE")
.is_ok_and(|v| v == "oblique"),
wall_exchange_axis: std::env::var("RTX_E3_WALL_EXCHANGE").is_ok_and(|v| v == "axis"),
pressure_centroid: std::env::var("RTX_E3_PRESSURE_CENTROID").is_ok_and(|v| v == "1"),
diffusion_centroid: std::env::var("RTX_E3_DIFFUSION_CENTROID").is_ok_and(|v| v == "1"),
}
}
}
@@ -241,6 +273,15 @@ impl Solver {
m.scheme = self.params.convection_scheme;
m.density = self.fluid.density;
m.wall_order = self.params.wall_order;
m.wall_distance_oblique = self.params.wall_distance_oblique;
m.wall_exchange_axis = self.params.wall_exchange_axis;
m.diffusion_centroid = self.params.diffusion_centroid;
if self.params.diffusion_centroid {
m.compute_face_shifts();
}
if self.params.pressure_centroid {
m.compute_gradient_weights();
}
m
})
.expect("embedded mask")
@@ -360,6 +401,24 @@ impl Solver {
// The projection's apertures: step-averaged on a moving cut wall
// (1 without a cut geometry).
#[inline]
/// The pressure-gradient weight `ω = h/δ` of a u / v / w face (1
/// without the centroid prototype).
pub(super) fn gu(&self, k: usize, j: usize, i: usize) -> f64 {
self.mask
.as_ref()
.map_or(1.0, |m| m.grad_weight(0, m.grid().uface(k, j, i)))
}
pub(super) fn gv(&self, k: usize, j: usize, i: usize) -> f64 {
self.mask
.as_ref()
.map_or(1.0, |m| m.grad_weight(1, m.grid().vface(k, j, i)))
}
pub(super) fn gw(&self, k: usize, j: usize, i: usize) -> f64 {
self.mask
.as_ref()
.map_or(1.0, |m| m.grad_weight(2, m.grid().wface(k, j, i)))
}
pub(super) fn au(&self, k: usize, j: usize, i: usize) -> f64 {
self.mask
.as_ref()
@@ -39,28 +39,28 @@ impl Solver {
extra += ae_outlet;
}
} else if self.u_is_unknown(k, j, i + 1) {
problem.ae[idx] = ae_interior * self.au(k, j, i + 1);
problem.ae[idx] = ae_interior * self.au(k, j, i + 1) * self.gu(k, j, i + 1);
}
if i == 0 {
if b.x0 == outlet {
extra += ae_outlet;
}
} else if self.u_is_unknown(k, j, i) {
problem.aw[idx] = ae_interior * self.au(k, j, i);
problem.aw[idx] = ae_interior * self.au(k, j, i) * self.gu(k, j, i);
}
if j + 1 == ny {
if b.y1 == outlet {
extra += an_outlet;
}
} else if self.v_is_unknown(k, j + 1, i) {
problem.an[idx] = an_interior * self.av(k, j + 1, i);
problem.an[idx] = an_interior * self.av(k, j + 1, i) * self.gv(k, j + 1, i);
}
if j == 0 {
if b.y0 == outlet {
extra += an_outlet;
}
} else if self.v_is_unknown(k, j, i) {
problem.as_[idx] = an_interior * self.av(k, j, i);
problem.as_[idx] = an_interior * self.av(k, j, i) * self.gv(k, j, i);
}
if k + 1 == nz && !periodic {
if b.z1 == outlet {
@@ -434,7 +434,8 @@ impl Solver {
for j in 0..ny {
for i in 1..nx {
if self.u_is_unknown(k, j, i) {
let dp_dx = (pp[g.cell(k, j, i)] - pp[g.cell(k, j, i - 1)]) / dx;
let dp_dx =
self.gu(k, j, i) * (pp[g.cell(k, j, i)] - pp[g.cell(k, j, i - 1)]) / dx;
let f = g.uface(k, j, i);
field.u[f] = field.u_star[f] - (dt / rho) * dp_dx;
}
@@ -453,7 +454,8 @@ impl Solver {
for i in 0..nx {
for j in 1..ny {
if self.v_is_unknown(k, j, i) {
let dp_dy = (pp[g.cell(k, j, i)] - pp[g.cell(k, j - 1, i)]) / dy;
let dp_dy =
self.gv(k, j, i) * (pp[g.cell(k, j, i)] - pp[g.cell(k, j - 1, i)]) / dy;
let f = g.vface(k, j, i);
field.v[f] = field.v_star[f] - (dt / rho) * dp_dy;
}