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
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:
co-authored by
Claude Fable 5.1
parent
afd1bff6ee
commit
e2edff9b1d
@@ -609,3 +609,83 @@ pub(crate) struct StepStart {
|
||||
/// Boundary-flux defect removed on a closed patch.
|
||||
pub(crate) adjustment: f64,
|
||||
}
|
||||
|
||||
/// Fluid force on a boundary side of the patch (per unit depth).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PatchLoad {
|
||||
/// Pressure part.
|
||||
pub pressure: [f64; 2],
|
||||
/// Viscous part.
|
||||
pub viscous: [f64; 2],
|
||||
/// Faces integrated.
|
||||
pub faces: usize,
|
||||
}
|
||||
|
||||
impl PatchLoad {
|
||||
/// Total force.
|
||||
pub fn total(&self) -> [f64; 2] {
|
||||
[
|
||||
self.pressure[0] + self.viscous[0],
|
||||
self.pressure[1] + self.viscous[1],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl CurvilinearPisoSolver {
|
||||
/// The fluid force on the body along `side` (the wall of an O-grid:
|
||||
/// `Inner`): `F = Σ_f (−p_f S_f + μ (∇u + ∇uᵀ)_f · S_f)` with `S_f`
|
||||
/// pointing from the body into the fluid. The face pressure is the
|
||||
/// wall cell's pressure extrapolated linearly with its least-squares
|
||||
/// gradient; the face velocity gradient is the wall cell's least-squares
|
||||
/// gradient with the face's Dirichlet value in the fit (the wall shear
|
||||
/// enters through the wall point itself). No traction reconstruction
|
||||
/// through a staircase — the second thing the overset buys (§2.1).
|
||||
pub fn surface_force(&self, field: &PatchField, side: PatchSide, t: f64) -> PatchLoad {
|
||||
let mesh = &self.mesh;
|
||||
let mu = self.config.viscosity;
|
||||
let (mut fp, mut fv) = ([0.0; 2], [0.0; 2]);
|
||||
let mut faces = 0usize;
|
||||
for (f, face) in mesh.faces().iter().enumerate() {
|
||||
if mesh.side(f) != Some(side) {
|
||||
continue;
|
||||
}
|
||||
let c = mesh.boundary_cell(f);
|
||||
// Outward from the body = into the fluid: for the Inner side
|
||||
// (k = 0, the cell is the neighbour) that is +S; for the Outer
|
||||
// side (the cell is the owner) it is −S.
|
||||
let sign = if face.neigh.is_some() { 1.0 } else { -1.0 };
|
||||
let s = [sign * face.s[0], sign * face.s[1]];
|
||||
let xc = mesh.centre(c);
|
||||
let dxf = [face.centre[0] - xc[0], face.centre[1] - xc[1]];
|
||||
let gp = self.pressure_gradient(&field.p, c);
|
||||
let p_f = field.p[c] + gp[0] * dxf[0] + gp[1] * dxf[1];
|
||||
fp[0] -= p_f * s[0];
|
||||
fp[1] -= p_f * s[1];
|
||||
let wall = |ff: usize| -> Option<(f64, f64)> {
|
||||
let fc = &mesh.faces()[ff];
|
||||
let sd = mesh.side(ff)?;
|
||||
match self.params.boundaries.get(sd) {
|
||||
SideBc::Velocity => {
|
||||
Some(self.boundary_velocity(sd, fc.centre[0], fc.centre[1], t))
|
||||
}
|
||||
SideBc::Outlet => None,
|
||||
}
|
||||
};
|
||||
let gu = self
|
||||
.ops
|
||||
.gradient(mesh, c, &field.u, &|ff| wall(ff).map(|w| w.0));
|
||||
let gv = self
|
||||
.ops
|
||||
.gradient(mesh, c, &field.v, &|ff| wall(ff).map(|w| w.1));
|
||||
// τ = μ (∇u + ∇uᵀ): τxx = 2 u_x, τxy = u_y + v_x, τyy = 2 v_y.
|
||||
fv[0] += mu * (2.0 * gu[0] * s[0] + (gu[1] + gv[0]) * s[1]);
|
||||
fv[1] += mu * ((gu[1] + gv[0]) * s[0] + 2.0 * gv[1] * s[1]);
|
||||
faces += 1;
|
||||
}
|
||||
PatchLoad {
|
||||
pressure: fp,
|
||||
viscous: fv,
|
||||
faces,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ pub use boundary_conditions::{
|
||||
};
|
||||
pub use curvilinear::{
|
||||
CurvilinearParameters, CurvilinearPisoSolver, CurvilinearResult, CurvilinearSolverState,
|
||||
NormalDiffusion, Operators, PatchBoundaries, PatchConvection, PatchField, SideBc, StepGeometry,
|
||||
NormalDiffusion, Operators, PatchBoundaries, PatchConvection, PatchField, PatchLoad, SideBc,
|
||||
StepGeometry,
|
||||
};
|
||||
pub use embedded::{EmbeddedParameters, EmbeddedPisoSolver, EmbeddedResult, EmbeddedSolverState};
|
||||
pub use embedded_body::{
|
||||
|
||||
@@ -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 active–fringe 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,
|
||||
|
||||
Reference in New Issue
Block a user