rtx-cfd: overset A-P2 — the patch overlaps the background (OversetPisoSolver), gated S1–S5
CI / Test (macos-latest) (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 (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
Documentation / Build User Guide (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
CI / Test (macos-latest) (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 (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
Documentation / Build User Guide (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
Background = the embedded solver with a mask from the overlap classification
(embedded/{mod,projection}.rs: module split, projection's solve/apply halves,
set_overlap, fringe p' Dirichlet by elimination into extra_diag/rhs, anchor
dropped, set_inner_stop_factor, phase API begin_step/solve_correction/
apply_correction/end_step; advance rebuilt on the phases — every suite digit-
identical, FSI2 default line-for-line). Patch = the curvilinear solver with an
acceptor ring (set_side_velocity; set_acceptor_ring/stamp_acceptors/
set_acceptor_correction; acceptor Dirichlet by elimination into
PressureSystem.links so the BiCGSTAB stop stays in flux units — identity rows
measured unconverged at 2431 iterations; same phase API). overset/overlap.rs:
OverlapMap — hole/fringe/active from the patch's own indices (hole = body or
k <= nn-1-overlap_rows, DEFAULT_OVERLAP_ROWS = 4 from the 2.9 h depth budget),
dual-quad inverse-bilinear donors patch→fringe, lattice donors →acceptors,
both invariants asserted, mass-defect measures. overset/mod.rs:
OversetPisoSolver — advance (exchange rebuilt BEFORE the predictors from the
previous corrected field), alternating Schwarz on the acceptor p' vector with
Anderson(3) (plain Schwarz measured 0.82/round: floating patch, Neumann wall)
and the previous step's vector as warm start (1 round/corrector at steady
state), stop relative to the STEP's p' scale (the MG absolute stop is
1e-9/dt² in pressure — the whole second correction), set_patch_mesh,
snapshot/restore carrying the warm-start vector.
Gates: overlap linear-exact 1e-13, quadratic orders 1.96/1.99 (acceptors),
1.40/1.91 (fringe); half-couplings: patch with exact acceptors Stokes 2.07/1.98
+ 2.08/1.98, upwind 0.84/0.84, background with exact fringe 7.86e-3/2.90e-3/
1.09e-3 (1.44/1.41); two-mesh MMS n=32/64: background 8.717e-3/4.207e-3 (1.03x/
0.97x the embedded circle), patch 1.322e-2/6.904e-3 (1.5-1.6x), orders 1.05/
0.94, patch div <= 5e-13, overlap mass defect 3.6e-3 -> 8.2e-4 of the overlap
flux (under the registered 1e-3 from n=64; disclosed at 32); motion: stationary
patch through set_patch_mesh bit-identical, snapshot/restore with a pending mesh
bit-identical, translating phantom circle 1.22x/1.19x the static level over
4.5 cells. Inherited, disclosed: poisson_equivalence's no-body multigrid pin
fails by 3.9e-9 at d46fb0b (M1's commit; verified in a clean worktree).
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
d46fb0b7a7
commit
afd1bff6ee
@@ -197,6 +197,16 @@ pub struct CurvilinearSolverState {
|
||||
|
||||
type VelocityFn = Box<dyn Fn(f64, f64, f64) -> (f64, f64) + Send + Sync>;
|
||||
|
||||
/// The acceptor ring of an overset patch (A-P2): the outer row of cells
|
||||
/// (`k = nn − 1`) carries values stamped from the background — `u, v, p`
|
||||
/// at the end of every step, the pressure correction `p'` during each
|
||||
/// projection — and no momentum or continuity equation of its own.
|
||||
#[derive(Debug, Clone)]
|
||||
struct AcceptorRing {
|
||||
/// Dirichlet `p'` per acceptor cell (column order), for the next solve.
|
||||
correction: Vec<f64>,
|
||||
}
|
||||
|
||||
/// PISO on a curvilinear patch.
|
||||
pub struct CurvilinearPisoSolver {
|
||||
config: CfdConfig,
|
||||
@@ -206,9 +216,35 @@ pub struct CurvilinearPisoSolver {
|
||||
pending: Option<PatchMesh>,
|
||||
ops: Operators,
|
||||
boundary_velocity: Option<VelocityFn>,
|
||||
/// Per-side overrides of `boundary_velocity` (Inner, Outer, SStart, SEnd).
|
||||
side_velocity: [Option<VelocityFn>; 4],
|
||||
momentum_source: Option<VelocityFn>,
|
||||
acceptors: Option<AcceptorRing>,
|
||||
time: f64,
|
||||
matrix: Option<(f64, CsrMatrix, Option<usize>)>,
|
||||
matrix: Option<PressureSystem>,
|
||||
}
|
||||
|
||||
/// The assembled pressure-correction system for one `dt` and geometry.
|
||||
pub(crate) struct PressureSystem {
|
||||
/// The `dt` it was assembled for.
|
||||
pub(crate) dt: f64,
|
||||
/// `−Σ_f sign (dt/ρ) L_f` with identity rows on acceptor cells.
|
||||
pub(crate) matrix: CsrMatrix,
|
||||
/// The anchored cell of a pure-Neumann patch.
|
||||
pub(crate) anchor: Option<usize>,
|
||||
/// `(row, acceptor cell, coefficient)`: the interior rows' couplings to
|
||||
/// acceptor cells, eliminated to the right-hand side at solve time so
|
||||
/// the residual stays in flux units.
|
||||
pub(crate) links: Vec<(usize, usize, f64)>,
|
||||
}
|
||||
|
||||
fn side_index(side: PatchSide) -> usize {
|
||||
match side {
|
||||
PatchSide::Inner => 0,
|
||||
PatchSide::Outer => 1,
|
||||
PatchSide::SStart => 2,
|
||||
PatchSide::SEnd => 3,
|
||||
}
|
||||
}
|
||||
|
||||
impl CurvilinearPisoSolver {
|
||||
@@ -226,7 +262,9 @@ impl CurvilinearPisoSolver {
|
||||
pending: None,
|
||||
ops,
|
||||
boundary_velocity: None,
|
||||
side_velocity: [None, None, None, None],
|
||||
momentum_source: None,
|
||||
acceptors: None,
|
||||
time: 0.0,
|
||||
matrix: None,
|
||||
})
|
||||
@@ -239,6 +277,118 @@ impl CurvilinearPisoSolver {
|
||||
{
|
||||
self.boundary_velocity = Some(Box::new(f));
|
||||
}
|
||||
/// Velocity on ONE `Velocity` side, overriding [`Self::set_boundary_velocity`]
|
||||
/// there (the overset patch: the wall on `Inner`, the background on
|
||||
/// `Outer`).
|
||||
pub fn set_side_velocity<F>(&mut self, side: PatchSide, f: F)
|
||||
where
|
||||
F: Fn(f64, f64, f64) -> (f64, f64) + Send + Sync + 'static,
|
||||
{
|
||||
self.side_velocity[side_index(side)] = Some(Box::new(f));
|
||||
}
|
||||
/// Turn the outer row of cells into acceptors (see `AcceptorRing`) or
|
||||
/// back into ordinary cells. The pressure matrix is rebuilt.
|
||||
pub fn set_acceptor_ring(&mut self, on: bool) {
|
||||
self.acceptors = on.then(|| AcceptorRing {
|
||||
correction: vec![0.0; self.mesh.ns()],
|
||||
});
|
||||
self.matrix = None;
|
||||
}
|
||||
/// Whether the outer row is an acceptor ring.
|
||||
pub fn has_acceptor_ring(&self) -> bool {
|
||||
self.acceptors.is_some()
|
||||
}
|
||||
/// Is cell `c` an acceptor (no equation of its own)?
|
||||
pub fn is_acceptor(&self, c: usize) -> bool {
|
||||
self.acceptors.is_some() && self.mesh.cell_ki(c).0 == self.mesh.nn() - 1
|
||||
}
|
||||
/// Stamp `(u, v, p)` onto the acceptor cells, column order `i = 0..ns`.
|
||||
pub fn stamp_acceptors(&self, field: &mut PatchField, values: &[(f64, f64, f64)]) {
|
||||
let nn = self.mesh.nn();
|
||||
for (i, &(u, v, p)) in values.iter().enumerate() {
|
||||
let c = self.mesh.cell(nn - 1, i);
|
||||
field.u[c] = u;
|
||||
field.v[c] = v;
|
||||
field.p[c] = p;
|
||||
}
|
||||
}
|
||||
/// The Dirichlet `p'` of the acceptor cells for the next
|
||||
/// [`Self::solve_correction`] (column order).
|
||||
pub fn set_acceptor_correction(&mut self, values: &[f64]) {
|
||||
if let Some(ring) = &mut self.acceptors {
|
||||
ring.correction.clear();
|
||||
ring.correction.extend_from_slice(values);
|
||||
}
|
||||
}
|
||||
/// One pressure-correction SOLVE (no application): `p'` on every cell
|
||||
/// (acceptor rows hold their Dirichlet values), with the BiCGSTAB
|
||||
/// report. `None` when the incoming divergence is already at the
|
||||
/// rounding floor. For the overset's Schwarz rounds.
|
||||
pub(crate) fn solve_correction(
|
||||
&self,
|
||||
field: &PatchField,
|
||||
dt: f64,
|
||||
) -> Option<(
|
||||
Vec<f64>,
|
||||
crate::solvers::incompressible::sparse_bicgstab::BicgstabResult,
|
||||
)> {
|
||||
let system = self
|
||||
.matrix
|
||||
.as_ref()
|
||||
.expect("begin_step assembled the matrix");
|
||||
debug_assert_eq!(system.dt, dt);
|
||||
let flux_scale: f64 = field.flux.iter().map(|f| f.abs()).sum::<f64>().max(1e-300);
|
||||
let floor = 1e-15 * flux_scale;
|
||||
let (rhs, incoming) = self.pressure_rhs(system, &field.flux);
|
||||
if incoming <= floor {
|
||||
return None;
|
||||
}
|
||||
let tolerance = self.params.tolerance * incoming + floor;
|
||||
Some(self.solve_pressure_correction(system, rhs, tolerance))
|
||||
}
|
||||
/// Apply a correction `pc` (from [`Self::solve_correction`]).
|
||||
pub(crate) fn apply_correction_pub(&self, field: &mut PatchField, pc: &[f64], dt: f64) {
|
||||
self.apply_correction(field, pc, dt);
|
||||
}
|
||||
/// Largest cell mass imbalance over the equation-carrying cells.
|
||||
pub(crate) fn max_divergence_pub(&self, flux: &[f64]) -> f64 {
|
||||
self.max_divergence(flux)
|
||||
}
|
||||
/// Overlap mass defect on the patch side: `Σ_acceptors |Σ_f sign F_f|`
|
||||
/// (the acceptors carry no continuity) with the OUTER face flux taken
|
||||
/// from `outer_velocity` (the background's velocity at that face
|
||||
/// centre, column order), and the flux scale `Σ |F_f|` over the faces
|
||||
/// between the acceptor ring and the interior.
|
||||
pub fn acceptor_mass_defect(
|
||||
&self,
|
||||
field: &PatchField,
|
||||
outer_velocity: &[(f64, f64)],
|
||||
) -> (f64, f64) {
|
||||
let mesh = &self.mesh;
|
||||
let nn = mesh.nn();
|
||||
if self.acceptors.is_none() || nn < 2 {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let mut defect = 0.0;
|
||||
let mut scale = 0.0;
|
||||
for i in 0..mesh.ns() {
|
||||
let c = mesh.cell(nn - 1, i);
|
||||
let outer = mesh.nface(nn, i);
|
||||
let mut div = 0.0;
|
||||
for (f, sign) in mesh.cell_faces(c) {
|
||||
if f == outer {
|
||||
let s = mesh.faces()[f].s;
|
||||
let (uo, vo) = outer_velocity.get(i).copied().unwrap_or((0.0, 0.0));
|
||||
div += sign * (uo * s[0] + vo * s[1]);
|
||||
} else {
|
||||
div += sign * field.flux[f];
|
||||
}
|
||||
}
|
||||
defect += div.abs();
|
||||
scale += field.flux[mesh.nface(nn - 1, i)].abs();
|
||||
}
|
||||
(defect, scale)
|
||||
}
|
||||
/// Body force per unit volume, `(x, y, t) -> (fx, fy)`.
|
||||
pub fn set_momentum_source<F>(&mut self, f: F)
|
||||
where
|
||||
@@ -314,11 +464,18 @@ impl CurvilinearPisoSolver {
|
||||
self.matrix = None;
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_velocity(&self, x: f64, y: f64, t: f64) -> (f64, f64) {
|
||||
self.boundary_velocity
|
||||
pub(crate) fn boundary_velocity(&self, side: PatchSide, x: f64, y: f64, t: f64) -> (f64, f64) {
|
||||
self.side_velocity[side_index(side)]
|
||||
.as_ref()
|
||||
.or(self.boundary_velocity.as_ref())
|
||||
.map_or((0.0, 0.0), |f| f(x, y, t))
|
||||
}
|
||||
/// Dirichlet `p'` of acceptor cell `c`, if it is one.
|
||||
pub(crate) fn acceptor_correction(&self, c: usize) -> Option<f64> {
|
||||
let ring = self.acceptors.as_ref()?;
|
||||
let (k, i) = self.mesh.cell_ki(c);
|
||||
(k == self.mesh.nn() - 1).then(|| ring.correction.get(i).copied().unwrap_or(0.0))
|
||||
}
|
||||
pub(crate) fn source_at(&self, xy: [f64; 2], t: f64) -> (f64, f64) {
|
||||
self.momentum_source
|
||||
.as_ref()
|
||||
@@ -350,12 +507,37 @@ impl CurvilinearPisoSolver {
|
||||
);
|
||||
}
|
||||
|
||||
/// Advance one step of `dt`.
|
||||
/// Advance one step of `dt`: [`Self::begin_step`], the correctors,
|
||||
/// [`Self::end_step`].
|
||||
pub async fn advance(
|
||||
&mut self,
|
||||
field: &mut PatchField,
|
||||
dt: f64,
|
||||
) -> CfdResult<CurvilinearResult> {
|
||||
let start = self.begin_step(field, dt)?;
|
||||
let mut iterations = 0;
|
||||
let mut converged = true;
|
||||
let mut performed = 0;
|
||||
let mut max_div = self.max_divergence(&field.flux);
|
||||
for _ in 0..self.params.corrector_steps {
|
||||
let Some((pc, out)) = self.solve_correction(field, dt) else {
|
||||
break;
|
||||
};
|
||||
iterations += out.iterations;
|
||||
converged &= out.converged;
|
||||
self.apply_correction(field, &pc, dt);
|
||||
performed += 1;
|
||||
max_div = self.max_divergence(&field.flux);
|
||||
}
|
||||
Ok(self.end_step(&start, performed, max_div, iterations, converged))
|
||||
}
|
||||
|
||||
/// Everything before the correctors: the mesh swap (if `set_mesh` named
|
||||
/// one), the step geometry, the predictor, the predicted fluxes with
|
||||
/// the closed-patch adjustment, `u*`, and the pressure matrix on the
|
||||
/// end-of-step geometry. The overset coupling runs this on the patch,
|
||||
/// then drives the correctors itself.
|
||||
pub(crate) fn begin_step(&mut self, field: &mut PatchField, dt: f64) -> CfdResult<StepStart> {
|
||||
let t_old = self.time;
|
||||
let t_new = t_old + dt;
|
||||
let rho = self.config.density;
|
||||
@@ -383,44 +565,47 @@ impl CurvilinearPisoSolver {
|
||||
let mut flux = self.predicted_fluxes(&uh, &vh, &field.p, dt, t_new, &geo);
|
||||
let adjustment = self.adjust_boundary_flux(&mut flux);
|
||||
for c in 0..mesh.cell_count() {
|
||||
if self.is_acceptor(c) {
|
||||
continue;
|
||||
}
|
||||
let g = self.pressure_gradient(&field.p, c);
|
||||
field.u[c] = uh[c] - dt / rho * g[0];
|
||||
field.v[c] = vh[c] - dt / rho * g[1];
|
||||
}
|
||||
field.flux = flux;
|
||||
|
||||
if self.matrix.as_ref().is_none_or(|(d, _, _)| *d != dt) {
|
||||
let (m, anchor) = self.assemble_pressure_matrix(dt);
|
||||
self.matrix = Some((dt, m, anchor));
|
||||
if self.matrix.as_ref().is_none_or(|s| s.dt != dt) {
|
||||
self.matrix = Some(self.assemble_pressure_matrix(dt));
|
||||
}
|
||||
let (_, matrix, anchor) = self.matrix.as_ref().expect("assembled");
|
||||
let flux_scale: f64 = field.flux.iter().map(|f| f.abs()).sum::<f64>().max(1e-300);
|
||||
let floor = 1e-15 * flux_scale;
|
||||
Ok(StepStart { t_new, adjustment })
|
||||
}
|
||||
|
||||
let mut iterations = 0;
|
||||
let mut converged = true;
|
||||
let mut performed = 0;
|
||||
let mut max_div = self.max_divergence(&field.flux);
|
||||
for _ in 0..self.params.corrector_steps {
|
||||
let incoming = self.divergence_l1(&field.flux);
|
||||
if incoming <= floor {
|
||||
break;
|
||||
}
|
||||
let tolerance = self.params.tolerance * incoming + floor;
|
||||
let (pc, out) = self.solve_pressure_correction(matrix, *anchor, &field.flux, tolerance);
|
||||
iterations += out.iterations;
|
||||
converged &= out.converged;
|
||||
self.apply_correction(field, &pc, dt);
|
||||
performed += 1;
|
||||
max_div = self.max_divergence(&field.flux);
|
||||
}
|
||||
self.time = t_new;
|
||||
Ok(CurvilinearResult {
|
||||
/// Everything after the correctors: the clock and the result.
|
||||
pub(crate) fn end_step(
|
||||
&mut self,
|
||||
start: &StepStart,
|
||||
performed: usize,
|
||||
max_div: f64,
|
||||
iterations: usize,
|
||||
converged: bool,
|
||||
) -> CurvilinearResult {
|
||||
self.time = start.t_new;
|
||||
CurvilinearResult {
|
||||
corrector_steps_performed: performed,
|
||||
max_divergence: max_div,
|
||||
poisson_iterations: iterations,
|
||||
poisson_converged: converged,
|
||||
boundary_flux_adjustment: adjustment,
|
||||
})
|
||||
boundary_flux_adjustment: start.adjustment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`CurvilinearPisoSolver::begin_step`] hands to
|
||||
/// [`CurvilinearPisoSolver::end_step`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct StepStart {
|
||||
/// End-of-step time.
|
||||
pub(crate) t_new: f64,
|
||||
/// Boundary-flux defect removed on a closed patch.
|
||||
pub(crate) adjustment: f64,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user