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
CI / Distributed Training Tests (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 / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / CI Success (push) Blocked by required conditions
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 / Distributed Training Tests (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 / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / CI Success (push) Blocked by required conditions
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
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5b1621e6ad
commit
0fa05f2056
@@ -25,6 +25,9 @@ use super::wall::{FaceKind, Mask};
|
||||
pub(super) const INERTIA_FLOOR: f64 = 0.1;
|
||||
/// The wall-distance floor of a face, in units of the smallest spacing.
|
||||
pub(super) const DISTANCE_FLOOR: f64 = 0.05;
|
||||
/// Virtual merging: a cell whose fluid fraction (at either end of the
|
||||
/// step) stays below this shares its pressure unknown with a neighbour.
|
||||
pub(super) const MERGE_FRACTION: f64 = 0.1;
|
||||
|
||||
/// Lattice addressing of faces and cells with the periodic wrap in z as
|
||||
/// data: a face of component `c` at `p = [i, j, k]` (its own coordinate is
|
||||
@@ -175,7 +178,7 @@ impl Mask {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
let mut mask = Self {
|
||||
grid: g,
|
||||
periodic_z: periodic,
|
||||
cell_fluid,
|
||||
@@ -190,7 +193,53 @@ impl Mask {
|
||||
cut: Some(cut),
|
||||
step_apertures: None,
|
||||
step_open: None,
|
||||
})
|
||||
merge_master: Vec::new(),
|
||||
};
|
||||
mask.compute_merging(None);
|
||||
Ok(mask)
|
||||
}
|
||||
|
||||
/// The virtual merging map: a small cell (fraction < `MERGE_FRACTION`
|
||||
/// at both ends of the step) takes as master its active face
|
||||
/// neighbour of largest fraction that is not small itself; a small
|
||||
/// cell without such a neighbour keeps its own row.
|
||||
pub(super) fn compute_merging(&mut self, old: Option<&Mask>) {
|
||||
let Some(cut) = self.cut.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let g = self.grid;
|
||||
let n = g.cells();
|
||||
let lat = self.lattice();
|
||||
let frac = |idx: usize| {
|
||||
let v = cut.vol[idx];
|
||||
old.and_then(|o| o.cut.as_ref())
|
||||
.map_or(v, |oc| v.max(oc.vol[idx]))
|
||||
};
|
||||
let small: Vec<bool> = (0..n)
|
||||
.map(|idx| self.cell_active(idx) && frac(idx) < MERGE_FRACTION)
|
||||
.collect();
|
||||
let mut master = vec![usize::MAX; n];
|
||||
for idx in (0..n).filter(|&i| small[i]) {
|
||||
let (k, j, i) = g.kji(idx);
|
||||
let p = [i as i64, j as i64, k as i64];
|
||||
let mut best: Option<(f64, usize)> = None;
|
||||
for d in 0..3 {
|
||||
for side in [-1i64, 1] {
|
||||
let mut q = p;
|
||||
q[d] += side;
|
||||
if let Some(nb) = lat.cell(q) {
|
||||
let v = cut.vol[nb];
|
||||
if self.cell_active(nb) && !small[nb] && best.is_none_or(|b| v > b.0) {
|
||||
best = Some((v, nb));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, m)) = best {
|
||||
master[idx] = m;
|
||||
}
|
||||
}
|
||||
self.merge_master = master;
|
||||
}
|
||||
|
||||
/// Set the step-averaged apertures and the space-time classification
|
||||
@@ -214,6 +263,7 @@ impl Mask {
|
||||
.collect();
|
||||
self.step_open = Some((open(&au), open(&av), open(&aw), active));
|
||||
self.step_apertures = Some((au, av, aw));
|
||||
self.compute_merging(Some(old));
|
||||
}
|
||||
|
||||
pub(super) fn lattice(&self) -> Lattice {
|
||||
|
||||
@@ -27,6 +27,8 @@ pub(crate) struct Level<T: MgScalar> {
|
||||
pub(crate) top: Vec<usize>,
|
||||
pub(crate) bot: Vec<usize>,
|
||||
pub(crate) coarse_of: Vec<usize>,
|
||||
/// Off-stencil links per cell (finest level only; empty elsewhere).
|
||||
pub(crate) links: Vec<Vec<(usize, T)>>,
|
||||
}
|
||||
|
||||
struct Work<T: MgScalar> {
|
||||
@@ -97,6 +99,15 @@ impl<T: MgScalar> Level<T> {
|
||||
.filter(|&idx| parity(idx) == 1)
|
||||
.collect();
|
||||
let cast = |v: &[f64]| v.iter().map(|&x| T::from_f64(x)).collect::<Vec<T>>();
|
||||
let links: Vec<Vec<(usize, T)>> = if problem.links.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
problem
|
||||
.link_lists()
|
||||
.into_iter()
|
||||
.map(|l| l.into_iter().map(|(o, c)| (o, T::from_f64(c))).collect())
|
||||
.collect()
|
||||
};
|
||||
Self {
|
||||
ae: cast(&problem.ae),
|
||||
aw: cast(&problem.aw),
|
||||
@@ -113,6 +124,7 @@ impl<T: MgScalar> Level<T> {
|
||||
top,
|
||||
bot,
|
||||
coarse_of: Vec::new(),
|
||||
links,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +157,11 @@ impl<T: MgScalar> Level<T> {
|
||||
if ab != T::ZERO {
|
||||
s += ab * x[self.bot[idx]];
|
||||
}
|
||||
if !self.links.is_empty() {
|
||||
for &(other, c) in &self.links[idx] {
|
||||
s += c * x[other];
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
@@ -356,6 +373,7 @@ impl Components {
|
||||
let mut members = Vec::new();
|
||||
let mut singular = Vec::new();
|
||||
let mut stack = Vec::new();
|
||||
let links = problem.link_lists();
|
||||
for &seed in cells {
|
||||
if id[seed] != usize::MAX {
|
||||
continue;
|
||||
@@ -395,6 +413,9 @@ impl Components {
|
||||
if let Some(b) = problem.bottom(idx, k) {
|
||||
visit(b, problem.ab[idx]);
|
||||
}
|
||||
for &(other, c) in &links[idx] {
|
||||
visit(other, c);
|
||||
}
|
||||
}
|
||||
members.push(list);
|
||||
singular.push(!has_dirichlet);
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) struct OperatorKey {
|
||||
periodic_z: bool,
|
||||
active: Vec<bool>,
|
||||
coefficients: Vec<u64>,
|
||||
links: Vec<(usize, usize, u64)>,
|
||||
smoother_sweeps: usize,
|
||||
coarsest_cells: usize,
|
||||
smoother: MgSmoother,
|
||||
@@ -44,6 +45,11 @@ impl OperatorKey {
|
||||
periodic_z: problem.periodic_z,
|
||||
active: problem.active.clone(),
|
||||
coefficients: Self::bits(problem).collect(),
|
||||
links: problem
|
||||
.links
|
||||
.iter()
|
||||
.map(|&(a, b, c)| (a, b, c.to_bits()))
|
||||
.collect(),
|
||||
smoother_sweeps: params.smoother_sweeps,
|
||||
coarsest_cells: params.coarsest_cells,
|
||||
smoother: params.smoother,
|
||||
@@ -59,6 +65,12 @@ impl OperatorKey {
|
||||
&& self.coarsest_cells == params.coarsest_cells
|
||||
&& self.smoother == params.smoother
|
||||
&& self.active == problem.active
|
||||
&& self.links.len() == problem.links.len()
|
||||
&& self
|
||||
.links
|
||||
.iter()
|
||||
.zip(&problem.links)
|
||||
.all(|(&(a, b, c), &(pa, pb, pc))| a == pa && b == pb && c == pc.to_bits())
|
||||
&& self.coefficients.iter().copied().eq(Self::bits(problem))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ pub struct Problem {
|
||||
pub ab: Vec<f64>,
|
||||
pub extra_diag: Vec<f64>,
|
||||
pub rhs: Vec<f64>,
|
||||
/// Off-stencil symmetric links `(a, b, coefficient)` between two active
|
||||
/// cells (a virtually merged small cell's master to the small cell's
|
||||
/// other neighbours); each adds `coefficient` to both diagonals and
|
||||
/// `−coefficient` off-diagonal both ways. Carried by the finest level
|
||||
/// only (the coarse levels stay seven-point: preconditioner quality).
|
||||
pub links: Vec<(usize, usize, f64)>,
|
||||
}
|
||||
|
||||
impl Problem {
|
||||
@@ -42,9 +48,31 @@ impl Problem {
|
||||
ab: vec![0.0; n],
|
||||
extra_diag: vec![0.0; n],
|
||||
rhs: vec![0.0; n],
|
||||
links: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The link coefficients per cell (`(other, coefficient)` lists).
|
||||
#[must_use]
|
||||
pub fn link_lists(&self) -> Vec<Vec<(usize, f64)>> {
|
||||
let mut out = vec![Vec::new(); self.nx * self.ny * self.nz];
|
||||
for &(a, b, c) in &self.links {
|
||||
out[a].push((b, c));
|
||||
out[b].push((a, c));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The sum of the link coefficients on a cell.
|
||||
#[must_use]
|
||||
pub fn link_diagonal(&self, idx: usize) -> f64 {
|
||||
self.links
|
||||
.iter()
|
||||
.filter(|&&(a, b, _)| a == idx || b == idx)
|
||||
.map(|&(_, _, c)| c)
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn index(&self, k: usize, j: usize, i: usize) -> usize {
|
||||
@@ -86,13 +114,18 @@ impl Problem {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn diagonal(&self, idx: usize) -> f64 {
|
||||
self.ae[idx]
|
||||
let stencil = self.ae[idx]
|
||||
+ self.aw[idx]
|
||||
+ self.an[idx]
|
||||
+ self.as_[idx]
|
||||
+ self.at[idx]
|
||||
+ self.ab[idx]
|
||||
+ self.extra_diag[idx]
|
||||
+ self.extra_diag[idx];
|
||||
if self.links.is_empty() {
|
||||
stencil
|
||||
} else {
|
||||
stencil + self.link_diagonal(idx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure Neumann: no active cell has a Dirichlet contribution.
|
||||
@@ -109,6 +142,7 @@ impl Problem {
|
||||
#[must_use]
|
||||
pub fn residual_l1(&self, p: &[f64]) -> f64 {
|
||||
let (nx, ny, nz) = (self.nx, self.ny, self.nz);
|
||||
let links = self.link_lists();
|
||||
let mut sum = 0.0;
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
@@ -144,6 +178,9 @@ impl Problem {
|
||||
nb += self.ab[idx] * p[b];
|
||||
}
|
||||
}
|
||||
for &(other, c) in &links[idx] {
|
||||
nb += c * p[other];
|
||||
}
|
||||
sum += (self.rhs[idx] - (ap * p[idx] - nb)).abs();
|
||||
}
|
||||
}
|
||||
@@ -170,6 +207,11 @@ impl Problem {
|
||||
return Err(format!("{name}: length {len}, expected nx*ny*nz = {n}"));
|
||||
}
|
||||
}
|
||||
for &(a, b, c) in &self.links {
|
||||
if a >= n || b >= n || a == b || !self.active[a] || !self.active[b] || c <= 0.0 || c.is_nan() {
|
||||
return Err(format!("invalid link ({a}, {b}, {c})"));
|
||||
}
|
||||
}
|
||||
let (nx, ny, nz) = (self.nx, self.ny, self.nz);
|
||||
let symmetric = |a: f64, b: f64| (a - b).abs() <= 1e-12 * a.abs().max(b.abs());
|
||||
for k in 0..nz {
|
||||
|
||||
+118
-6
@@ -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
|
||||
|
||||
@@ -88,6 +88,10 @@ pub struct Mask {
|
||||
/// step (a dying cell empties through the apertures it had); `None` =
|
||||
/// the instantaneous kinds.
|
||||
pub(super) step_open: Option<(Vec<bool>, Vec<bool>, Vec<bool>, Vec<bool>)>,
|
||||
/// Virtual merging (item 10b): the master cell of every small cell
|
||||
/// (`usize::MAX` = its own row) — a small cell shares its pressure
|
||||
/// unknown with its largest active face neighbour in the projection.
|
||||
pub(super) merge_master: Vec<usize>,
|
||||
}
|
||||
|
||||
/// The z lattice position of a query: the lower plane index, the upper
|
||||
@@ -497,9 +501,27 @@ impl Mask {
|
||||
cut: None,
|
||||
step_apertures: None,
|
||||
step_open: None,
|
||||
merge_master: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The master of a virtually merged small cell.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn master(&self, idx: usize) -> Option<usize> {
|
||||
match self.merge_master.get(idx) {
|
||||
Some(&m) if m != usize::MAX => Some(m),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub fn merged_cells(&self) -> usize {
|
||||
self.merge_master
|
||||
.iter()
|
||||
.filter(|&&m| m != usize::MAX)
|
||||
.count()
|
||||
}
|
||||
|
||||
// The projection's unknowns (the instantaneous kinds at rest).
|
||||
#[inline]
|
||||
#[must_use]
|
||||
|
||||
Reference in New Issue
Block a user