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,
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ impl CurvilinearPisoSolver {
|
||||
|
||||
let bvel = |side: PatchSide, xy: [f64; 2]| -> Option<(f64, f64)> {
|
||||
match self.params.boundaries.get(side) {
|
||||
SideBc::Velocity => Some(self.boundary_velocity(xy[0], xy[1], t_old)),
|
||||
SideBc::Velocity => Some(self.boundary_velocity(side, xy[0], xy[1], t_old)),
|
||||
SideBc::Outlet => None,
|
||||
}
|
||||
};
|
||||
@@ -49,6 +49,12 @@ impl CurvilinearPisoSolver {
|
||||
let mut uh = vec![0.0; n];
|
||||
let mut vh = vec![0.0; n];
|
||||
for c in 0..n {
|
||||
if self.is_acceptor(c) {
|
||||
// Acceptors carry the stamped velocity; no momentum equation.
|
||||
uh[c] = field.u[c];
|
||||
vh[c] = field.v[c];
|
||||
continue;
|
||||
}
|
||||
let (mut cu, mut cv, mut du, mut dv) = (0.0, 0.0, 0.0, 0.0);
|
||||
for (f, sign) in mesh.cell_faces(c) {
|
||||
let face = &mesh.faces()[f];
|
||||
@@ -165,6 +171,7 @@ impl CurvilinearPisoSolver {
|
||||
let mut upper = vec![0.0; nn];
|
||||
let mut ru = vec![0.0; nn];
|
||||
let mut rv = vec![0.0; nn];
|
||||
let acceptor_row = self.acceptors.as_ref().map(|_| nn - 1);
|
||||
for i in 0..ns {
|
||||
for k in 0..nn {
|
||||
let c = mesh.cell(k, i);
|
||||
@@ -173,6 +180,13 @@ impl CurvilinearPisoSolver {
|
||||
let (mut lo, mut up) = (0.0, 0.0);
|
||||
ru[k] = uh[c];
|
||||
rv[k] = vh[c];
|
||||
if acceptor_row == Some(k) {
|
||||
// Acceptor: identity row, keeps its stamped value.
|
||||
lower[k] = 0.0;
|
||||
diag[k] = 1.0;
|
||||
upper[k] = 0.0;
|
||||
continue;
|
||||
}
|
||||
for (f, sign) in [(mesh.nface(k, i), -1.0), (mesh.nface(k + 1, i), 1.0)] {
|
||||
let alpha = self.ops.face(f).alpha;
|
||||
let coef = dt * nu * alpha / a;
|
||||
@@ -188,8 +202,12 @@ impl CurvilinearPisoSolver {
|
||||
Some(s) => match self.params.boundaries.get(s) {
|
||||
SideBc::Velocity => {
|
||||
let face = &old.faces()[f];
|
||||
let b =
|
||||
self.boundary_velocity(face.centre[0], face.centre[1], t_old);
|
||||
let b = self.boundary_velocity(
|
||||
s,
|
||||
face.centre[0],
|
||||
face.centre[1],
|
||||
t_old,
|
||||
);
|
||||
d += coef;
|
||||
ru[k] += coef * b.0;
|
||||
rv[k] += coef * b.1;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! pressure-correction equation on the 9-point operator, and the flux and
|
||||
//! velocity corrections.
|
||||
|
||||
use super::{CurvilinearPisoSolver, PatchField, SideBc, StepGeometry};
|
||||
use super::{CurvilinearPisoSolver, PatchField, PressureSystem, SideBc, StepGeometry};
|
||||
use crate::mesh::PatchSide;
|
||||
use crate::solvers::incompressible::sparse_bicgstab::{
|
||||
BicgstabResult, CsrMatrix, bicgstab_jacobi, project_mean,
|
||||
@@ -77,10 +77,21 @@ impl CurvilinearPisoSolver {
|
||||
}
|
||||
_ => {
|
||||
let c = mesh.boundary_cell(f);
|
||||
match self.params.boundaries.get(mesh.side(f).expect("boundary")) {
|
||||
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(face.centre[0], face.centre[1], t_new);
|
||||
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],
|
||||
@@ -105,7 +116,7 @@ impl CurvilinearPisoSolver {
|
||||
]
|
||||
.iter()
|
||||
.any(|&s| self.params.boundaries.get(s) == SideBc::Outlet);
|
||||
if has_outlet {
|
||||
if has_outlet || self.acceptors.is_some() {
|
||||
return 0.0;
|
||||
}
|
||||
let (mut net, mut total_len) = (0.0, 0.0);
|
||||
@@ -130,15 +141,22 @@ impl CurvilinearPisoSolver {
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
/// 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 = false;
|
||||
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) {
|
||||
self.ops
|
||||
.face_gradient_coeffs(mesh, &self.params.boundaries, f, &mut coefs);
|
||||
@@ -146,53 +164,88 @@ impl CurvilinearPisoSolver {
|
||||
any_dirichlet = true;
|
||||
}
|
||||
for &(col, v) in &coefs {
|
||||
tri.push((c, col, -sign * dt / rho * v));
|
||||
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 a = CsrMatrix::from_triplets(n, &tri);
|
||||
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));
|
||||
a.set_row_identity(a_cell);
|
||||
matrix.set_row_identity(a_cell);
|
||||
Some(a_cell)
|
||||
};
|
||||
(a, anchor)
|
||||
PressureSystem {
|
||||
dt,
|
||||
matrix,
|
||||
anchor,
|
||||
links,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
/// 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;
|
||||
}
|
||||
if let Some(a) = anchor {
|
||||
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(
|
||||
matrix,
|
||||
&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)
|
||||
}
|
||||
|
||||
@@ -205,6 +258,9 @@ impl CurvilinearPisoSolver {
|
||||
field.flux[f] -= dt / rho * lp[f];
|
||||
}
|
||||
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];
|
||||
@@ -212,10 +268,12 @@ impl CurvilinearPisoSolver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Total cell mass imbalance `Σ_c |Σ_f sign F_f|`.
|
||||
/// 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()
|
||||
@@ -226,10 +284,12 @@ impl CurvilinearPisoSolver {
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Largest cell mass imbalance `|Σ sign F_f|`.
|
||||
/// 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()
|
||||
|
||||
Reference in New Issue
Block a user