Files
rustytorch/crates/specialized/rtx-cfd/src/solvers/incompressible/curvilinear/projection.rs
T
Omar SobhandClaude Fable 5.1 172a26dee4
CI / Format Check (push) Failing after 6s
CI / Build (ubuntu-latest) (push) Failing after 5s
CI / Clippy Check (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
Performance Benchmarks / Run Benchmarks (push) Failing after 27s
CI / Build CPU-Only (Explicit) (push) Failing after 1m24s
Documentation / Build API Documentation (push) Failing after 1m34s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (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
PERF-2 P0: intra-step profiler — StepTimers on the overset solver (RTX_PROFILE; overlap build / predictors / patch BiCGSTAB / background Poisson with its setup-vs-iterate split / round exchange / apply / end exchange), PoissonSolution carries setup_ns and iterate_ns, the harness times advance / restore / snapshot / load sampling / force / FEA predictor and prints the wall split of the coupled phase; no clock is read when profiling is off; the reclassified/step progress denominator fixed (was ×4)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
2026-09-15 22:25:52 -05:00

324 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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, PressureSystem, SideBc, StepGeometry};
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 `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],
vh: &[f64],
p: &[f64],
dt: f64,
t_new: f64,
geo: &StepGeometry,
) -> 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)| {
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);
let side = mesh.side(f).expect("boundary");
if side == PatchSide::Outer && self.acceptors.is_some() {
// The acceptor ring's outer faces: the stamped
// velocity's own flux (read only by the overlap
// mass-defect measure).
return uh[c] * s[0] + vh[c] * s[1];
}
match self.params.boundaries.get(side) {
SideBc::Velocity => {
let (ub, vb) = self.boundary_velocity(
side,
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],
}
}
}
})
.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);
// A Robin wall absorbs the net flux through its compliance (the
// pressure system is then not pure Neumann).
if has_outlet || self.acceptors.is_some() || self.robin.is_some() {
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. Acceptor cells get identity rows
/// (their `p'` is Dirichlet) and the interior rows' couplings to them
/// are recorded as links, eliminated at solve time.
pub(super) fn assemble_pressure_matrix(&self, dt: f64) -> PressureSystem {
let mesh = &self.mesh;
let rho = self.config.density;
let n = mesh.cell_count();
let mut tri = Vec::with_capacity(n * 12);
let mut links = Vec::new();
let mut coefs = Vec::new();
let mut any_dirichlet = self.acceptors.is_some();
for c in 0..n {
if self.is_acceptor(c) {
tri.push((c, c, 1.0));
continue;
}
for (f, sign) in mesh.cell_faces(c) {
if let Some(w) = &self.robin {
if mesh.side(f) == Some(PatchSide::Inner) {
// The compliant wall: outward flux `+|S| p'_c / alpha`.
let sv = mesh.faces()[f].s;
let len = (sv[0] * sv[0] + sv[1] * sv[1]).sqrt();
tri.push((c, c, len / w.alpha));
any_dirichlet = true;
}
}
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 {
let coef = -sign * dt / rho * v;
if self.is_acceptor(col) {
links.push((c, col, coef));
} else {
tri.push((c, col, coef));
}
}
}
tri.push((c, c, 0.0)); // guarantee a diagonal entry
}
let mut matrix = 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));
matrix.set_row_identity(a_cell);
Some(a_cell)
};
PressureSystem {
dt,
matrix,
anchor,
links,
}
}
/// The right-hand side `−Σ sign F` on the equation-carrying cells, with
/// the acceptor couplings eliminated (`rhs = coef · p'_acceptor`) and
/// zero on acceptor rows; mean-projected and anchored when pure Neumann.
/// Also returns its L1 norm BEFORE the projection (the incoming
/// imbalance the stop is relative to — the static path's
/// `divergence_l1`, unchanged to the bit).
pub(super) fn pressure_rhs(&self, system: &PressureSystem, flux: &[f64]) -> (Vec<f64>, f64) {
let mesh = &self.mesh;
let n = mesh.cell_count();
let mut rhs = vec![0.0; n];
for c in 0..n {
if self.is_acceptor(c) {
continue;
}
let mut div = 0.0;
for (f, sign) in mesh.cell_faces(c) {
div += sign * flux[f];
}
rhs[c] = -div;
}
for &(row, acc, coef) in &system.links {
rhs[row] -= coef * self.acceptor_correction(acc).unwrap_or(0.0);
}
let incoming: f64 = rhs.iter().map(|r| r.abs()).sum();
if let Some(a) = system.anchor {
project_mean(&mut rhs);
rhs[a] = 0.0;
}
(rhs, incoming)
}
/// Solve the assembled system for `p'` (zero start); acceptor entries
/// are then set to their Dirichlet values.
pub(super) fn solve_pressure_correction(
&self,
system: &PressureSystem,
rhs: Vec<f64>,
tolerance: f64,
) -> (Vec<f64>, BicgstabResult) {
let n = self.mesh.cell_count();
let mut pc = vec![0.0; n];
let out = bicgstab_jacobi(
&system.matrix,
&rhs,
&mut pc,
tolerance,
self.params.max_poisson_iterations,
);
if self.acceptors.is_some() {
for c in 0..n {
if let Some(v) = self.acceptor_correction(c) {
pc[c] = v;
}
}
}
(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];
}
if let Some(w) = &self.robin {
// The compliant wall's flux answer: `δu_b = p' S / (alpha |S|)`,
// `δF = δu_b · S = p' |S| / alpha` in the face's own orientation.
for &f in &self.robin_faces {
let c = mesh.boundary_cell(f);
let sv = mesh.faces()[f].s;
let len = (sv[0] * sv[0] + sv[1] * sv[1]).sqrt();
field.flux[f] -= pc[c] * len / w.alpha;
}
}
for c in 0..mesh.cell_count() {
if self.is_acceptor(c) {
continue;
}
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];
}
}
/// Total cell mass imbalance `Σ_c |Σ_f sign F_f|` over the cells that
/// carry continuity (acceptors excluded).
pub(super) fn divergence_l1(&self, flux: &[f64]) -> f64 {
let mesh = &self.mesh;
(0..mesh.cell_count())
.filter(|&c| !self.is_acceptor(c))
.map(|c| {
mesh.cell_faces(c)
.iter()
.map(|&(f, sign)| sign * flux[f])
.sum::<f64>()
.abs()
})
.sum()
}
/// Largest cell mass imbalance `|Σ sign F_f|` over the cells that
/// carry continuity (acceptors excluded).
pub(super) fn max_divergence(&self, flux: &[f64]) -> f64 {
let mesh = &self.mesh;
(0..mesh.cell_count())
.filter(|&c| !self.is_acceptor(c))
.map(|c| {
mesh.cell_faces(c)
.iter()
.map(|&(f, sign)| sign * flux[f])
.sum::<f64>()
.abs()
})
.fold(0.0, f64::max)
}
}