embedded3: moving-body box route (Δ(Vu) unsteady term, Solver::previous_volumes), floor-source / closure-lag / applied-force instruments, moving.rs split; falsifier prints the route residual under each
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 / 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 / Build (macos-latest) (push) Waiting to run
CI / Format Check (push) Failing after 4s
CI / Clippy Check (push) Failing after 4s
CI / Build (ubuntu-latest) (push) Failing after 3s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 7s
CI / Build CPU-Only (Explicit) (push) Failing after 29s
Documentation / Build API Documentation (push) Failing after 31s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-18 06:27:04 -05:00
co-authored by Claude Fable 5.1
parent 62b46194dd
commit 53b1babb91
6 changed files with 354 additions and 134 deletions
@@ -25,6 +25,7 @@ impl Solver {
let periodic = self.params.boundaries.periodic_z();
let lat = mask.lattice();
let w_range = if periodic { 0..nz } else { 1..nz };
let mut floor = [0.0; 3];
for c in 0..3 {
let (ir, jr, kr) = match c {
0 => (1..nx, 0..ny, 0..nz),
@@ -45,7 +46,9 @@ impl Solver {
}
let p = [i as i64, j as i64, k as i64];
let idx = lat.face(c, p).expect("face");
updates.push((idx, self.cut_face_update(field, c, p, dt, t_old)));
let (val, src) = self.cut_face_update(field, c, p, dt, t_old);
floor[c] += src;
updates.push((idx, val));
}
}
}
@@ -58,6 +61,7 @@ impl Solver {
out[idx] = val;
}
}
self.floor_source.set(floor);
if periodic {
for j in 0..ny {
for i in 0..nx {
@@ -68,9 +72,17 @@ impl Solver {
}
/// The predicted value of the unknown face of component `c` at lattice
/// `p` from the old field.
/// `p` from the old field, with the momentum the inertia floor added
/// on this face, `ρ (V_eff V_α)(u* uⁿ)/dt`.
#[allow(clippy::too_many_lines)]
fn cut_face_update(&self, field: &Field, c: usize, p: [i64; 3], dt: f64, t_old: f64) -> f64 {
fn cut_face_update(
&self,
field: &Field,
c: usize,
p: [i64; 3],
dt: f64,
t_old: f64,
) -> (f64, f64) {
let mask = self.mask.as_ref().expect("cut mask");
let body = self.body.as_ref().expect("body");
let g = field.grid;
@@ -223,6 +235,9 @@ impl Solver {
};
let v_eff = fraction.max(INERTIA_FLOOR) * h[c] * area[c];
let inertia = rho * v_eff / dt;
(inertia * u0 - conv + diff + pressure + source + shear * ub) / (inertia + shear)
let u_star =
(inertia * u0 - conv + diff + pressure + source + shear * ub) / (inertia + shear);
let v_alpha = fraction * h[c] * area[c];
(u_star, rho * (v_eff - v_alpha) * (u_star - u0) / dt)
}
}
@@ -7,6 +7,7 @@
mod cut_predictor;
#[cfg(feature = "cuda")]
pub mod device;
mod moving;
mod predictor;
mod projection;
@@ -128,6 +129,17 @@ pub struct Solver {
pub fluid: Fluid,
pub params: Parameters,
pub(super) momentum_source: Option<Vec3Fn>,
/// The momentum the small-cell inertia floor added over the last
/// step, `Σ ρ (V_eff V_α)(u* uⁿ)/dt` over the unknown faces (host
/// cut predictor only): the box route counts it, the operator route
/// does not.
pub(super) floor_source: std::cell::Cell<[f64; 3]>,
/// The closure lag of the pressure corrections over the last step
/// (`Mask::closure_lag` summed over the correctors; host path).
pub(super) pressure_lag: std::cell::Cell<[f64; 3]>,
/// The previous mask's cell volumes (moving bodies; the box route's
/// unsteady term).
pub(super) vol_old: Vec<f64>,
boundary_velocity: Option<Vec3Fn>,
body: Option<Body>,
/// The body moves: the mask is rebuilt at every step's new time.
@@ -158,6 +170,9 @@ impl Solver {
fluid,
params,
momentum_source: None,
floor_source: std::cell::Cell::new([0.0; 3]),
pressure_lag: std::cell::Cell::new([0.0; 3]),
vol_old: Vec::new(),
boundary_velocity: None,
body: None,
moving: false,
@@ -221,6 +236,27 @@ impl Solver {
.expect("embedded mask")
}
/// The small-cell floor's momentum source over the last step (a force
/// on the fluid; host cut predictor only, zero otherwise).
#[must_use]
pub fn floor_source(&self) -> [f64; 3] {
self.floor_source.get()
}
/// The pressure corrections' closure lag over the last step (a force
/// on the body the operator route lacks on a moving body; host path).
#[must_use]
pub fn pressure_lag(&self) -> [f64; 3] {
self.pressure_lag.get()
}
/// The previous step's cell volumes (fraction of the cell; 0 for a
/// solid cell), once a moving body's mask has been rebuilt.
#[must_use]
pub fn previous_volumes(&self) -> Option<&[f64]> {
(!self.vol_old.is_empty()).then_some(self.vol_old.as_slice())
}
#[must_use]
pub fn body(&self) -> Option<&Body> {
self.body.as_ref()
@@ -506,69 +542,6 @@ impl Solver {
&self.wall_fluxes
}
/// The moving body's mask at the end-of-step geometry `t_new`: the
/// pressure of the cells that just became fluid refilled from their
/// neighbours (fluid in both masks), the new mask's prescribed and
/// ghost values imposed from the previous corrected field, the
/// step-averaged apertures and the GCL wall-flux table (cut wall).
/// Returns the fresh-cell count. `field` holds the predicted field.
pub fn rebuild_moving_mask(&mut self, field: &mut Field, dt: f64, t_new: f64) -> usize {
let Some(body) = &self.body else {
return 0;
};
let mut fresh_cells = 0;
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);
let n_in = self.params.aperture_substeps;
if n_in == 0 {
new_mask.set_step_apertures(old_mask);
} else {
// The intermediate geometries, each within the band of the
// previous one.
let g = field.grid;
let h = g.dx.min(g.dy).min(g.dz);
let t_old = t_new - dt;
let mut inner: Vec<CutGeometry> = Vec::with_capacity(n_in);
for m in 1..=n_in {
let tm = t_old + dt * m as f64 / (n_in + 1) as f64;
let prev: Option<(&CutGeometry, f64, f64)> = match (
self.params.max_surface_speed,
inner.last().or(old_mask.cut()),
) {
(Some(speed), Some(c)) => {
Some((c, 3.0 * h, speed * dt / (n_in + 1) as f64))
}
_ => None,
};
inner.push(CutGeometry::build_from(body, g, tm, prev));
}
let refs: Vec<&CutGeometry> = inner.iter().collect();
new_mask.set_step_apertures_with(old_mask, &refs);
}
}
new_mask.impose_from(
body,
&field.u_old,
&field.v_old,
&field.w_old,
&mut field.u,
&mut field.v,
&mut field.w,
t_new,
);
if new_mask.cut().is_some() {
let (table, correction) = match &self.mask {
Some(old_mask) => new_mask.gcl_flux_table(old_mask, dt),
None => new_mask.wall_flux_table(body, t_new),
};
self.wall_fluxes = table;
self.last_ghost_correction = correction;
}
self.mask = Some(new_mask);
fresh_cells
}
/// One step of `dt`: predictor, correctors, clock.
pub fn advance(&mut self, field: &mut Field, dt: f64) -> StepResult {
assert!(
@@ -581,6 +554,7 @@ impl Solver {
let t_old = self.time;
let t_new = t_old + dt;
field.update_old_values();
self.pressure_lag.set([0.0; 3]);
self.momentum_predictor(field, dt, t_old);
self.apply_boundary_normals(field, t_new);
// A moving body: the mask at the end-of-step geometry.
@@ -607,6 +581,14 @@ impl Solver {
let mut poisson_iterations = 0;
for corrector in 0..self.params.corrector_steps.max(1) {
let sol = self.solve_correction(field, dt, corrector == 0);
if let Some(m) = self.mask.as_ref() {
let l = m.closure_lag(&field.p_prime);
let mut acc = self.pressure_lag.get();
for c in 0..3 {
acc[c] += l[c];
}
self.pressure_lag.set(acc);
}
poisson_iterations += sol.iterations;
let mass_residual = self.apply_correction(field, dt);
if std::env::var_os("RTX_E3_DEBUG").is_some() {
@@ -637,61 +619,3 @@ impl Solver {
}
}
}
/// Refill the pressure of the cells fluid in `new` and not in `old` from
/// their face neighbours fluid in both; returns their count.
fn refill_fresh_cells(old: &Mask, new: &Mask, field: &mut Field) -> usize {
let g = field.grid;
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
let periodic = new.periodic_z();
let mut fresh = 0;
let mut refills = Vec::new();
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let idx = g.cell(k, j, i);
if !(new.is_fluid_cell(idx) && !old.is_fluid_cell(idx)) {
continue;
}
fresh += 1;
let mut sum = 0.0;
let mut count = 0usize;
let mut visit = |nb: usize| {
if new.is_fluid_cell(nb) && old.is_fluid_cell(nb) {
sum += field.p[nb];
count += 1;
}
};
if i + 1 < nx {
visit(g.cell(k, j, i + 1));
}
if i > 0 {
visit(g.cell(k, j, i - 1));
}
if j + 1 < ny {
visit(g.cell(k, j + 1, i));
}
if j > 0 {
visit(g.cell(k, j - 1, i));
}
if k + 1 < nz {
visit(g.cell(k + 1, j, i));
} else if periodic && nz > 1 {
visit(g.cell(0, j, i));
}
if k > 0 {
visit(g.cell(k - 1, j, i));
} else if periodic && nz > 1 {
visit(g.cell(nz - 1, j, i));
}
if count > 0 {
refills.push((idx, sum / count as f64));
}
}
}
}
for (idx, p) in refills {
field.p[idx] = p;
}
fresh
}
@@ -0,0 +1,141 @@
//! The moving body's per-step mask rebuild: fresh-cell pressure refill,
//! imposition from history, step-averaged apertures, the GCL wall-flux
//! table and the previous volumes for the box route.
use super::super::cut::CutGeometry;
use super::super::field::Field;
use super::super::wall::{Mask, WallScheme};
use super::Solver;
impl Solver {
/// The moving body's mask at the end-of-step geometry `t_new`: the
/// pressure of the cells that just became fluid refilled from their
/// neighbours (fluid in both masks), the new mask's prescribed and
/// ghost values imposed from the previous corrected field, the
/// step-averaged apertures and the GCL wall-flux table (cut wall).
/// Returns the fresh-cell count. `field` holds the predicted field.
pub fn rebuild_moving_mask(&mut self, field: &mut Field, dt: f64, t_new: f64) -> usize {
let Some(body) = &self.body else {
return 0;
};
let mut fresh_cells = 0;
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);
let n_in = self.params.aperture_substeps;
if n_in == 0 {
new_mask.set_step_apertures(old_mask);
} else {
// The intermediate geometries, each within the band of the
// previous one.
let g = field.grid;
let h = g.dx.min(g.dy).min(g.dz);
let t_old = t_new - dt;
let mut inner: Vec<CutGeometry> = Vec::with_capacity(n_in);
for m in 1..=n_in {
let tm = t_old + dt * m as f64 / (n_in + 1) as f64;
let prev: Option<(&CutGeometry, f64, f64)> = match (
self.params.max_surface_speed,
inner.last().or(old_mask.cut()),
) {
(Some(speed), Some(c)) => {
Some((c, 3.0 * h, speed * dt / (n_in + 1) as f64))
}
_ => None,
};
inner.push(CutGeometry::build_from(body, g, tm, prev));
}
let refs: Vec<&CutGeometry> = inner.iter().collect();
new_mask.set_step_apertures_with(old_mask, &refs);
}
}
new_mask.impose_from(
body,
&field.u_old,
&field.v_old,
&field.w_old,
&mut field.u,
&mut field.v,
&mut field.w,
t_new,
);
if new_mask.cut().is_some() {
let (table, correction) = match &self.mask {
Some(old_mask) => new_mask.gcl_flux_table(old_mask, dt),
None => new_mask.wall_flux_table(body, t_new),
};
self.wall_fluxes = table;
self.last_ghost_correction = correction;
}
if let Some(old) = &self.mask {
self.vol_old = (0..field.grid.cells())
.map(|i| {
if old.is_fluid_cell(i) {
old.vol(i)
} else {
0.0
}
})
.collect();
}
self.mask = Some(new_mask);
fresh_cells
}
}
/// Refill the pressure of the cells fluid in `new` and not in `old` from
/// their face neighbours fluid in both; returns their count.
fn refill_fresh_cells(old: &Mask, new: &Mask, field: &mut Field) -> usize {
let g = field.grid;
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
let periodic = new.periodic_z();
let mut fresh = 0;
let mut refills = Vec::new();
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let idx = g.cell(k, j, i);
if !(new.is_fluid_cell(idx) && !old.is_fluid_cell(idx)) {
continue;
}
fresh += 1;
let mut sum = 0.0;
let mut count = 0usize;
let mut visit = |nb: usize| {
if new.is_fluid_cell(nb) && old.is_fluid_cell(nb) {
sum += field.p[nb];
count += 1;
}
};
if i + 1 < nx {
visit(g.cell(k, j, i + 1));
}
if i > 0 {
visit(g.cell(k, j, i - 1));
}
if j + 1 < ny {
visit(g.cell(k, j + 1, i));
}
if j > 0 {
visit(g.cell(k, j - 1, i));
}
if k + 1 < nz {
visit(g.cell(k + 1, j, i));
} else if periodic && nz > 1 {
visit(g.cell(0, j, i));
}
if k > 0 {
visit(g.cell(k - 1, j, i));
} else if periodic && nz > 1 {
visit(g.cell(nz - 1, j, i));
}
if count > 0 {
refills.push((idx, sum / count as f64));
}
}
}
}
for (idx, p) in refills {
field.p[idx] = p;
}
fresh
}