embedded3 S2-7b: the curved-wall instrument (tests/embedded3_wall_position_curved.rs: Taylor–Couette between embedded cylinders, rigid-rotation linear mode, outer-driven mode; walls' effective radii, pressure error by cell class against the exact p(r), wall-flux and ghost diagnostics); the sphere operator probe's sub-bands and force units; two host prototypes, default off, device refuses them: RTX_E3_WALL_EXCHANGE=axisfoot (the axis exchange takes the wall velocity at the axis foot — the S2-7 default read the solid face's own value, exact only for a uniform wall velocity: rigid rotation A 1.012 → 1.003) and RTX_E3_CONV_SIDES=exact (convective mass fluxes from the sides' own apertures; moves nothing)
CI / Build CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 4s
CI / Format Check (push) Failing after 10s
CI / Build (ubuntu-latest) (push) Failing after 1m25s
CI / Clippy Check (push) Failing after 1m47s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m19s
CI / CI Success (push) Canceled after 0s
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

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-20 07:54:01 -05:00
co-authored by Claude Fable 5.1
parent c430510802
commit d25a36d908
6 changed files with 417 additions and 6 deletions
@@ -221,6 +221,8 @@ impl Mask {
wall_exchange_axis: false,
cv_sides_exact: false,
wall_order2_centroid: false,
wall_exchange_foot: false,
conv_sides_exact: false,
grad_weights: None,
diffusion_centroid: false,
face_shifts: None,
@@ -435,6 +437,29 @@ impl Mask {
/// crosses a side (the in-plane momentum residual on oblique walls).
/// `None` at a domain side (the default stays).
fn exact_cv_sides(&self, cut: &CutGeometry, c: usize, p: [i64; 3]) -> Option<[[f64; 2]; 3]> {
let parts = self.exact_cv_side_parts(cut, c, p)?;
let mut ap = [[1.0; 2]; 3];
for d in 0..3 {
for side in 0..2 {
ap[d][side] = if d == c {
parts[d][side].0
} else {
0.5 * (parts[d][side].0 + parts[d][side].1)
};
}
}
Some(ap)
}
/// The parts of [`Self::exact_cv_sides`]: across `c`, the (far half of
/// `cell_minus`'s face, near half of `cell_plus`'s face) apertures per
/// side; along `c`, the centre plane's aperture (twice).
pub(super) fn exact_cv_side_parts(
&self,
cut: &CutGeometry,
c: usize,
p: [i64; 3],
) -> Option<[[(f64, f64); 2]; 3]> {
let lat = self.lattice();
let g = self.grid;
let mut pm = p;
@@ -456,7 +481,7 @@ impl Mask {
corner(cell, o)
}
};
let mut ap = [[1.0; 2]; 3];
let mut ap = [[(1.0, 1.0); 2]; 3];
for d in 0..3 {
if d == c {
// The two cells' centre planes across `c`: corners at the
@@ -469,7 +494,8 @@ impl Mask {
o[d2] = o2;
value(*cell, o, true)
};
ap[d][side] = super::cut::quad_fraction(mid(0, 0), mid(1, 0), mid(0, 1), mid(1, 1));
let a = super::cut::quad_fraction(mid(0, 0), mid(1, 0), mid(0, 1), mid(1, 1));
ap[d][side] = (a, a);
}
} else {
let e = 3 - c - d;
@@ -496,7 +522,7 @@ impl Mask {
super::cut::quad_fraction(at(0, 0), at(1, 0), at(0, 1), at(1, 1))
}
};
ap[d][side] = 0.5 * (half(cells[0], true) + half(cells[1], false));
ap[d][side] = (half(cells[0], true), half(cells[1], false));
}
}
}
@@ -109,6 +109,12 @@ impl Solver {
let val = |cc: usize, q: [i64; 3]| lat.face(cc, q).map(|f| old[cc][f]);
let ap = |cc: usize, q: [i64; 3]| mask.aperture(cc, q);
let cv = mask.cv_geometry(c, p);
// S2-7b: the convective sides' own apertures (host prototype).
let conv_parts = if mask.conv_sides_exact {
mask.cut().and_then(|cut| mask.exact_cv_side_parts(cut, c, p))
} else {
None
};
let x = lat.face_position(c, p);
let u0 = val(c, p).expect("the face");
let ec = e(c);
@@ -133,7 +139,22 @@ impl Solver {
// The control volume's mass fluxes through its plus / minus faces
// along d: the averages of the two adjacent cells' face fluxes
// (own direction: the face's and its neighbours' fluxes).
let (m_plus, m_minus) = if d == c {
let (m_plus, m_minus) = if let Some(parts) = conv_parts {
if d == c {
// The centre planes' apertures times the mean velocity.
(
parts[d][1].0 * 0.5 * (u0 + up1.unwrap_or(u0)) * a_d,
parts[d][0].0 * 0.5 * (dn1.unwrap_or(u0) + u0) * a_d,
)
} else {
// The two half faces' own apertures times their faces' values.
let v = |q: [i64; 3]| val(d, q).unwrap_or(0.0);
(
0.5 * (parts[d][1].0 * v(add(cell_minus, ed, 1)) + parts[d][1].1 * v(add(cell_plus, ed, 1))) * a_d,
0.5 * (parts[d][0].0 * v(cell_minus) + parts[d][0].1 * v(cell_plus)) * a_d,
)
}
} else if d == c {
let f_up = ap(c, add(p, ec, 1)).unwrap_or(cv.alpha) * up1.unwrap_or(u0);
let f_dn = ap(c, add(p, ec, -1)).unwrap_or(cv.alpha) * dn1.unwrap_or(u0);
let f0 = cv.alpha * u0;
@@ -278,11 +299,22 @@ impl Solver {
diff -= sign * mu * gap * a_d * transverse(q, uq, sign) / spacing(q, sign);
}
}
// S2-7b: the wall velocity at the axis foot (distance δ along d)
// instead of the solid face's own value at distance h.
let foot_value = |un: f64, sign: f64| -> f64 {
if !mask.wall_exchange_foot {
return un;
}
let delta = mask.exchange_delta(&cv, d);
let mut xf = x;
xf[d] += sign * delta;
mask.surface_velocity_at(body, xf, c, t_old)
};
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;
wall_rhs += k * foot_value(un, 1.0);
0.0
}
Some(un) if centroid => {
@@ -310,7 +342,7 @@ impl Solver {
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;
wall_rhs += k * foot_value(ud, -1.0);
0.0
}
Some(ud) if centroid => {
@@ -177,6 +177,10 @@ impl DeviceStep {
!solver.params.wall_order2_centroid,
"the order-2 second point at the neighbour's centroid (S2-7) is a host prototype: the device kernels do not carry it"
);
assert!(
!solver.params.wall_exchange_foot && !solver.params.conv_sides_exact,
"the axis-foot exchange / exact convective sides (S2-7b) are host prototypes: the device kernels do not carry them"
);
assert!(
!solver.params.cv_sides_exact,
"the exact control-volume sides (S2-7) are a host prototype: the device kernels do not carry it"
@@ -163,6 +163,17 @@ pub struct Parameters {
/// two differ by ½h(1 α)n_t, O(1) of the step on cut faces). The
/// device refuses it. `RTX_E3_WALL_ORDER2=centroid`.
pub wall_order2_centroid: bool,
/// HOST PROTOTYPE (S2-7b): the axis exchange with a solid neighbour face
/// takes the wall velocity at the AXIS FOOT (distance δ) instead of the
/// solid face's own imposed value (at distance h): consistent for a
/// linear field on a moving or rotating wall (identical on a wall at
/// rest). `RTX_E3_WALL_EXCHANGE=axisfoot`; the device refuses it.
pub wall_exchange_foot: bool,
/// HOST PROTOTYPE (S2-7b): the convective mass fluxes through a cut
/// face's control-volume sides from the sides' own apertures (the half
/// faces across, the cell-centre planes along) instead of the averages
/// of whole-face fluxes. `RTX_E3_CONV_SIDES=exact`; the device refuses it.
pub conv_sides_exact: 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
@@ -205,6 +216,8 @@ impl Default for Parameters {
.is_ok_and(|v| v == "off"),
// ON by default since S2-7 (`=h` reproduces the records before it).
wall_exchange_axis: std::env::var("RTX_E3_WALL_EXCHANGE").map_or(true, |v| v != "h"),
wall_exchange_foot: std::env::var("RTX_E3_WALL_EXCHANGE").is_ok_and(|v| v == "axisfoot"),
conv_sides_exact: std::env::var("RTX_E3_CONV_SIDES").is_ok_and(|v| v == "exact"),
pressure_centroid: std::env::var("RTX_E3_PRESSURE_CENTROID").is_ok_and(|v| v == "1"),
momentum_volume_tiled: std::env::var("RTX_E3_MOMENTUM_VOLUME")
.is_ok_and(|v| v == "tiled"),
@@ -351,6 +364,8 @@ impl Solver {
m.exchange_convection_off = self.params.exchange_convection_off;
m.cv_sides_exact = self.params.cv_sides_exact;
m.wall_order2_centroid = self.params.wall_order2_centroid;
m.wall_exchange_foot = self.params.wall_exchange_foot;
m.conv_sides_exact = self.params.conv_sides_exact;
m.diffusion_centroid = self.params.diffusion_centroid;
if self.params.diffusion_centroid {
m.compute_face_shifts();
@@ -114,6 +114,10 @@ pub struct Mask {
/// S2-7: the quadratic wall gradient's second point at the neighbour's
/// own centroid distance (host prototype).
pub(super) wall_order2_centroid: bool,
/// S2-7b host prototypes: the axis foot's wall velocity in the solid
/// exchange; the convective sides from the sides' own apertures.
pub(super) wall_exchange_foot: bool,
pub(super) conv_sides_exact: bool,
/// The centroid prototype's pressure-gradient weights per u / v / w face.
/// The centroid-distance cross diffusion (S2-5).
pub(super) diffusion_centroid: bool,
@@ -542,6 +546,8 @@ impl Mask {
wall_exchange_axis: false,
cv_sides_exact: false,
wall_order2_centroid: false,
wall_exchange_foot: false,
conv_sides_exact: false,
grad_weights: None,
diffusion_centroid: false,
face_shifts: None,