rtx-cfd: curvilinear collocated PISO on a structured patch (overset A-P0, WIP) — PatchMesh (right-handed s,n; periodic seam with shift; face metrics), patch generators (TFI, skewed annulus, sheared/varying-skew channels), CSR + Jacobi-BiCGSTAB, the Zang–Street–Koseff incremental step with the node-based 9-point L_f, LSQ gradients, explicit and line-implicit-n predictors, adjustPhi; tests: mesh metrics (5 green), operators exact on linear fields incl. the seam (green), sparse (2 green), MMS ladder (Cartesian 16/32: 1.37–1.39x the staggered error, order 0.83; n=64 stalls at a |du/dt| floor 2e-4 — open, tolerance-scaling hypothesis), annulus/Poiseuille not yet run
CI / Distributed Training Tests (push) Canceled after 0s
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 / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s

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 05:00:08 -07:00
co-authored by Claude Fable 5.1
parent 1347bc6772
commit 52da75a3a9
13 changed files with 2659 additions and 0 deletions
@@ -0,0 +1,223 @@
//! The pressure step: face fluxes from the predictor with the compact
//! pressure term, boundary-flux adjustment for closed patches, the
//! pressure-correction equation on the 9-point operator, and the flux and
//! velocity corrections.
use super::{CurvilinearPisoSolver, PatchField, SideBc};
use crate::mesh::PatchSide;
use crate::solvers::incompressible::sparse_bicgstab::{
BicgstabResult, CsrMatrix, bicgstab_jacobi, project_mean,
};
impl CurvilinearPisoSolver {
/// Node values of a pressure-like cell field: zero on outlet sides,
/// extrapolated elsewhere.
pub(super) fn pressure_nodes(&self, p: &[f64]) -> Vec<f64> {
let b = &self.params.boundaries;
self.ops
.node_values(&self.mesh, p, &|side: PatchSide, _| match b.get(side) {
SideBc::Outlet => Some(0.0),
SideBc::Velocity => None,
})
}
/// `L_f(p)` on every face (zero on Neumann faces, outlet value zero).
pub(super) fn pressure_face_gradients(&self, p: &[f64]) -> Vec<f64> {
let mesh = &self.mesh;
let pn = self.pressure_nodes(p);
(0..mesh.faces().len())
.map(|f| {
let bval = mesh
.side(f)
.and_then(|s| match self.params.boundaries.get(s) {
SideBc::Outlet => Some(0.0),
SideBc::Velocity => None,
});
self.ops.face_gradient_flux(mesh, f, p, &pn, bval)
})
.collect()
}
/// Least-squares cell gradient of a pressure-like field (outlet faces
/// at zero, Neumann faces left out).
pub(super) fn pressure_gradient(&self, p: &[f64], c: usize) -> [f64; 2] {
let mesh = &self.mesh;
self.ops.gradient(mesh, c, p, &|f| match mesh.side(f) {
Some(s) if self.params.boundaries.get(s) == SideBc::Outlet => Some(0.0),
_ => None,
})
}
/// `F* = interp(û)·S (dt/ρ) L_f(p^n)` on interior and outlet faces,
/// the prescribed flux on velocity faces (at `t_new`).
pub(super) fn predicted_fluxes(
&self,
uh: &[f64],
vh: &[f64],
p: &[f64],
dt: f64,
t_new: f64,
) -> Vec<f64> {
let mesh = &self.mesh;
let rho = self.config.density;
let lp = self.pressure_face_gradients(p);
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]
}
SideBc::Outlet => uh[c] * face.s[0] + vh[c] * face.s[1] - dt / rho * lp[f],
}
}
})
.collect()
}
/// On a patch with no outlet the prescribed boundary fluxes must sum
/// to zero for the projection to be solvable; the O(h²) defect of
/// face-centre sampling is spread over the velocity faces by area
/// (OpenFOAM's `adjustPhi`). Returns the defect removed.
pub(super) fn adjust_boundary_flux(&self, flux: &mut [f64]) -> f64 {
let mesh = &self.mesh;
let has_outlet = [
PatchSide::Inner,
PatchSide::Outer,
PatchSide::SStart,
PatchSide::SEnd,
]
.iter()
.any(|&s| self.params.boundaries.get(s) == SideBc::Outlet);
if has_outlet {
return 0.0;
}
let (mut net, mut total_len) = (0.0, 0.0);
for (f, face) in mesh.faces().iter().enumerate() {
if mesh.side(f).is_some() {
let out_sign = if face.owner.is_some() { 1.0 } else { -1.0 };
net += out_sign * flux[f];
total_len += (face.s[0] * face.s[0] + face.s[1] * face.s[1]).sqrt();
}
}
if total_len == 0.0 {
return net;
}
for (f, face) in mesh.faces().iter().enumerate() {
if mesh.side(f).is_some() {
let out_sign = if face.owner.is_some() { 1.0 } else { -1.0 };
let len = (face.s[0] * face.s[0] + face.s[1] * face.s[1]).sqrt();
flux[f] -= out_sign * net * len / total_len;
}
}
net
}
/// Assemble `−Σ_f sign (dt/ρ) L_f` (positive diagonal) and pick the
/// anchor for the pure-Neumann case.
pub(super) fn assemble_pressure_matrix(&self, dt: f64) -> (CsrMatrix, Option<usize>) {
let mesh = &self.mesh;
let rho = self.config.density;
let n = mesh.cell_count();
let mut tri = Vec::with_capacity(n * 12);
let mut coefs = Vec::new();
let mut any_dirichlet = false;
for c in 0..n {
for (f, sign) in mesh.cell_faces(c) {
self.ops
.face_gradient_coeffs(mesh, &self.params.boundaries, f, &mut coefs);
if mesh.side(f).is_some() && !coefs.is_empty() {
any_dirichlet = true;
}
for &(col, v) in &coefs {
tri.push((c, col, -sign * dt / rho * v));
}
}
tri.push((c, c, 0.0)); // guarantee a diagonal entry
}
let mut a = CsrMatrix::from_triplets(n, &tri);
let anchor = if any_dirichlet {
None
} else {
// An interior cell away from the seam: (1, 1).
let a_cell = mesh.cell(1.min(mesh.nn() - 1), 1.min(mesh.ns() - 1));
a.set_row_identity(a_cell);
Some(a_cell)
};
(a, anchor)
}
/// Solve `−Σ sign (dt/ρ) L_f(p') = −Σ sign F` for `p'` (zero start).
pub(super) fn solve_pressure_correction(
&self,
matrix: &CsrMatrix,
anchor: Option<usize>,
flux: &[f64],
tolerance: f64,
) -> (Vec<f64>, BicgstabResult) {
let mesh = &self.mesh;
let n = mesh.cell_count();
let mut rhs = vec![0.0; n];
for c in 0..n {
let mut div = 0.0;
for (f, sign) in mesh.cell_faces(c) {
div += sign * flux[f];
}
rhs[c] = -div;
}
if let Some(a) = anchor {
project_mean(&mut rhs);
rhs[a] = 0.0;
}
let mut pc = vec![0.0; n];
let out = bicgstab_jacobi(
matrix,
&rhs,
&mut pc,
tolerance,
self.params.max_poisson_iterations,
);
(pc, out)
}
/// `F = (dt/ρ) L_f(p')`, `u = (dt/ρ) ∇p'`, `p += p'`.
pub(super) fn apply_correction(&self, field: &mut PatchField, pc: &[f64], dt: f64) {
let mesh = &self.mesh;
let rho = self.config.density;
let lp = self.pressure_face_gradients(pc);
for f in 0..mesh.faces().len() {
field.flux[f] -= dt / rho * lp[f];
}
for c in 0..mesh.cell_count() {
let g = self.pressure_gradient(pc, c);
field.u[c] -= dt / rho * g[0];
field.v[c] -= dt / rho * g[1];
field.p[c] += pc[c];
}
}
/// Largest cell mass imbalance `|Σ sign F_f|`.
pub(super) fn max_divergence(&self, flux: &[f64]) -> f64 {
let mesh = &self.mesh;
(0..mesh.cell_count())
.map(|c| {
mesh.cell_faces(c)
.iter()
.map(|&(f, sign)| sign * flux[f])
.sum::<f64>()
.abs()
})
.fold(0.0, f64::max)
}
}