rtx-cfd: overset A-P1 GATED — the curvilinear patch moves and deforms under an exact 2-D DGCL
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
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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s

StepGeometry (motion.rs): time-averaged face vectors S̄_f = ½(S^n + S^{n+1}) and
swept volumes δV_f = S̄_f·δc_f — exact for linear node motion on any quad, so
Σ sign δV_f = V^{n+1} − V^n is algebra (1.8e-14 measured; the EndOfStep control
2.1e-3). CurvilinearPisoSolver::set_mesh(next) names the end-of-step geometry;
advance swaps it in, rebuilds operators + pressure matrix on it (L_f, LSQ
gradients, no mesh-velocity term in the projection), keeps the old mesh for
V^n and the explicit boundary data; predictor in the conservative ALE form
V^{n+1} û = V^n u^n + dt(−Σ sign (F − δV/dt) u_f + ν D + f V^n), written as
u·(V^n/V^{n+1}) + … so a stationary mesh is bitwise the static path; fluxes on
S̄_f; snapshot carries both meshes; swept_face_rule knob (Trapezoidal default,
EndOfStep = negative control). Stokes limit keeps the mesh flux (was dropped
with convection) and centres it (upwinding it cost an order: 1.06/1.00).

Gates (tests/curvilinear_ale.rs, 11 tests, 83 s): uniform flow on a wiggling
AND bending annulus 4.44e-15 over 400 steps, p exactly 0, 0 pressure
iterations; control deviates 4.9e-5; stationary mesh through the moving path
bit-identical (both diffusion variants); snapshot/restore on the moving mesh
bit-identical; Taylor–Green orders unchanged — upwind 0.995/0.976 vs fixed
0.987/0.975 at 1.05× error, Stokes 1.92/1.97 vs 1.94/1.99 at 2.4×, moving
annulus 2.11/2.02; linear-field falsifier 1.95/1.92 (annulus), 1.91/1.43
(square, sliding wall nodes). P0 ladders re-run identical to every digit.

Rule from the diagnosis: start a moving run ON the t = 0 mesh and sweep less
than a cell per step — a first step that jumped 2–4 cells imprinted an
O(displacement) error no refinement removed (dt- and motion-independent).

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-04 17:00:04 -07:00
co-authored by Claude Fable 5.1
parent c63d79c300
commit d46fb0b7a7
6 changed files with 1060 additions and 54 deletions
@@ -3,7 +3,7 @@
//! pressure-correction equation on the 9-point operator, and the flux and
//! velocity corrections.
use super::{CurvilinearPisoSolver, PatchField, SideBc};
use super::{CurvilinearPisoSolver, PatchField, SideBc, StepGeometry};
use crate::mesh::PatchSide;
use crate::solvers::incompressible::sparse_bicgstab::{
BicgstabResult, CsrMatrix, bicgstab_jacobi, project_mean,
@@ -48,8 +48,9 @@ impl CurvilinearPisoSolver {
})
}
/// `F* = interp(û)·S (dt/ρ) L_f(p^n)` on interior and outlet faces,
/// the prescribed flux on velocity faces (at `t_new`).
/// `F* = interp(û)·S̄ (dt/ρ) L_f(p^n)` on interior and outlet faces,
/// the prescribed flux `u_b · S̄` on velocity faces (at `t_new`, on the
/// end-of-step face centres). `S̄` is the step's face vector (`geo`).
pub(super) fn predicted_fluxes(
&self,
uh: &[f64],
@@ -57,6 +58,7 @@ impl CurvilinearPisoSolver {
p: &[f64],
dt: f64,
t_new: f64,
geo: &StepGeometry,
) -> Vec<f64> {
let mesh = &self.mesh;
let rho = self.config.density;
@@ -64,22 +66,25 @@ impl CurvilinearPisoSolver {
mesh.faces()
.iter()
.enumerate()
.map(|(f, face)| match (face.owner, face.neigh) {
(Some(o), Some(n)) => {
let w = face.w;
let uf = w * uh[o] + (1.0 - w) * uh[n];
let vf = w * vh[o] + (1.0 - w) * vh[n];
uf * face.s[0] + vf * face.s[1] - dt / rho * lp[f]
}
_ => {
let c = mesh.boundary_cell(f);
match self.params.boundaries.get(mesh.side(f).expect("boundary")) {
SideBc::Velocity => {
let (ub, vb) =
self.boundary_velocity(face.centre[0], face.centre[1], t_new);
ub * face.s[0] + vb * face.s[1]
.map(|(f, face)| {
let s = geo.s_bar[f];
match (face.owner, face.neigh) {
(Some(o), Some(n)) => {
let w = face.w;
let uf = w * uh[o] + (1.0 - w) * uh[n];
let vf = w * vh[o] + (1.0 - w) * vh[n];
uf * s[0] + vf * s[1] - dt / rho * lp[f]
}
_ => {
let c = mesh.boundary_cell(f);
match self.params.boundaries.get(mesh.side(f).expect("boundary")) {
SideBc::Velocity => {
let (ub, vb) =
self.boundary_velocity(face.centre[0], face.centre[1], t_new);
ub * s[0] + vb * s[1]
}
SideBc::Outlet => uh[c] * s[0] + vh[c] * s[1] - dt / rho * lp[f],
}
SideBc::Outlet => uh[c] * face.s[0] + vh[c] * face.s[1] - dt / rho * lp[f],
}
}
})