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
@@ -0,0 +1,171 @@
//! A-P3, first task (`docs/overset_metal_campaign.md` §5.10): the wall
//! force on the curvilinear patch. On the phantom circle of the embedded
//! MMS ((0.6, 0.45), r = 0.2, the exact field as the wall velocity) the
//! patch's surface force must converge to the exact surface integral of
//! the manufactured stress, `F = ∮ (p I + μ(∇u + ∇uᵀ)) n ds` — the
//! embedded solver's staircase reconstruction measured relative errors
//! 0.52 / 0.29 / 0.15 at n = 16 / 32 / 64 (first order). The patch runs
//! standalone with its acceptor ring stamped from the exact field (the S3
//! harness), so the wall force is the patch's own.
use rtx_cfd::mesh::PatchSide;
use rtx_cfd::mesh::patch_gen::annulus_skewed;
use rtx_cfd::solvers::incompressible::{
CurvilinearParameters, CurvilinearPisoSolver, NormalDiffusion, PatchField,
};
use rtx_cfd::{CfdConfig, CfdResult};
use std::f64::consts::PI;
const RHO: f64 = 1.0;
const MU: f64 = 0.05;
const CX: f64 = 0.6;
const CY: f64 = 0.45;
const R0: f64 = 0.2;
const R1: f64 = 0.354;
fn u_exact(x: f64, y: f64) -> f64 {
(PI * x).sin() * (PI * y).cos()
}
fn v_exact(x: f64, y: f64) -> f64 {
-(PI * x).cos() * (PI * y).sin()
}
fn p_exact(x: f64, y: f64) -> f64 {
(PI * x).sin() * (PI * y).sin()
}
fn source(x: f64, y: f64) -> (f64, f64) {
let conv = RHO * 0.5 * PI;
(
conv * (2.0 * PI * x).sin()
+ 2.0 * PI * PI * MU * u_exact(x, y)
+ PI * (PI * x).cos() * (PI * y).sin(),
conv * (2.0 * PI * y).sin()
+ 2.0 * PI * PI * MU * v_exact(x, y)
+ PI * (PI * x).sin() * (PI * y).cos(),
)
}
/// `F = ∮ (p I + μ(∇u + ∇uᵀ)) n ds` on the circle by fine quadrature
/// (`embedded_mms.rs`).
fn exact_force() -> (f64, f64) {
let n = 20_000;
let (mut fx, mut fy) = (0.0, 0.0);
for k in 0..n {
let theta = (k as f64 + 0.5) * 2.0 * PI / n as f64;
let (s, c) = theta.sin_cos();
let (x, y) = (CX + R0 * c, CY + R0 * s);
let ux = PI * (PI * x).cos() * (PI * y).cos();
let uy = -PI * (PI * x).sin() * (PI * y).sin();
let vx = PI * (PI * x).sin() * (PI * y).sin();
let vy = -PI * (PI * x).cos() * (PI * y).cos();
let p = p_exact(x, y);
let sxx = -p + 2.0 * MU * ux;
let syy = -p + 2.0 * MU * vy;
let sxy = MU * (uy + vx);
let ds = 2.0 * PI * R0 / n as f64;
fx += (sxx * c + sxy * s) * ds;
fy += (sxy * c + syy * s) * ds;
}
(fx, fy)
}
async fn wall_force(n: usize) -> CfdResult<((f64, f64), usize)> {
let mesh = annulus_skewed([CX, CY], R0, R1, 9 * n / 4, n / 4, 0.3, 3.0)?;
let nu = MU / RHO;
let mut hs = f64::INFINITY;
for c in 0..mesh.cell_count() {
for (f, _) in mesh.cell_faces(c) {
if mesh.is_sface(f) {
let d = mesh.faces()[f].d;
hs = hs.min((d[0] * d[0] + d[1] * d[1]).sqrt());
}
}
}
let dt = 0.4 * (hs * hs / (4.0 * nu)).min(1.0 / n as f64);
let config = CfdConfig::new()
.with_density(RHO)
.with_viscosity(MU)
.with_reference_velocity(1.0)
.with_reference_length(1.0);
let mut solver = CurvilinearPisoSolver::new(
config,
CurvilinearParameters {
tolerance: 1e-5,
normal_diffusion: NormalDiffusion::LineImplicit,
..CurvilinearParameters::default()
},
mesh,
)?;
solver.set_boundary_velocity(|x, y, _| (u_exact(x, y), v_exact(x, y)));
solver.set_momentum_source(|x, y, _| source(x, y));
solver.set_acceptor_ring(true);
let (ns, nn) = (solver.mesh().ns(), solver.mesh().nn());
let acc: Vec<(f64, f64, f64)> = (0..ns)
.map(|i| {
let xy = solver.mesh().centre(solver.mesh().cell(nn - 1, i));
(
u_exact(xy[0], xy[1]),
v_exact(xy[0], xy[1]),
p_exact(xy[0], xy[1]),
)
})
.collect();
let zeros = vec![0.0; ns];
let mut field = PatchField::new(solver.mesh());
solver.initialize(&mut field, |_, _| (0.0, 0.0));
solver.stamp_acceptors(&mut field, &acc);
solver.set_acceptor_correction(&zeros);
let mut steady = f64::INFINITY;
let mut steps = 0;
for _ in 0..400_000 {
let before = (field.u.clone(), field.v.clone());
solver.advance(&mut field, dt).await?;
solver.stamp_acceptors(&mut field, &acc);
steps += 1;
let change = field
.u
.iter()
.zip(&before.0)
.chain(field.v.iter().zip(&before.1))
.map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max);
steady = change / dt;
if steady < 1e-6 {
break;
}
}
assert!(steady < 1e-6, "no steady state: {steady:.3e}");
let load = solver.surface_force(&field, PatchSide::Inner, solver.time());
let f = load.total();
Ok(((f[0], f[1]), steps))
}
#[tokio::test]
async fn wall_force_on_the_phantom_circle_converges_to_the_exact_stress_integral() -> CfdResult<()>
{
let (ex, ey) = exact_force();
let scale = (ex * ex + ey * ey).sqrt();
println!(" exact force ({ex:.6e}, {ey:.6e}), |F| {scale:.4e}");
let mut errs = Vec::new();
for n in [32usize, 64, 128] {
let ((fx, fy), steps) = wall_force(n).await?;
let rel = ((fx - ex).powi(2) + (fy - ey).powi(2)).sqrt() / scale;
println!(
" patch n = {n} (ns {} nn {}): force ({fx:.6e}, {fy:.6e}), relative error {rel:.3e}, {steps} steps",
9 * n / 4,
n / 4
);
errs.push(rel);
}
let orders: Vec<f64> = errs.windows(2).map(|w| (w[0] / w[1]).log2()).collect();
println!(" wall-force orders {orders:?} (embedded staircase: 0.52 / 0.29 / 0.15 at 16/32/64)");
// Better than the staircase reconstruction at equal h, and converging.
assert!(
errs[0] < 0.29 && errs[1] < 0.15,
"patch wall force worse than the staircase: {errs:?}"
);
assert!(
orders.iter().all(|&o| o > 0.8),
"wall-force orders {orders:?}"
);
Ok(())
}