rtx-cfd: overset A-P3 — the falsifier plate on the overset (FAILS the registered gates by one order less than the staircase); wall force; composite pressure-level pin; Schwarz stall detection
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (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
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
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

patch_gen::{stadium, graded_fractions}: the falsifier plate as a stadium O-grid
(semicircular ends r = half-thickness; 16 cells per end arc, straights graded
0.30 h -> h at 1.15, offset 6 h, 12 rows stretched 4x; 148x12 cells, every ray
a normal, worst non-orthogonality 4 deg). CurvilinearPisoSolver::surface_force
(+ PatchLoad): F = sum(-p_f S_f + mu (grad u + grad u^T)_f . S_f) on the wall
faces with the wall cell's LSQ gradients (wall Dirichlet in the velocity fit);
HELD on the phantom circle against the exact stress integral: 1.3e-2 / 6.3e-3 /
3.6e-3 at n = 32/64/128 (orders 1.05 / 0.81), 22x the staircase's accuracy.
OversetPisoSolver: the composite p' level pinned to zero mean over the active
cells every round (the coupled problem is pure Neumann; the temporal warm start
handed each step's level to the next — background pressure 1e7 growing 5e4 per
step on the falsifier; an unpinned level also inflated the relative Schwarz
stop); stall detection (no progress over three rounds = the inner solvers'
noise floor; 6560 of 150k steps burned the 20-round cap at n = 64, a 7.5 h
n = 128 march); schwarz_stalled in the result.

tests/overset_falsifier.rs (records; RTX_OVERSET_FALSIFIER_STRICT asserts the
registered gates, _LADDER runs dt/2 and dt/4, _TRACE the top-12 spike steps):
max spike 594 / 981 / 1720 N/m at dt / dt/2 / dt/4 (staircase 6490 / 12600 /
25600), rms spike 61-89 (810), far probe 502-1509 (7900), KE injection 0.16-0.21
J/m per event on the common cell set (2.6) — every large spike a ~104-cell
full-row reclassification; exponent -0.77 (-1.0). The registered 5% gate (8.75
N/m) is missed 68x: the overset's own reclassification impulse is the finding
(omni-cortex overset_metal_campaign.md §5.10); P3b = locate per cell, then the
fringe flux balance. tests/patch_stadium.rs, curvilinear_loads.rs,
overset_common::plate_patch.

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 04:32:43 -07:00
co-authored by Claude Fable 5.1
parent afd1bff6ee
commit e2edff9b1d
8 changed files with 943 additions and 2 deletions
@@ -85,6 +85,9 @@ pub struct OversetResult {
pub rounds: Vec<usize>,
/// Whether every corrector's Schwarz iteration met its stop.
pub schwarz_converged: bool,
/// Correctors whose rounds stalled at the inner solvers' noise floor
/// (no progress over three rounds) and were stopped there.
pub schwarz_stalled: usize,
/// Background mass residual after the last corrector (its own measure).
pub background_residual: f64,
/// Largest patch cell imbalance after the last corrector.
@@ -296,6 +299,33 @@ impl OversetPisoSolver {
self.background.initialize(&mut field.background)
}
/// Subtract the mean of `p'` over the active background cells (the
/// composite level pin; the fringe cells' Dirichlet values shift with
/// it so the face corrections across activefringe faces are unchanged).
fn remove_background_mean(&self, field: &mut FlowField) {
let (nx, ny, _, _) = self.grid;
let (mut sum, mut count) = (0.0, 0usize);
for j in 0..ny {
for i in 0..nx {
if self.overlap.class(j, i) == CellClass::Active {
sum += field.p_prime[(j, i)];
count += 1;
}
}
}
if count == 0 {
return;
}
let mean = sum / count as f64;
for j in 0..ny {
for i in 0..nx {
if self.overlap.class(j, i) != CellClass::Hole {
field.p_prime[(j, i)] -= mean;
}
}
}
}
/// The exchange for the next step: prescribed background faces and
/// fringe `p` from the patch's cell field; acceptor `u, v, p` from the
/// background.
@@ -410,6 +440,7 @@ impl OversetPisoSolver {
let n_acc = self.overlap.acceptors.len();
let mut rounds = Vec::with_capacity(self.params.corrector_steps);
let mut schwarz_converged = true;
let mut schwarz_stalled = 0usize;
let mut residual_history = Vec::new();
let mut final_residual = f64::INFINITY;
let mut patch_iterations = 0usize;
@@ -432,6 +463,7 @@ impl OversetPisoSolver {
let mut patch_pc = vec![0.0; self.patch.mesh().cell_count()];
let mut done = false;
let mut used = 0usize;
let mut history: Vec<f64> = Vec::new();
for round in 0..self.params.max_rounds.max(1) {
used = round + 1;
// Patch with Dirichlet a.
@@ -453,6 +485,14 @@ impl OversetPisoSolver {
dt,
corrector == 0 || round > 0,
)?;
// Pin the composite level: the coupled p' problem is pure
// Neumann (walls everywhere), so its constant mode is
// undamped by the rounds and the warm start hands each
// step's level to the next — measured as a background
// pressure of 1e7 growing 5e4 per step on the falsifier.
// A constant in p' moves no velocity; zero mean over the
// active cells pins it.
self.remove_background_mean(&mut field.background);
let g = self.overlap.acceptor_scalar(&field.background.p_prime);
let r: Vec<f64> = g.iter().zip(&a).map(|(x, y)| x - y).collect();
let g_max = g.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
@@ -469,6 +509,17 @@ impl OversetPisoSolver {
done = true;
break;
}
// Stall: no progress over three rounds means the exchanged
// values sit at the inner solvers' noise floor (the step's
// own p' is that small near a steady state); more rounds
// cannot help — measured as 6560 of 150k steps burning the
// 20-round cap at n = 64, and a 7.5 h n = 128 march.
history.push(change);
if history.len() >= 4 && change > 0.7 * history[history.len() - 4] {
schwarz_stalled += 1;
done = true;
break;
}
let next = anderson.next(&a, &r);
// Guard: an extrapolation far beyond the data is noise-driven;
// take the plain Schwarz step instead.
@@ -525,6 +576,7 @@ impl OversetPisoSolver {
Ok(OversetResult {
rounds,
schwarz_converged,
schwarz_stalled,
background_residual: bg.solver_result.final_residual,
patch_max_divergence: patch_max_div,
patch_poisson_iterations: patch_iterations,