rtx-cfd: overset P3b — the reclassification impulse located (the fringe ring is a staircase of the interpolated velocities' mass defect) and removed by a converged fringe flux balance (default on): falsifier max spike 594 → 5.50 N/m at the FSI2 step (staircase 6490), 10.95 / 16.79 at dt/2 / dt/4 (12600 / 25600), rms spike 0.07% of the force, far probe 6 (7900), KE per event 4.9e-3 J/m falling with Δt (2.6 fixed)
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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
CI / CI Success (push) Canceled after 0s

OverlapMap::balance_fringe_fluxes: Gauss–Seidel through the prescribed faces of
every fringe cell to 1e-12 of the prescribed flux scale (≤ 50 sweeps), after
every fringe stamping (3 fixed sweeps 101 N/m, 10 sweeps 5.5 — converged is the
rule). OversetParameters: fringe_flux_balance (default on, RTX_OVERSET_NO_BALANCE
off), fringe_balance_tolerance, refill_turned_active (measured no effect: 593.7 →
593.8; kept as the record), stall_rounds opt-in. P3b locating trace
RTX_OVERSET_TRACE_SP (continuity source by class change in cell volumes/step,
stored-pressure jump of turned-active cells): the flipped cells' mass source
≤ 6e-3 cell volumes/step, their stored pressure 5–10% of the range off their
neighbours (4.4% on the static MMS — the meshes' discretization disagreement).
Knock-outs refuted (RTX_OVERSET_H1 keep own face velocities, H4 no warm start,
pressure refill): 593–597 N/m each. S4 MMS with the balance: velocity errors
within 0.1% of the pinned values, the background's overlap mass defect 1e-13 by
construction, pressure errors unchanged. overset_mms prints pressure
diagnostics; overset_falsifier records the balanced ladder (regression guard
20 N/m at dt; RTX_OVERSET_FALSIFIER_STRICT asserts the registered gates — (ii)
holds at dt, misses at dt/2, dt/4; (iv) fails: residual ∝ 1/Δt^0.8).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
Omar Sobh
2026-09-05 19:16:16 -07:00
co-authored by Claude Fable 5.1
parent 6b8837301f
commit 62df6bd628
5 changed files with 372 additions and 15 deletions
@@ -431,6 +431,77 @@ impl OverlapMap {
.collect()
}
/// Flux balance at the fringe (ChesshireHenshaw in spirit): make every
/// fringe cell divergence-free by adjusting only its PRESCRIBED faces
/// (never a face shared with an active cell), spreading each cell's
/// imbalance over them by face length, in GaussSeidel sweeps (a face
/// shared by two fringe cells is corrected by both) until the largest
/// fringe-cell imbalance is below `tol` (volume flux) or `max_sweeps`
/// is reached. Returns `(sweeps, worst imbalance)`. This is what removes
/// the reclassification impulse of A-P3: with the fringe ring a
/// staircase of the interpolated velocities' mass defect, every
/// row flip injected that defect in one step (§5.10).
pub fn balance_fringe_fluxes(
&self,
field: &mut FlowField,
tol: f64,
max_sweeps: usize,
) -> (usize, f64) {
let (nx, ny, dx, dy) = (self.nx, self.ny, self.dx, self.dy);
let is_prescribed_u: std::collections::HashSet<(usize, usize)> =
self.fringe_u.iter().map(|e| (e.j, e.i)).collect();
let is_prescribed_v: std::collections::HashSet<(usize, usize)> =
self.fringe_v.iter().map(|e| (e.j, e.i)).collect();
let _ = (nx, ny);
let mut worst = f64::INFINITY;
let mut sweeps = 0usize;
while sweeps < max_sweeps && worst > tol {
sweeps += 1;
worst = 0.0;
for e in &self.fringe_cells {
let (j, i) = (e.j, e.i);
let div = (field.u[(j, i + 1)] - field.u[(j, i)]) * dy
+ (field.v[(j + 1, i)] - field.v[(j, i)]) * dx;
// Prescribed faces of this cell with their outward sign and length.
let mut faces: Vec<(bool, usize, usize, f64, f64)> = Vec::with_capacity(4);
if is_prescribed_u.contains(&(j, i + 1)) {
faces.push((true, j, i + 1, 1.0, dy));
}
if is_prescribed_u.contains(&(j, i)) {
faces.push((true, j, i, -1.0, dy));
}
if is_prescribed_v.contains(&(j + 1, i)) {
faces.push((false, j + 1, i, 1.0, dx));
}
if is_prescribed_v.contains(&(j, i)) {
faces.push((false, j, i, -1.0, dx));
}
let total_len: f64 = faces.iter().map(|f| f.4).sum();
if total_len == 0.0 {
worst = worst.max(div.abs());
continue;
}
for (is_u, jj, ii, sign, len) in faces {
// outward flux change on this face = div · len / total_len
let dvel = -sign * div / total_len;
if is_u {
field.u[(jj, ii)] += dvel;
} else {
field.v[(jj, ii)] += dvel;
}
let _ = len;
}
}
for e in &self.fringe_cells {
let (j, i) = (e.j, e.i);
let div = (field.u[(j, i + 1)] - field.u[(j, i)]) * dy
+ (field.v[(j + 1, i)] - field.v[(j, i)]) * dx;
worst = worst.max(div.abs());
}
}
(sweeps, worst)
}
/// A cell-centred background scalar (e.g. `p'`) at every acceptor.
pub fn acceptor_scalar(&self, m: &nalgebra::DMatrix<f64>) -> Vec<f64> {
self.acceptors.iter().map(|a| a.p.value(m)).collect()