embedded3 S2-2b (host lever): narrow-band φ re-evaluation for moving bodies (max_surface_speed) — bit-identical, the flag body's ny 62 rebuild 3.54 → 0.30 s; the flag driver uses it
CI / Format Check (push) Failing after 4s
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
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
CI / Build CPU-Only (Explicit) (push) Failing after 1m5s
Documentation / Build API Documentation (push) Failing after 1m8s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-17 19:30:54 -05:00
co-authored by Claude Fable 5.1
parent aa096465a6
commit c30e3dba34
5 changed files with 192 additions and 9 deletions
@@ -28,6 +28,11 @@ pub struct CutGeometry {
pub d_u: Vec<f64>,
pub d_v: Vec<f64>,
pub d_w: Vec<f64>,
/// A lower bound on |φ| per corner (the narrow band of a moving body:
/// a corner is re-evaluated only when its bound, decayed by the body's
/// motion, comes within the band; far corners keep a stale value with
/// the right sign, which is all their cells use).
pub bound: Vec<f64>,
}
impl CutGeometry {
@@ -36,16 +41,41 @@ impl CutGeometry {
(k * (g.ny + 1) + j) * (g.nx + 1) + i
}
/// Build the cut data of `body` at time `t`.
/// Build the cut data of `body` at time `t` (every corner evaluated).
pub fn build(body: &Body, grid: Grid, t: f64) -> Self {
Self::build_from(body, grid, t, None)
}
/// Build the cut data of `body` at `t`, re-evaluating only the corners
/// of `prev` whose |φ| bound, decayed by `motion` (the body's largest
/// displacement since `prev`), falls within `band` of the surface.
/// Identical to [`Self::build`] in every cut cell.
pub fn build_from(
body: &Body,
grid: Grid,
t: f64,
prev: Option<(&CutGeometry, f64, 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)];
let n_nodes = (nx + 1) * (ny + 1) * (nz + 1);
let mut phi = vec![0.0; n_nodes];
let mut bound = vec![0.0; n_nodes];
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 n = Self::node(g, k, j, i);
if let Some((p, band, motion)) = prev {
let b = p.bound[n] - motion;
if b > band {
phi[n] = p.phi[n];
bound[n] = b;
continue;
}
}
let v = body.phi(i as f64 * dx, j as f64 * dy, k as f64 * dz, t);
phi[n] = v;
bound[n] = v.abs();
}
}
}
@@ -181,6 +211,7 @@ impl CutGeometry {
d_u,
d_v,
d_w,
bound,
}
}
@@ -108,7 +108,20 @@ pub(super) struct CvGeometry {
impl Mask {
/// Classify the grid against `body` at `t` by its cut geometry.
pub fn build_cut(body: &Body, g: Grid, t: f64, b: Boundaries) -> Result<Self, String> {
let cut = CutGeometry::build(body, g, t);
Self::build_cut_from(body, g, t, b, None)
}
/// As [`Self::build_cut`], re-evaluating φ only within `band` of the
/// previous geometry moved by at most `motion` (see
/// [`CutGeometry::build_from`]).
pub fn build_cut_from(
body: &Body,
g: Grid,
t: f64,
b: Boundaries,
prev: Option<(&CutGeometry, f64, f64)>,
) -> Result<Self, String> {
let cut = CutGeometry::build_from(body, g, t, prev);
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
let periodic = b.z0 == Side::Periodic;
let allowed = |side: Side| matches!(side, Side::Velocity | Side::Periodic | Side::SlipWall);
@@ -74,6 +74,11 @@ pub struct Parameters {
pub inner_stop_factor: f64,
/// The wall treatment of an embedded body.
pub wall_scheme: WallScheme,
/// A moving body's largest surface speed: with it the cut geometry's
/// φ is re-evaluated only within three cells of the surface each step
/// (identical results; the far corners keep their sign). `None` =
/// every corner every step.
pub max_surface_speed: Option<f64>,
}
impl Default for Parameters {
@@ -87,6 +92,7 @@ impl Default for Parameters {
convection_scheme: ConvectionScheme::Upwind,
inner_stop_factor: 1e-2,
wall_scheme: WallScheme::GhostBinary,
max_surface_speed: None,
}
}
}
@@ -182,10 +188,17 @@ impl Solver {
self.mask = None;
}
fn build_mask(&self, body: &Body, g: Grid, t: f64) -> Mask {
fn build_mask(&self, body: &Body, g: Grid, t: f64, dt: f64) -> Mask {
match self.params.wall_scheme {
WallScheme::GhostBinary => Mask::build(body, g, t, self.params.boundaries),
WallScheme::CutCell => Mask::build_cut(body, g, t, self.params.boundaries),
WallScheme::CutCell => {
let h = g.dx.min(g.dy).min(g.dz);
let prev = match (self.params.max_surface_speed, &self.mask) {
(Some(speed), Some(m)) => m.cut().map(|c| (c, 3.0 * h, speed * dt)),
_ => None,
};
Mask::build_cut_from(body, g, t, self.params.boundaries, prev)
}
}
.expect("embedded mask")
}
@@ -453,7 +466,7 @@ impl Solver {
let t = self.time;
if let Some(body) = &self.body {
if self.mask.is_none() {
self.mask = Some(self.build_mask(body, field.grid, t));
self.mask = Some(self.build_mask(body, field.grid, t, 0.0));
}
}
self.apply_boundary_normals(field, t);
@@ -486,7 +499,7 @@ impl Solver {
return 0;
};
let mut fresh_cells = 0;
let mut new_mask = self.build_mask(body, field.grid, t_new);
let mut new_mask = self.build_mask(body, field.grid, t_new, dt);
if let Some(old_mask) = &self.mask {
fresh_cells = refill_fresh_cells(old_mask, &new_mask, field);
new_mask.set_step_apertures(old_mask);