embedded3 item 10b: virtual merging of small cells in the projection (fraction < 0.1 → master = largest active face neighbour; off-stencil links on the fine Poisson level; merged rhs, anchor, mass residual and source scale); static sphere rows within 0.02 %, loads 9.4/10.4 %; stadium falsifier spikes 39–54× below the binary wall (circle 6–8×), energy per event 6–9× lower
Performance Benchmarks / Run Benchmarks (push) Failing after 4s
CI / Clippy Check (push) Failing after 3s
CI / Build (ubuntu-latest) (push) Failing after 3s
CI / Format Check (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 6s
Documentation / Build API Documentation (push) Failing after 19s
CI / Build CPU-Only (Explicit) (push) Failing after 1m21s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Distributed Training Tests (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 / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-17 16:41:29 -05:00
co-authored by Claude Fable 5.1
parent 5b1621e6ad
commit 0fa05f2056
7 changed files with 281 additions and 16 deletions
@@ -80,13 +80,98 @@ impl Solver {
}
}
}
self.merge_small_cells(&mut problem);
problem
}
/// Virtual merging (item 10b): every small cell's row is folded into
/// its master's — the small cell becomes inactive, its faces to other
/// cells become links from the master (to the neighbour, or to that
/// neighbour's master), its Dirichlet contribution moves to the
/// master; the face between the two is internal to the merged cell.
fn merge_small_cells(&self, problem: &mut Problem) {
let Some(mask) = self.mask.as_ref() else {
return;
};
if mask.merged_cells() == 0 {
return;
}
let g = mask.grid();
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
let target = |idx: usize| mask.master(idx).unwrap_or(idx);
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let s = g.cell(k, j, i);
let Some(m) = mask.master(s) else { continue };
if !problem.active[s] {
continue;
}
problem.active[s] = false;
// (neighbour, coefficient on s, the mirror coefficient on the neighbour)
let mut faces: Vec<(usize, f64)> = Vec::with_capacity(6);
if i + 1 < nx {
faces.push((s + 1, problem.ae[s]));
problem.aw[s + 1] = 0.0;
}
if i > 0 {
faces.push((s - 1, problem.aw[s]));
problem.ae[s - 1] = 0.0;
}
if j + 1 < ny {
faces.push((s + nx, problem.an[s]));
problem.as_[s + nx] = 0.0;
}
if j > 0 {
faces.push((s - nx, problem.as_[s]));
problem.an[s - nx] = 0.0;
}
if let Some(t) = problem.top(s, k) {
faces.push((t, problem.at[s]));
problem.ab[t] = 0.0;
}
if let Some(b) = problem.bottom(s, k) {
faces.push((b, problem.ab[s]));
problem.at[b] = 0.0;
}
problem.ae[s] = 0.0;
problem.aw[s] = 0.0;
problem.an[s] = 0.0;
problem.as_[s] = 0.0;
problem.at[s] = 0.0;
problem.ab[s] = 0.0;
for (n, c) in faces {
let t = target(n);
if c > 0.0 && t != m {
problem.links.push((m.min(t), m.max(t), c));
}
}
problem.extra_diag[m] += problem.extra_diag[s];
problem.extra_diag[s] = 0.0;
}
}
}
}
/// Fold the small cells' right-hand sides into their masters'.
fn merge_rhs(&self, problem: &mut Problem) {
let Some(mask) = self.mask.as_ref() else {
return;
};
for s in 0..problem.rhs.len() {
if let Some(m) = mask.master(s) {
let v = problem.rhs[s];
problem.rhs[m] += v;
problem.rhs[s] = 0.0;
}
}
}
/// The operator with `field.sp` as the right-hand side.
pub(crate) fn poisson_problem(&self, field: &Field, dt: f64) -> Problem {
let mut problem = self.poisson_operator(field.grid, dt);
problem.rhs.copy_from_slice(&field.sp);
self.merge_rhs(&mut problem);
problem
}
@@ -94,9 +179,10 @@ impl Solver {
/// the 2D `(1, 1)` at `k = 0`), or `None` with an outlet.
pub(crate) fn anchor_cell(&self, g: Grid) -> Option<usize> {
(!self.params.boundaries.any_outlet()).then(|| {
self.mask
.as_ref()
.map_or(g.cell(0, 1, 1), super::super::wall::Mask::anchor)
self.mask.as_ref().map_or(g.cell(0, 1, 1), |m| {
let a = m.anchor();
m.master(a).unwrap_or(a)
})
})
}
@@ -274,8 +360,20 @@ impl Solver {
}
}
}
let inner_stop = self.inner_stop(g, source_scale);
let problem = self.poisson_problem(field, dt);
// The source scale reads the merged right-hand side (a merged small
// cell's own divergence is not zero, its pair's is); identical to
// the per-cell sum when nothing is merged.
if self.mask.as_ref().is_some_and(|m| m.merged_cells() > 0) {
source_scale = problem
.rhs
.iter()
.zip(&problem.active)
.filter(|&(_, &a)| a)
.map(|(r, _)| r.abs())
.sum();
}
let inner_stop = self.inner_stop(g, source_scale);
let mut p_prime = vec![0.0; g.cells()];
if warm_start {
for k in 0..nz {
@@ -310,6 +408,13 @@ impl Solver {
c0 + 1,
k0 + solution.iterations as u64,
);
if let Some(mask) = self.mask.as_ref() {
for s in 0..p_prime.len() {
if let Some(m) = mask.master(s) {
p_prime[s] = p_prime[m];
}
}
}
field.p_prime.copy_from_slice(&p_prime);
solution
}
@@ -401,7 +506,7 @@ impl Solver {
}
}
}
let mut mass_imbalance = 0.0;
let mut cell_flux = vec![0.0; g.cells()];
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
@@ -422,10 +527,17 @@ impl Solver {
if cut {
divergence_flux += rho * self.wall_flux(idx);
}
mass_imbalance += divergence_flux.abs();
// A merged small cell's flux counts with its master's.
let owner = self
.mask
.as_ref()
.and_then(|m| m.master(idx))
.unwrap_or(idx);
cell_flux[owner] += divergence_flux;
}
}
}
let mass_imbalance: f64 = cell_flux.iter().map(|f| f.abs()).sum();
let reference_flux = self.reference_flux(g);
if reference_flux > 0.0 {
mass_imbalance / reference_flux