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()
|
||||
|
||||
+155
-424
@@ -36,6 +36,8 @@
|
||||
//! history the explicit predictor differentiates. Only the first step
|
||||
//! stamps `t = 0` data, via [`EmbeddedPisoSolver::initialize`] or lazily.
|
||||
|
||||
mod projection;
|
||||
|
||||
use super::ale::{AleBoundaries, SideBoundary};
|
||||
use super::embedded_body::{EmbeddedBody, EmbeddedMask, FaceKind};
|
||||
use super::poisson::{
|
||||
@@ -98,11 +100,13 @@ impl Default for EmbeddedParameters {
|
||||
|
||||
/// A snapshot of [`EmbeddedPisoSolver`]'s per-step state, for re-running a
|
||||
/// step within a coupling subiteration. See [`EmbeddedPisoSolver::snapshot`].
|
||||
#[derive(Clone)]
|
||||
pub struct EmbeddedSolverState {
|
||||
mask: Option<EmbeddedMask>,
|
||||
time: f64,
|
||||
initialized: bool,
|
||||
alpha: Option<Vec<f64>>,
|
||||
fringe: Option<Vec<bool>>,
|
||||
}
|
||||
|
||||
/// Result of one embedded PISO step.
|
||||
@@ -153,6 +157,17 @@ pub struct EmbeddedPisoSolver {
|
||||
boundary_velocity: Option<VelocityFn>,
|
||||
body: Option<EmbeddedBody>,
|
||||
mask: Option<EmbeddedMask>,
|
||||
/// Overset fringe (A-P2): per-cell flag, and the Dirichlet `p'` the
|
||||
/// fringe cells carry in the next projection (row-major, full size).
|
||||
fringe: Option<Vec<bool>>,
|
||||
fringe_correction: Vec<f64>,
|
||||
/// Relative part of the pressure solve's inner stop (default 1e-2 =
|
||||
/// bit-identical with the record): each solve reduces the residual to
|
||||
/// this fraction of the continuity source. The overset's Schwarz rounds
|
||||
/// exchange the solution and need it accurate below their own stop
|
||||
/// (measured: at 1e-2 the first corrector's rounds never settled below
|
||||
/// a 1e-3 relative change — 20/20 rounds every step).
|
||||
inner_stop_factor: f64,
|
||||
moving: bool,
|
||||
/// Mask hysteresis band in multiples of the min cell size (0 = off).
|
||||
mask_hysteresis: f64,
|
||||
@@ -176,6 +191,9 @@ impl EmbeddedPisoSolver {
|
||||
boundary_velocity: None,
|
||||
body: None,
|
||||
mask: None,
|
||||
fringe: None,
|
||||
fringe_correction: Vec::new(),
|
||||
inner_stop_factor: 1e-2,
|
||||
moving: false,
|
||||
mask_hysteresis: 0.0,
|
||||
time: 0.0,
|
||||
@@ -247,6 +265,72 @@ impl EmbeddedPisoSolver {
|
||||
self.moving = true;
|
||||
}
|
||||
|
||||
/// The overset background (A-P2): a mask from the overlap
|
||||
/// classification (active cells fluid, prescribed faces `Ghost` with no
|
||||
/// reconstruction) and the fringe flags. Fringe cells carry no
|
||||
/// continuity equation; their `p'` is Dirichlet
|
||||
/// ([`Self::set_fringe_correction`]) and their `p` and prescribed faces
|
||||
/// are stamped by the caller from the patch. No body: nothing is
|
||||
/// re-imposed at the end of a step.
|
||||
pub fn set_overlap(&mut self, mask: EmbeddedMask, fringe: Vec<bool>) {
|
||||
self.fringe_correction = vec![0.0; fringe.len()];
|
||||
self.fringe = Some(fringe);
|
||||
self.mask = Some(mask);
|
||||
self.body = None;
|
||||
self.moving = false;
|
||||
}
|
||||
|
||||
/// Dirichlet `p'` of the fringe cells for the next
|
||||
/// [`Self::solve_correction`] (`cells` as `(j, i)`).
|
||||
pub fn set_fringe_correction(&mut self, cells: &[(usize, usize)], values: &[f64]) {
|
||||
let nx = self.mask.as_ref().map_or(0, |m| m.nx());
|
||||
for (&(j, i), &v) in cells.iter().zip(values) {
|
||||
self.fringe_correction[j * nx + i] = v;
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative part of the pressure solve's inner stop (see the field).
|
||||
pub fn set_inner_stop_factor(&mut self, factor: f64) {
|
||||
self.inner_stop_factor = factor;
|
||||
}
|
||||
/// The inner-stop factor.
|
||||
pub fn inner_stop_factor(&self) -> f64 {
|
||||
self.inner_stop_factor
|
||||
}
|
||||
|
||||
/// Whether an overset fringe is set.
|
||||
pub fn has_fringe(&self) -> bool {
|
||||
self.fringe.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_fringe(&self, j: usize, i: usize) -> bool {
|
||||
match (&self.fringe, &self.mask) {
|
||||
(Some(f), Some(m)) => f[j * m.nx() + i],
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Dirichlet `p'` of fringe cell `(j, i)`.
|
||||
#[inline]
|
||||
pub(crate) fn fringe_correction(&self, j: usize, i: usize) -> f64 {
|
||||
let nx = self.mask.as_ref().map_or(0, |m| m.nx());
|
||||
self.fringe_correction[j * nx + i]
|
||||
}
|
||||
|
||||
/// Write the fringe cells' Dirichlet `p'` into `p_prime` (the face
|
||||
/// corrections across active–fringe faces read it there).
|
||||
pub(crate) fn stamp_fringe_correction(&self, p_prime: &mut nalgebra::DMatrix<f64>) {
|
||||
if let (Some(f), Some(m)) = (&self.fringe, &self.mask) {
|
||||
let nx = m.nx();
|
||||
for (idx, &is_fringe) in f.iter().enumerate() {
|
||||
if is_fringe {
|
||||
p_prime[(idx / nx, idx % nx)] = self.fringe_correction[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Field extension for faces that turn fluid (moving-body path).
|
||||
pub fn set_field_extension(&mut self, on: bool) {
|
||||
self.field_extension = on;
|
||||
@@ -285,6 +369,7 @@ impl EmbeddedPisoSolver {
|
||||
time: self.time,
|
||||
initialized: self.initialized,
|
||||
alpha: self.alpha_old.clone(),
|
||||
fringe: self.fringe.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +380,10 @@ impl EmbeddedPisoSolver {
|
||||
self.time = state.time;
|
||||
self.initialized = state.initialized;
|
||||
self.alpha_old = state.alpha.clone();
|
||||
self.fringe = state.fringe.clone();
|
||||
if let Some(f) = &self.fringe {
|
||||
self.fringe_correction = vec![0.0; f.len()];
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the accumulated time.
|
||||
@@ -680,413 +769,40 @@ impl EmbeddedPisoSolver {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The pressure-correction system of one projection as a
|
||||
/// [`PoissonProblem`]: active = fluid cells, coefficient `dt A / delta`
|
||||
/// across every fluid interior face and zero across every prescribed
|
||||
/// one (domain Velocity / SlipWall sides, non-fluid interior faces),
|
||||
/// the outlet's Dirichlet `p' = 0` half a cell away as a diagonal-only
|
||||
/// `extra_diag`, right-hand side the mass imbalance `sp`. Arm for arm
|
||||
/// the coefficients the SOR loop of [`Self::project`] forms in place.
|
||||
fn poisson_problem(&self, field: &FlowField, dt: f64) -> PoissonProblem {
|
||||
let (nx, ny, dx, dy) = field.grid_info();
|
||||
let b = self.parameters.boundaries;
|
||||
let outlet = SideBoundary::PressureOutlet;
|
||||
let ae_interior = dt * dy / dx;
|
||||
let an_interior = dt * dx / dy;
|
||||
let ae_outlet = dt * dy / (0.5 * dx);
|
||||
let an_outlet = dt * dx / (0.5 * dy);
|
||||
|
||||
let mut problem = PoissonProblem::new(nx, ny);
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let idx = j * nx + i;
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
problem.active[idx] = false;
|
||||
continue;
|
||||
}
|
||||
let mut extra = 0.0;
|
||||
if i + 1 == nx {
|
||||
if b.right == outlet {
|
||||
extra += ae_outlet;
|
||||
}
|
||||
} else if self.u_is_fluid(j, i + 1) {
|
||||
problem.ae[idx] = ae_interior;
|
||||
}
|
||||
if i == 0 {
|
||||
if b.left == outlet {
|
||||
extra += ae_outlet;
|
||||
}
|
||||
} else if self.u_is_fluid(j, i) {
|
||||
problem.aw[idx] = ae_interior;
|
||||
}
|
||||
if j + 1 == ny {
|
||||
if b.top == outlet {
|
||||
extra += an_outlet;
|
||||
}
|
||||
} else if self.v_is_fluid(j + 1, i) {
|
||||
problem.an[idx] = an_interior;
|
||||
}
|
||||
if j == 0 {
|
||||
if b.bottom == outlet {
|
||||
extra += an_outlet;
|
||||
}
|
||||
} else if self.v_is_fluid(j, i) {
|
||||
problem.as_[idx] = an_interior;
|
||||
}
|
||||
problem.extra_diag[idx] = extra;
|
||||
problem.rhs[idx] = field.sp[(j, i)];
|
||||
}
|
||||
}
|
||||
problem
|
||||
}
|
||||
|
||||
/// One projection on the fluid cells: the fixed-grid PISO's, with a
|
||||
/// zero coefficient across every prescribed face (domain Velocity /
|
||||
/// SlipWall sides and every non-fluid interior face), a Dirichlet `p' =
|
||||
/// 0` half a cell beyond an outlet face, and the anchor on the first
|
||||
/// fluid cell when no outlet exists. Returns the normalised mass
|
||||
/// imbalance of the corrected field over the fluid cells.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn project(&self, field: &mut FlowField, dt: f64, warm_start: bool) -> CfdResult<f64> {
|
||||
let (nx, ny, dx, dy) = field.grid_info();
|
||||
let rho = self.config.density;
|
||||
let b = self.parameters.boundaries;
|
||||
let outlet = SideBoundary::PressureOutlet;
|
||||
let any_outlet = [b.left, b.right, b.bottom, b.top].contains(&outlet);
|
||||
let anchor = self.mask.as_ref().map_or((1, 1), EmbeddedMask::anchor);
|
||||
|
||||
// With `warm_start` (the FIRST corrector only), the previous
|
||||
// step's correction is the multigrid initial guess — the
|
||||
// correction field is temporally correlated step to step
|
||||
// (measured 2026-08-30 on the FSI2 rigid phase: 2.96 PCG
|
||||
// iterations/solve from zero, 1.27 warm). Later correctors
|
||||
// solve for a much SMALLER correction, and the first
|
||||
// corrector's p' is a WORSE guess than zero there (measured:
|
||||
// the all-correctors draft cost 3.9 iters/solve in the coupled
|
||||
// phase). The SOR fallback below still starts from zero,
|
||||
// exactly as before.
|
||||
|
||||
let mut source_scale = 0.0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
field.sp[(j, i)] = 0.0;
|
||||
continue;
|
||||
}
|
||||
let divergence_flux = rho
|
||||
* ((field.u_star[(j, i + 1)] - field.u_star[(j, i)]) * dy
|
||||
+ (field.v_star[(j + 1, i)] - field.v_star[(j, i)]) * dx);
|
||||
field.sp[(j, i)] = -divergence_flux;
|
||||
source_scale += divergence_flux.abs();
|
||||
}
|
||||
}
|
||||
// Swept-volume source (knob; see `swept_volume`): the corrected
|
||||
// field must satisfy Σ u·n A = −dV_f/dt in every interface cell.
|
||||
if let (true, Some(a_new), Some(a_old)) =
|
||||
(self.swept_volume != 0.0, &self.alpha_new, &self.alpha_old)
|
||||
{
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
let k = j * nx + i;
|
||||
let da = a_new[k] - a_old[k];
|
||||
if da != 0.0 {
|
||||
field.sp[(j, i)] -= self.swept_volume * rho * da * dx * dy / dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reporting-only divergence trace (RTX_EMBEDDED_TRACE_SP): where the
|
||||
// projection's source sits relative to the step's fresh cells, in
|
||||
// units of one whole cell volume per step (rho dx dy / dt).
|
||||
if warm_start && !self.fresh_trace.is_empty() {
|
||||
let unit = rho * dx * dy / dt;
|
||||
let is_fresh =
|
||||
|j: usize, i: usize| self.fresh_trace.iter().any(|&(a, b)| a == j && b == i);
|
||||
let is_nbr = |j: usize, i: usize| {
|
||||
self.fresh_trace.iter().any(|&(a, b)| {
|
||||
(a == j && (b + 1 == i || i + 1 == b)) || (b == i && (a + 1 == j || j + 1 == a))
|
||||
})
|
||||
};
|
||||
let (mut mf, mut mn, mut mo) = (0.0f64, 0.0f64, 0.0f64);
|
||||
let (mut arg, mut argv) = ((0usize, 0usize), 0.0f64);
|
||||
let mut sum_fresh = 0.0f64;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
let v = field.sp[(j, i)] / unit;
|
||||
if is_fresh(j, i) {
|
||||
mf = mf.max(v.abs());
|
||||
sum_fresh += v;
|
||||
} else if is_nbr(j, i) {
|
||||
mn = mn.max(v.abs());
|
||||
} else {
|
||||
mo = mo.max(v.abs());
|
||||
}
|
||||
if v.abs() > argv.abs() {
|
||||
argv = v;
|
||||
arg = (j, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
let class = if is_fresh(arg.0, arg.1) {
|
||||
"FRESH"
|
||||
} else if is_nbr(arg.0, arg.1) {
|
||||
"NEIGHBOUR"
|
||||
} else {
|
||||
"other"
|
||||
};
|
||||
// The argmax cell's 3x3 neighbourhood: F = fluid, S = solid,
|
||||
// * = fresh this step (row above first).
|
||||
let mut hood = String::new();
|
||||
for dj in [1i64, 0, -1] {
|
||||
for di in [-1i64, 0, 1] {
|
||||
let (jj, ii) = (arg.0 as i64 + dj, arg.1 as i64 + di);
|
||||
let c = if jj < 0 || ii < 0 || jj >= ny as i64 || ii >= nx as i64 {
|
||||
'#'
|
||||
} else if is_fresh(jj as usize, ii as usize) {
|
||||
'*'
|
||||
} else if self.cell_is_fluid(jj as usize, ii as usize) {
|
||||
'F'
|
||||
} else {
|
||||
'S'
|
||||
};
|
||||
hood.push(c);
|
||||
}
|
||||
hood.push('/');
|
||||
}
|
||||
let fj = self.fresh_trace.iter().map(|c| c.0);
|
||||
let fi = self.fresh_trace.iter().map(|c| c.1);
|
||||
println!(
|
||||
" SP-TRACE fresh rows {:?}..{:?} cols {:?}..{:?}; argmax hood {hood}",
|
||||
fj.clone().min(),
|
||||
fj.max(),
|
||||
fi.clone().min(),
|
||||
fi.max()
|
||||
);
|
||||
println!(
|
||||
" SP-TRACE t = {:.6}: {} fresh cells; max |sp| {:+.3} cell-volumes/step at ({}, {}) [{class}]; \
|
||||
max over fresh {:.3}, neighbours {:.3}, others {:.3}; sum over fresh {:+.3}",
|
||||
self.time + dt,
|
||||
self.fresh_trace.len(),
|
||||
argv,
|
||||
arg.0,
|
||||
arg.1,
|
||||
mf,
|
||||
mn,
|
||||
mo,
|
||||
sum_fresh
|
||||
);
|
||||
}
|
||||
|
||||
let ae_interior = dt * dy / dx;
|
||||
let an_interior = dt * dx / dy;
|
||||
// Outlet face: p' = 0 half a cell away.
|
||||
let ae_outlet = dt * dy / (0.5 * dx);
|
||||
let an_outlet = dt * dx / (0.5 * dy);
|
||||
|
||||
let reference_flux = rho * self.config.reference_velocity * self.config.reference_length;
|
||||
let inner_stop =
|
||||
(1e-2 * source_scale).max(0.1 * self.parameters.tolerance * reference_flux) + 1e-14;
|
||||
let mut multigrid_converged = false;
|
||||
if self.parameters.poisson_solver == PoissonSolverKind::Multigrid {
|
||||
// The same system the SOR loop below sweeps, handed to the
|
||||
// multigrid-preconditioned CG solver: anchored on the first
|
||||
// fluid cell when there is no outlet (the SOR loop pins it to
|
||||
// zero), level-free otherwise. Non-fluid cells and isolated
|
||||
// fluid cells are never written and keep `p' = 0`.
|
||||
let problem = self.poisson_problem(field, dt);
|
||||
// Warm start from the previous correction on the CURRENT
|
||||
// fluid cells; everything else stays zero, preserving the
|
||||
// p' = 0 invariant on non-fluid cells through the copy-back.
|
||||
let mut p_prime = vec![0.0; nx * ny];
|
||||
if warm_start {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if self.cell_is_fluid(j, i) {
|
||||
p_prime[j * nx + i] = field.p_prime[(j, i)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let anchor_cell = (!any_outlet).then_some(anchor.0 * nx + anchor.1);
|
||||
let solution = solve_multigrid_pcg(
|
||||
&problem,
|
||||
&mut p_prime,
|
||||
&MultigridParameters {
|
||||
precision: self.parameters.poisson_precision,
|
||||
..MultigridParameters::default()
|
||||
},
|
||||
inner_stop,
|
||||
anchor_cell,
|
||||
);
|
||||
// Unconverged: fall back to the SOR sweeps for this projection
|
||||
// rather than apply a correction that did not reach the stop.
|
||||
multigrid_converged = solution.converged;
|
||||
if multigrid_converged {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
field.p_prime[(j, i)] = p_prime[j * nx + i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !multigrid_converged {
|
||||
// The fallback is unchanged: SOR from zero, as it always ran.
|
||||
field.p_prime.fill(0.0);
|
||||
let omega = 2.0 / (1.0 + (std::f64::consts::PI / nx.max(ny) as f64).sin());
|
||||
for _sweep in 0..2000 {
|
||||
let mut residual = 0.0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
if !any_outlet && (j, i) == anchor {
|
||||
field.p_prime[(j, i)] = 0.0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// A coefficient is zero exactly when the face is
|
||||
// prescribed: a domain side with velocity data, or a
|
||||
// non-fluid interior face.
|
||||
let ae = if i + 1 == nx {
|
||||
if b.right == outlet { ae_outlet } else { 0.0 }
|
||||
} else if self.u_is_fluid(j, i + 1) {
|
||||
ae_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let aw = if i == 0 {
|
||||
if b.left == outlet { ae_outlet } else { 0.0 }
|
||||
} else if self.u_is_fluid(j, i) {
|
||||
ae_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let an = if j + 1 == ny {
|
||||
if b.top == outlet { an_outlet } else { 0.0 }
|
||||
} else if self.v_is_fluid(j + 1, i) {
|
||||
an_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let as_ = if j == 0 {
|
||||
if b.bottom == outlet { an_outlet } else { 0.0 }
|
||||
} else if self.v_is_fluid(j, i) {
|
||||
an_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let ap = ae + aw + an + as_;
|
||||
if ap == 0.0 {
|
||||
// An isolated fluid cell enclosed by prescribed
|
||||
// faces has no equation; leave p' = 0 there.
|
||||
continue;
|
||||
}
|
||||
|
||||
let east = if i + 1 < nx {
|
||||
ae * field.p_prime[(j, i + 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let west = if i > 0 {
|
||||
aw * field.p_prime[(j, i - 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let north = if j + 1 < ny {
|
||||
an * field.p_prime[(j + 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let south = if j > 0 {
|
||||
as_ * field.p_prime[(j - 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let rhs = field.sp[(j, i)] + east + west + north + south;
|
||||
let p_old = field.p_prime[(j, i)];
|
||||
residual += (rhs - ap * p_old).abs();
|
||||
field.p_prime[(j, i)] = (1.0 - omega) * p_old + omega * rhs / ap;
|
||||
}
|
||||
}
|
||||
if residual < inner_stop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Correct exactly the faces the equations treated as correctable:
|
||||
// fluid interior faces, and outlet faces against p' = 0 outside.
|
||||
for j in 0..ny {
|
||||
for i in 1..nx {
|
||||
if self.u_is_fluid(j, i) {
|
||||
let dp_dx = (field.p_prime[(j, i)] - field.p_prime[(j, i - 1)]) / dx;
|
||||
field.u[(j, i)] = field.u_star[(j, i)] - (dt / rho) * dp_dx;
|
||||
}
|
||||
}
|
||||
if b.left == outlet {
|
||||
let dp_dx = (field.p_prime[(j, 0)] - 0.0) / (0.5 * dx);
|
||||
field.u[(j, 0)] = field.u_star[(j, 0)] - (dt / rho) * dp_dx;
|
||||
}
|
||||
if b.right == outlet {
|
||||
let dp_dx = (0.0 - field.p_prime[(j, nx - 1)]) / (0.5 * dx);
|
||||
field.u[(j, nx)] = field.u_star[(j, nx)] - (dt / rho) * dp_dx;
|
||||
}
|
||||
}
|
||||
for i in 0..nx {
|
||||
for j in 1..ny {
|
||||
if self.v_is_fluid(j, i) {
|
||||
let dp_dy = (field.p_prime[(j, i)] - field.p_prime[(j - 1, i)]) / dy;
|
||||
field.v[(j, i)] = field.v_star[(j, i)] - (dt / rho) * dp_dy;
|
||||
}
|
||||
}
|
||||
if b.bottom == outlet {
|
||||
let dp_dy = (field.p_prime[(0, i)] - 0.0) / (0.5 * dy);
|
||||
field.v[(0, i)] = field.v_star[(0, i)] - (dt / rho) * dp_dy;
|
||||
}
|
||||
if b.top == outlet {
|
||||
let dp_dy = (0.0 - field.p_prime[(ny - 1, i)]) / (0.5 * dy);
|
||||
field.v[(ny, i)] = field.v_star[(ny, i)] - (dt / rho) * dp_dy;
|
||||
}
|
||||
}
|
||||
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if self.cell_is_fluid(j, i) {
|
||||
field.p[(j, i)] += field.p_prime[(j, i)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut mass_imbalance = 0.0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
let divergence_flux = rho
|
||||
* ((field.u[(j, i + 1)] - field.u[(j, i)]) * dy
|
||||
+ (field.v[(j + 1, i)] - field.v[(j, i)]) * dx);
|
||||
mass_imbalance += divergence_flux.abs();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(if reference_flux > 0.0 {
|
||||
mass_imbalance / reference_flux
|
||||
} else {
|
||||
mass_imbalance
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 FlowField, dt: f64) -> CfdResult<EmbeddedResult> {
|
||||
let start = self.begin_step(field, dt)?;
|
||||
|
||||
let mut residual_history = Vec::new();
|
||||
let mut total_corrector_steps = 0;
|
||||
let mut final_residual = f64::INFINITY;
|
||||
for corrector in 0..self.parameters.corrector_steps.max(1) {
|
||||
let mass_residual = self.project(field, dt, corrector == 0)?;
|
||||
residual_history.push(mass_residual);
|
||||
final_residual = mass_residual;
|
||||
total_corrector_steps += 1;
|
||||
if mass_residual < self.parameters.tolerance {
|
||||
break;
|
||||
}
|
||||
field.copy_to_starred();
|
||||
}
|
||||
|
||||
Ok(self.end_step(
|
||||
field,
|
||||
&start,
|
||||
residual_history,
|
||||
total_corrector_steps,
|
||||
final_residual,
|
||||
))
|
||||
}
|
||||
|
||||
/// Everything before the correctors: validation, lazy initialisation,
|
||||
/// the history shift, the explicit predictor, the new interval's
|
||||
/// boundary data, the moving-body mask rebuild (fresh-cell refill,
|
||||
/// ghost re-imposition), and `u* = u`. The overset coupling runs this
|
||||
/// on the background, then drives the correctors itself.
|
||||
pub(crate) fn begin_step(&mut self, field: &mut FlowField, dt: f64) -> CfdResult<StepStart> {
|
||||
if dt <= 0.0 || !dt.is_finite() {
|
||||
return Err(CfdError::invalid_parameter(format!(
|
||||
"time step must be positive and finite, got {dt}"
|
||||
@@ -1208,21 +924,25 @@ impl EmbeddedPisoSolver {
|
||||
}
|
||||
}
|
||||
field.copy_to_starred();
|
||||
Ok(StepStart {
|
||||
start_time,
|
||||
t_new,
|
||||
fresh_cells,
|
||||
})
|
||||
}
|
||||
|
||||
let mut residual_history = Vec::new();
|
||||
let mut total_corrector_steps = 0;
|
||||
let mut final_residual = f64::INFINITY;
|
||||
for corrector in 0..self.parameters.corrector_steps.max(1) {
|
||||
let mass_residual = self.project(field, dt, corrector == 0)?;
|
||||
residual_history.push(mass_residual);
|
||||
final_residual = mass_residual;
|
||||
total_corrector_steps += 1;
|
||||
if mass_residual < self.parameters.tolerance {
|
||||
break;
|
||||
}
|
||||
field.copy_to_starred();
|
||||
}
|
||||
|
||||
/// Everything after the correctors: ghost re-imposition from the
|
||||
/// corrected field, the clock, the swept-volume bookkeeping, the
|
||||
/// result.
|
||||
pub(crate) fn end_step(
|
||||
&mut self,
|
||||
field: &mut FlowField,
|
||||
start: &StepStart,
|
||||
residual_history: Vec<f64>,
|
||||
total_corrector_steps: usize,
|
||||
final_residual: f64,
|
||||
) -> EmbeddedResult {
|
||||
let t_new = start.t_new;
|
||||
// Ghost faces follow the corrected fluid field; they are the
|
||||
// stencil and flux data of the next step.
|
||||
let ghost_correction = match (&self.body, &self.mask) {
|
||||
@@ -1234,17 +954,28 @@ impl EmbeddedPisoSolver {
|
||||
if let Some(a) = self.alpha_new.take() {
|
||||
self.alpha_old = Some(a);
|
||||
}
|
||||
Ok(EmbeddedResult {
|
||||
EmbeddedResult {
|
||||
solver_result: SolverResult {
|
||||
converged: final_residual < self.parameters.tolerance,
|
||||
iterations: total_corrector_steps,
|
||||
final_residual,
|
||||
residual_history,
|
||||
solve_time: start_time.elapsed(),
|
||||
solve_time: start.start_time.elapsed(),
|
||||
},
|
||||
corrector_steps_performed: total_corrector_steps,
|
||||
ghost_correction,
|
||||
fresh_cells,
|
||||
})
|
||||
fresh_cells: start.fresh_cells,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`EmbeddedPisoSolver::begin_step`] hands to
|
||||
/// [`EmbeddedPisoSolver::end_step`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct StepStart {
|
||||
start_time: std::time::Instant,
|
||||
/// End-of-step time.
|
||||
pub(crate) t_new: f64,
|
||||
/// Pressure cells that flipped solid → fluid in this step's rebuild.
|
||||
pub(crate) fresh_cells: usize,
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
//! The pressure step of the embedded-boundary PISO: the masked five-point
|
||||
//! problem and the projection, split into its solve and apply halves so
|
||||
//! the overset coupling (`overset/`) can iterate the solve across two
|
||||
//! meshes before applying the correction once.
|
||||
|
||||
use super::EmbeddedPisoSolver;
|
||||
use crate::CfdResult;
|
||||
use crate::solvers::incompressible::ale::SideBoundary;
|
||||
use crate::solvers::incompressible::poisson::{
|
||||
MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
|
||||
};
|
||||
use crate::solvers::incompressible::{EmbeddedMask, FlowField};
|
||||
|
||||
impl EmbeddedPisoSolver {
|
||||
/// The pressure-correction system of one projection as a
|
||||
/// [`PoissonProblem`]: active = fluid cells, coefficient `dt A / delta`
|
||||
/// across every fluid interior face and zero across every prescribed
|
||||
/// one (domain Velocity / SlipWall sides, non-fluid interior faces),
|
||||
/// the outlet's Dirichlet `p' = 0` half a cell away as a diagonal-only
|
||||
/// `extra_diag`, right-hand side the mass imbalance `sp`. Arm for arm
|
||||
/// the coefficients the SOR loop of [`Self::project`] forms in place.
|
||||
pub(super) fn poisson_problem(&self, field: &FlowField, dt: f64) -> PoissonProblem {
|
||||
let (nx, ny, dx, dy) = field.grid_info();
|
||||
let b = self.parameters.boundaries;
|
||||
let outlet = SideBoundary::PressureOutlet;
|
||||
let ae_interior = dt * dy / dx;
|
||||
let an_interior = dt * dx / dy;
|
||||
let ae_outlet = dt * dy / (0.5 * dx);
|
||||
let an_outlet = dt * dx / (0.5 * dy);
|
||||
|
||||
let mut problem = PoissonProblem::new(nx, ny);
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let idx = j * nx + i;
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
problem.active[idx] = false;
|
||||
continue;
|
||||
}
|
||||
let mut extra = 0.0;
|
||||
// An overset fringe neighbour is a Dirichlet cell: its
|
||||
// coefficient moves to the diagonal and its known p' to
|
||||
// the right-hand side (the outlet's construction).
|
||||
let mut rhs_extra = 0.0;
|
||||
if i + 1 == nx {
|
||||
if b.right == outlet {
|
||||
extra += ae_outlet;
|
||||
}
|
||||
} else if self.u_is_fluid(j, i + 1) {
|
||||
if self.is_fringe(j, i + 1) {
|
||||
extra += ae_interior;
|
||||
rhs_extra += ae_interior * self.fringe_correction(j, i + 1);
|
||||
} else {
|
||||
problem.ae[idx] = ae_interior;
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
if b.left == outlet {
|
||||
extra += ae_outlet;
|
||||
}
|
||||
} else if self.u_is_fluid(j, i) {
|
||||
if self.is_fringe(j, i - 1) {
|
||||
extra += ae_interior;
|
||||
rhs_extra += ae_interior * self.fringe_correction(j, i - 1);
|
||||
} else {
|
||||
problem.aw[idx] = ae_interior;
|
||||
}
|
||||
}
|
||||
if j + 1 == ny {
|
||||
if b.top == outlet {
|
||||
extra += an_outlet;
|
||||
}
|
||||
} else if self.v_is_fluid(j + 1, i) {
|
||||
if self.is_fringe(j + 1, i) {
|
||||
extra += an_interior;
|
||||
rhs_extra += an_interior * self.fringe_correction(j + 1, i);
|
||||
} else {
|
||||
problem.an[idx] = an_interior;
|
||||
}
|
||||
}
|
||||
if j == 0 {
|
||||
if b.bottom == outlet {
|
||||
extra += an_outlet;
|
||||
}
|
||||
} else if self.v_is_fluid(j, i) {
|
||||
if self.is_fringe(j - 1, i) {
|
||||
extra += an_interior;
|
||||
rhs_extra += an_interior * self.fringe_correction(j - 1, i);
|
||||
} else {
|
||||
problem.as_[idx] = an_interior;
|
||||
}
|
||||
}
|
||||
problem.extra_diag[idx] = extra;
|
||||
problem.rhs[idx] = field.sp[(j, i)] + rhs_extra;
|
||||
}
|
||||
}
|
||||
problem
|
||||
}
|
||||
|
||||
/// One projection on the fluid cells: the fixed-grid PISO's, with a
|
||||
/// zero coefficient across every prescribed face (domain Velocity /
|
||||
/// SlipWall sides and every non-fluid interior face), a Dirichlet `p' =
|
||||
/// 0` half a cell beyond an outlet face, and the anchor on the first
|
||||
/// fluid cell when no outlet exists. Returns the normalised mass
|
||||
/// imbalance of the corrected field over the fluid cells.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
/// One projection on the fluid cells: [`Self::solve_correction`] then
|
||||
/// [`Self::apply_correction`] — the two halves the overset coupling
|
||||
/// calls separately (several solves, one application).
|
||||
pub(super) fn project(
|
||||
&self,
|
||||
field: &mut FlowField,
|
||||
dt: f64,
|
||||
warm_start: bool,
|
||||
) -> CfdResult<f64> {
|
||||
self.solve_correction(field, dt, warm_start)?;
|
||||
Ok(self.apply_correction(field, dt))
|
||||
}
|
||||
|
||||
/// The solve half of a projection: the continuity source from `u*`
|
||||
/// into `field.sp`, then `p'` into `field.p_prime` (multigrid PCG with
|
||||
/// the SOR fallback). Nothing else in `field` is touched.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) fn solve_correction(
|
||||
&self,
|
||||
field: &mut FlowField,
|
||||
dt: f64,
|
||||
warm_start: bool,
|
||||
) -> CfdResult<()> {
|
||||
let (nx, ny, dx, dy) = field.grid_info();
|
||||
let rho = self.config.density;
|
||||
let b = self.parameters.boundaries;
|
||||
let outlet = SideBoundary::PressureOutlet;
|
||||
let any_outlet = [b.left, b.right, b.bottom, b.top].contains(&outlet);
|
||||
let anchor = self.mask.as_ref().map_or((1, 1), EmbeddedMask::anchor);
|
||||
|
||||
// With `warm_start` (the FIRST corrector only), the previous
|
||||
// step's correction is the multigrid initial guess — the
|
||||
// correction field is temporally correlated step to step
|
||||
// (measured 2026-08-30 on the FSI2 rigid phase: 2.96 PCG
|
||||
// iterations/solve from zero, 1.27 warm). Later correctors
|
||||
// solve for a much SMALLER correction, and the first
|
||||
// corrector's p' is a WORSE guess than zero there (measured:
|
||||
// the all-correctors draft cost 3.9 iters/solve in the coupled
|
||||
// phase). The SOR fallback below still starts from zero,
|
||||
// exactly as before.
|
||||
|
||||
let mut source_scale = 0.0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
field.sp[(j, i)] = 0.0;
|
||||
continue;
|
||||
}
|
||||
let divergence_flux = rho
|
||||
* ((field.u_star[(j, i + 1)] - field.u_star[(j, i)]) * dy
|
||||
+ (field.v_star[(j + 1, i)] - field.v_star[(j, i)]) * dx);
|
||||
field.sp[(j, i)] = -divergence_flux;
|
||||
source_scale += divergence_flux.abs();
|
||||
}
|
||||
}
|
||||
// Swept-volume source (knob; see `swept_volume`): the corrected
|
||||
// field must satisfy Σ u·n A = −dV_f/dt in every interface cell.
|
||||
if let (true, Some(a_new), Some(a_old)) =
|
||||
(self.swept_volume != 0.0, &self.alpha_new, &self.alpha_old)
|
||||
{
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
let k = j * nx + i;
|
||||
let da = a_new[k] - a_old[k];
|
||||
if da != 0.0 {
|
||||
field.sp[(j, i)] -= self.swept_volume * rho * da * dx * dy / dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reporting-only divergence trace (RTX_EMBEDDED_TRACE_SP): where the
|
||||
// projection's source sits relative to the step's fresh cells, in
|
||||
// units of one whole cell volume per step (rho dx dy / dt).
|
||||
if warm_start && !self.fresh_trace.is_empty() {
|
||||
let unit = rho * dx * dy / dt;
|
||||
let is_fresh =
|
||||
|j: usize, i: usize| self.fresh_trace.iter().any(|&(a, b)| a == j && b == i);
|
||||
let is_nbr = |j: usize, i: usize| {
|
||||
self.fresh_trace.iter().any(|&(a, b)| {
|
||||
(a == j && (b + 1 == i || i + 1 == b)) || (b == i && (a + 1 == j || j + 1 == a))
|
||||
})
|
||||
};
|
||||
let (mut mf, mut mn, mut mo) = (0.0f64, 0.0f64, 0.0f64);
|
||||
let (mut arg, mut argv) = ((0usize, 0usize), 0.0f64);
|
||||
let mut sum_fresh = 0.0f64;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
let v = field.sp[(j, i)] / unit;
|
||||
if is_fresh(j, i) {
|
||||
mf = mf.max(v.abs());
|
||||
sum_fresh += v;
|
||||
} else if is_nbr(j, i) {
|
||||
mn = mn.max(v.abs());
|
||||
} else {
|
||||
mo = mo.max(v.abs());
|
||||
}
|
||||
if v.abs() > argv.abs() {
|
||||
argv = v;
|
||||
arg = (j, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
let class = if is_fresh(arg.0, arg.1) {
|
||||
"FRESH"
|
||||
} else if is_nbr(arg.0, arg.1) {
|
||||
"NEIGHBOUR"
|
||||
} else {
|
||||
"other"
|
||||
};
|
||||
// The argmax cell's 3x3 neighbourhood: F = fluid, S = solid,
|
||||
// * = fresh this step (row above first).
|
||||
let mut hood = String::new();
|
||||
for dj in [1i64, 0, -1] {
|
||||
for di in [-1i64, 0, 1] {
|
||||
let (jj, ii) = (arg.0 as i64 + dj, arg.1 as i64 + di);
|
||||
let c = if jj < 0 || ii < 0 || jj >= ny as i64 || ii >= nx as i64 {
|
||||
'#'
|
||||
} else if is_fresh(jj as usize, ii as usize) {
|
||||
'*'
|
||||
} else if self.cell_is_fluid(jj as usize, ii as usize) {
|
||||
'F'
|
||||
} else {
|
||||
'S'
|
||||
};
|
||||
hood.push(c);
|
||||
}
|
||||
hood.push('/');
|
||||
}
|
||||
let fj = self.fresh_trace.iter().map(|c| c.0);
|
||||
let fi = self.fresh_trace.iter().map(|c| c.1);
|
||||
println!(
|
||||
" SP-TRACE fresh rows {:?}..{:?} cols {:?}..{:?}; argmax hood {hood}",
|
||||
fj.clone().min(),
|
||||
fj.max(),
|
||||
fi.clone().min(),
|
||||
fi.max()
|
||||
);
|
||||
println!(
|
||||
" SP-TRACE t = {:.6}: {} fresh cells; max |sp| {:+.3} cell-volumes/step at ({}, {}) [{class}]; \
|
||||
max over fresh {:.3}, neighbours {:.3}, others {:.3}; sum over fresh {:+.3}",
|
||||
self.time + dt,
|
||||
self.fresh_trace.len(),
|
||||
argv,
|
||||
arg.0,
|
||||
arg.1,
|
||||
mf,
|
||||
mn,
|
||||
mo,
|
||||
sum_fresh
|
||||
);
|
||||
}
|
||||
|
||||
let ae_interior = dt * dy / dx;
|
||||
let an_interior = dt * dx / dy;
|
||||
// Outlet face: p' = 0 half a cell away.
|
||||
let ae_outlet = dt * dy / (0.5 * dx);
|
||||
let an_outlet = dt * dx / (0.5 * dy);
|
||||
|
||||
let reference_flux = rho * self.config.reference_velocity * self.config.reference_length;
|
||||
let inner_stop = (self.inner_stop_factor * source_scale)
|
||||
.max(0.1 * self.parameters.tolerance * reference_flux)
|
||||
+ 1e-14;
|
||||
let mut multigrid_converged = false;
|
||||
if self.parameters.poisson_solver == PoissonSolverKind::Multigrid {
|
||||
// The same system the SOR loop below sweeps, handed to the
|
||||
// multigrid-preconditioned CG solver: anchored on the first
|
||||
// fluid cell when there is no outlet (the SOR loop pins it to
|
||||
// zero), level-free otherwise. Non-fluid cells and isolated
|
||||
// fluid cells are never written and keep `p' = 0`.
|
||||
let problem = self.poisson_problem(field, dt);
|
||||
// Warm start from the previous correction on the CURRENT
|
||||
// fluid cells; everything else stays zero, preserving the
|
||||
// p' = 0 invariant on non-fluid cells through the copy-back.
|
||||
let mut p_prime = vec![0.0; nx * ny];
|
||||
if warm_start {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if self.cell_is_fluid(j, i) {
|
||||
p_prime[j * nx + i] = field.p_prime[(j, i)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let anchor_cell =
|
||||
(!any_outlet && !self.has_fringe()).then_some(anchor.0 * nx + anchor.1);
|
||||
let solution = solve_multigrid_pcg(
|
||||
&problem,
|
||||
&mut p_prime,
|
||||
&MultigridParameters {
|
||||
precision: self.parameters.poisson_precision,
|
||||
..MultigridParameters::default()
|
||||
},
|
||||
inner_stop,
|
||||
anchor_cell,
|
||||
);
|
||||
// Unconverged: fall back to the SOR sweeps for this projection
|
||||
// rather than apply a correction that did not reach the stop.
|
||||
multigrid_converged = solution.converged;
|
||||
if multigrid_converged {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
field.p_prime[(j, i)] = p_prime[j * nx + i];
|
||||
}
|
||||
}
|
||||
self.stamp_fringe_correction(&mut field.p_prime);
|
||||
}
|
||||
}
|
||||
if !multigrid_converged {
|
||||
// The fallback is unchanged: SOR from zero, as it always ran.
|
||||
field.p_prime.fill(0.0);
|
||||
self.stamp_fringe_correction(&mut field.p_prime);
|
||||
let omega = 2.0 / (1.0 + (std::f64::consts::PI / nx.max(ny) as f64).sin());
|
||||
for _sweep in 0..2000 {
|
||||
let mut residual = 0.0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
if !any_outlet && !self.has_fringe() && (j, i) == anchor {
|
||||
field.p_prime[(j, i)] = 0.0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// A coefficient is zero exactly when the face is
|
||||
// prescribed: a domain side with velocity data, or a
|
||||
// non-fluid interior face.
|
||||
let ae = if i + 1 == nx {
|
||||
if b.right == outlet { ae_outlet } else { 0.0 }
|
||||
} else if self.u_is_fluid(j, i + 1) {
|
||||
ae_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let aw = if i == 0 {
|
||||
if b.left == outlet { ae_outlet } else { 0.0 }
|
||||
} else if self.u_is_fluid(j, i) {
|
||||
ae_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let an = if j + 1 == ny {
|
||||
if b.top == outlet { an_outlet } else { 0.0 }
|
||||
} else if self.v_is_fluid(j + 1, i) {
|
||||
an_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let as_ = if j == 0 {
|
||||
if b.bottom == outlet { an_outlet } else { 0.0 }
|
||||
} else if self.v_is_fluid(j, i) {
|
||||
an_interior
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let ap = ae + aw + an + as_;
|
||||
if ap == 0.0 {
|
||||
// An isolated fluid cell enclosed by prescribed
|
||||
// faces has no equation; leave p' = 0 there.
|
||||
continue;
|
||||
}
|
||||
|
||||
let east = if i + 1 < nx {
|
||||
ae * field.p_prime[(j, i + 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let west = if i > 0 {
|
||||
aw * field.p_prime[(j, i - 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let north = if j + 1 < ny {
|
||||
an * field.p_prime[(j + 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let south = if j > 0 {
|
||||
as_ * field.p_prime[(j - 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let rhs = field.sp[(j, i)] + east + west + north + south;
|
||||
let p_old = field.p_prime[(j, i)];
|
||||
residual += (rhs - ap * p_old).abs();
|
||||
field.p_prime[(j, i)] = (1.0 - omega) * p_old + omega * rhs / ap;
|
||||
}
|
||||
}
|
||||
if residual < inner_stop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The apply half of a projection: correct the fluid faces from `u*`
|
||||
/// with the `p'` in `field.p_prime` (outlet faces against `p' = 0`
|
||||
/// outside), add `p'` to `p` on the fluid cells, and return the
|
||||
/// normalised mass imbalance of the corrected field.
|
||||
pub(crate) fn apply_correction(&self, field: &mut FlowField, dt: f64) -> f64 {
|
||||
let (nx, ny, dx, dy) = field.grid_info();
|
||||
let rho = self.config.density;
|
||||
let b = self.parameters.boundaries;
|
||||
let outlet = SideBoundary::PressureOutlet;
|
||||
let reference_flux = rho * self.config.reference_velocity * self.config.reference_length;
|
||||
|
||||
// Correct exactly the faces the equations treated as correctable:
|
||||
// fluid interior faces, and outlet faces against p' = 0 outside.
|
||||
for j in 0..ny {
|
||||
for i in 1..nx {
|
||||
if self.u_is_fluid(j, i) {
|
||||
let dp_dx = (field.p_prime[(j, i)] - field.p_prime[(j, i - 1)]) / dx;
|
||||
field.u[(j, i)] = field.u_star[(j, i)] - (dt / rho) * dp_dx;
|
||||
}
|
||||
}
|
||||
if b.left == outlet {
|
||||
let dp_dx = (field.p_prime[(j, 0)] - 0.0) / (0.5 * dx);
|
||||
field.u[(j, 0)] = field.u_star[(j, 0)] - (dt / rho) * dp_dx;
|
||||
}
|
||||
if b.right == outlet {
|
||||
let dp_dx = (0.0 - field.p_prime[(j, nx - 1)]) / (0.5 * dx);
|
||||
field.u[(j, nx)] = field.u_star[(j, nx)] - (dt / rho) * dp_dx;
|
||||
}
|
||||
}
|
||||
for i in 0..nx {
|
||||
for j in 1..ny {
|
||||
if self.v_is_fluid(j, i) {
|
||||
let dp_dy = (field.p_prime[(j, i)] - field.p_prime[(j - 1, i)]) / dy;
|
||||
field.v[(j, i)] = field.v_star[(j, i)] - (dt / rho) * dp_dy;
|
||||
}
|
||||
}
|
||||
if b.bottom == outlet {
|
||||
let dp_dy = (field.p_prime[(0, i)] - 0.0) / (0.5 * dy);
|
||||
field.v[(0, i)] = field.v_star[(0, i)] - (dt / rho) * dp_dy;
|
||||
}
|
||||
if b.top == outlet {
|
||||
let dp_dy = (0.0 - field.p_prime[(ny - 1, i)]) / (0.5 * dy);
|
||||
field.v[(ny, i)] = field.v_star[(ny, i)] - (dt / rho) * dp_dy;
|
||||
}
|
||||
}
|
||||
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if self.cell_is_fluid(j, i) {
|
||||
field.p[(j, i)] += field.p_prime[(j, i)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut mass_imbalance = 0.0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(j, i) {
|
||||
continue;
|
||||
}
|
||||
let divergence_flux = rho
|
||||
* ((field.u[(j, i + 1)] - field.u[(j, i)]) * dy
|
||||
+ (field.v[(j + 1, i)] - field.v[(j, i)]) * dx);
|
||||
mass_imbalance += divergence_flux.abs();
|
||||
}
|
||||
}
|
||||
|
||||
if reference_flux > 0.0 {
|
||||
mass_imbalance / reference_flux
|
||||
} else {
|
||||
mass_imbalance
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,6 +643,52 @@ impl EmbeddedMask {
|
||||
})
|
||||
}
|
||||
|
||||
/// A mask from an explicit classification (the overset's hole/fringe
|
||||
/// map): `cell_fluid` row-major, `u_kind` `ny × (nx + 1)`, `v_kind`
|
||||
/// `(ny + 1) × nx`. No ghost reconstruction data — every non-fluid face
|
||||
/// is prescribed by whoever built the classification, and
|
||||
/// [`Self::impose`] has nothing to do. The anchor is the first fluid cell.
|
||||
pub fn from_classification(
|
||||
nx: usize,
|
||||
ny: usize,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
cell_fluid: Vec<bool>,
|
||||
u_kind: Vec<FaceKind>,
|
||||
v_kind: Vec<FaceKind>,
|
||||
) -> Self {
|
||||
assert_eq!(cell_fluid.len(), nx * ny);
|
||||
assert_eq!(u_kind.len(), ny * (nx + 1));
|
||||
assert_eq!(v_kind.len(), (ny + 1) * nx);
|
||||
let fluid_cells = cell_fluid.iter().filter(|&&f| f).count();
|
||||
let anchor = cell_fluid
|
||||
.iter()
|
||||
.position(|&f| f)
|
||||
.map_or((0, 0), |idx| (idx / nx, idx % nx));
|
||||
Self {
|
||||
nx,
|
||||
ny,
|
||||
dx,
|
||||
dy,
|
||||
cell_fluid,
|
||||
u_kind,
|
||||
v_kind,
|
||||
u_ghosts: Vec::new(),
|
||||
v_ghosts: Vec::new(),
|
||||
anchor,
|
||||
fluid_cells,
|
||||
}
|
||||
}
|
||||
|
||||
/// Cells in x.
|
||||
pub fn nx(&self) -> usize {
|
||||
self.nx
|
||||
}
|
||||
/// Cells in y.
|
||||
pub fn ny(&self) -> usize {
|
||||
self.ny
|
||||
}
|
||||
|
||||
/// Is pressure cell `(j, i)` fluid?
|
||||
#[inline]
|
||||
pub fn is_fluid_cell(&self, j: usize, i: usize) -> bool {
|
||||
|
||||
@@ -21,6 +21,8 @@ pub mod embedded;
|
||||
pub mod embedded_body;
|
||||
/// Flow field data structures
|
||||
pub mod flow_field;
|
||||
/// The overset hybrid: curvilinear patch over the fixed background
|
||||
pub mod overset;
|
||||
/// PISO algorithm implementation
|
||||
pub mod piso;
|
||||
/// GPU-accelerated PISO algorithm implementation
|
||||
@@ -54,6 +56,10 @@ pub use embedded_body::{
|
||||
polygon_signed_distance,
|
||||
};
|
||||
pub use flow_field::FlowField;
|
||||
pub use overset::{
|
||||
CellClass, OverlapMap, OversetField, OversetParameters, OversetPisoSolver, OversetResult,
|
||||
OversetSolverState,
|
||||
};
|
||||
pub use piso::{PisoParameters, PisoResult, PisoSolver};
|
||||
#[cfg(feature = "cuda")]
|
||||
pub use piso_gpu::PisoGpuSolver;
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
//! The overset (chimera) hybrid: a curvilinear patch around the body over
|
||||
//! the fixed background grid (`docs/overset_metal_campaign.md` §2, A-P2).
|
||||
//!
|
||||
//! [`OversetPisoSolver`] drives an [`EmbeddedPisoSolver`] (the background,
|
||||
//! its mask from the overlap classification: holes, fringe) and a
|
||||
//! [`CurvilinearPisoSolver`] (the patch, its outer row an acceptor ring)
|
||||
//! through one PISO step together:
|
||||
//!
|
||||
//! 1. patch predictor (`begin_step`: mesh swap if the patch moves, fluxes,
|
||||
//! `u*`, matrix); if the mesh moved, the overlap is rebuilt and the
|
||||
//! fringe re-stamped from the patch's previous corrected field (values
|
||||
//! identical when the mesh did not move — the moving path with a
|
||||
//! stationary patch is the static path to the bit);
|
||||
//! 2. background predictor (`begin_step`);
|
||||
//! 3. per corrector, alternating Schwarz on `p'` with Dirichlet transmission
|
||||
//! both ways: fringe `p'` ← patch `p'` (zero in the first round),
|
||||
//! background solve; acceptor `p'` ← background `p'`, patch solve; stop
|
||||
//! when the exchanged values change by less than `schwarz_tolerance ×
|
||||
//! max |p'|`; then ONE application of the correction on each mesh;
|
||||
//! 4. the exchange for the next step, from the corrected fields (the
|
||||
//! boundary-history principle): prescribed background faces and fringe
|
||||
//! `p` from the patch, acceptor `u, v, p` from the background.
|
||||
//!
|
||||
//! The overlap mass defect — continuity is enforced on neither the fringe
|
||||
//! nor the acceptor cells — is measured every step on both sides.
|
||||
|
||||
pub mod overlap;
|
||||
|
||||
pub use overlap::{Acceptor, CellClass, DualDonor, FringeEntry, LatticeDonor, OverlapMap};
|
||||
|
||||
use crate::error::{CfdError, CfdResult};
|
||||
use crate::mesh::PatchMesh;
|
||||
use crate::solvers::incompressible::curvilinear::{
|
||||
CurvilinearPisoSolver, CurvilinearSolverState, PatchField,
|
||||
};
|
||||
use crate::solvers::incompressible::embedded::{EmbeddedPisoSolver, EmbeddedSolverState};
|
||||
use crate::solvers::incompressible::flow_field::FlowField;
|
||||
|
||||
/// Parameters of the composite step.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OversetParameters {
|
||||
/// Projections per step (both meshes).
|
||||
pub corrector_steps: usize,
|
||||
/// Schwarz stop: the largest change of an exchanged `p'` value between
|
||||
/// rounds, relative to the largest exchanged `|p'|`.
|
||||
pub schwarz_tolerance: f64,
|
||||
/// Round cap per corrector.
|
||||
pub max_rounds: usize,
|
||||
/// Anderson-acceleration depth on the acceptor `p'` vector (0 = plain
|
||||
/// alternating Schwarz). Plain Schwarz converges at ≈ 0.82 per round
|
||||
/// here — the patch's wall is Neumann for `p'`, so its pressure level
|
||||
/// is pinned only through the fringe and decays weakly across the
|
||||
/// overlap (measured: 4.5e-3 relative after 20 rounds).
|
||||
pub anderson_depth: usize,
|
||||
/// Patch rows below the acceptor row kept non-hole
|
||||
/// (`overlap::DEFAULT_OVERLAP_ROWS`).
|
||||
pub overlap_rows: usize,
|
||||
}
|
||||
|
||||
impl Default for OversetParameters {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
corrector_steps: 2,
|
||||
schwarz_tolerance: 1e-3,
|
||||
max_rounds: 20,
|
||||
anderson_depth: 3,
|
||||
overlap_rows: overlap::DEFAULT_OVERLAP_ROWS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The two fields.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OversetField {
|
||||
/// Background staggered field.
|
||||
pub background: FlowField,
|
||||
/// Patch collocated field.
|
||||
pub patch: PatchField,
|
||||
}
|
||||
|
||||
/// What one composite step reports.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OversetResult {
|
||||
/// Schwarz rounds per corrector.
|
||||
pub rounds: Vec<usize>,
|
||||
/// Whether every corrector's Schwarz iteration met its stop.
|
||||
pub schwarz_converged: bool,
|
||||
/// Background mass residual after the last corrector (its own measure).
|
||||
pub background_residual: f64,
|
||||
/// Largest patch cell imbalance after the last corrector.
|
||||
pub patch_max_divergence: f64,
|
||||
/// Patch BiCGSTAB iterations, summed.
|
||||
pub patch_poisson_iterations: usize,
|
||||
/// Whether every patch pressure solve converged.
|
||||
pub patch_converged: bool,
|
||||
/// `Σ_fringe |div|` on the background (volume flux).
|
||||
pub background_mass_defect: f64,
|
||||
/// `Σ_acceptors |div|` on the patch (volume flux).
|
||||
pub patch_mass_defect: f64,
|
||||
/// `Σ |F|` over the acceptor–interior faces: the flux scale through the overlap.
|
||||
pub overlap_flux_scale: f64,
|
||||
/// Background cells that changed class in this step's overlap rebuild.
|
||||
pub reclassified_cells: usize,
|
||||
/// Background cells that jumped hole → active (patch moved > 1 cell).
|
||||
pub fresh_cells: usize,
|
||||
}
|
||||
|
||||
/// Restorable state of the composite (the coupling re-runs a step).
|
||||
#[derive(Clone)]
|
||||
pub struct OversetSolverState {
|
||||
background: EmbeddedSolverState,
|
||||
patch: CurvilinearSolverState,
|
||||
overlap: OverlapMap,
|
||||
pending: Option<PatchMesh>,
|
||||
acceptor_warm: Vec<f64>,
|
||||
}
|
||||
|
||||
/// The composite solver.
|
||||
pub struct OversetPisoSolver {
|
||||
background: EmbeddedPisoSolver,
|
||||
patch: CurvilinearPisoSolver,
|
||||
overlap: OverlapMap,
|
||||
params: OversetParameters,
|
||||
grid: (usize, usize, f64, f64),
|
||||
pending: Option<PatchMesh>,
|
||||
/// The first corrector's converged acceptor `p'` of the previous step:
|
||||
/// the temporal warm start (the correction is correlated step to step).
|
||||
acceptor_warm: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Anderson acceleration of a fixed-point iteration `x ← G(x)` with residual
|
||||
/// `r = G(x) − x`, depth `m` (least squares by normal equations, tiny).
|
||||
struct Anderson {
|
||||
m: usize,
|
||||
xs: Vec<Vec<f64>>,
|
||||
rs: Vec<Vec<f64>>,
|
||||
}
|
||||
|
||||
impl Anderson {
|
||||
fn new(m: usize) -> Self {
|
||||
Self {
|
||||
m,
|
||||
xs: Vec::new(),
|
||||
rs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Next iterate from the current `x` and its residual `r`.
|
||||
fn next(&mut self, x: &[f64], r: &[f64]) -> Vec<f64> {
|
||||
self.xs.push(x.to_vec());
|
||||
self.rs.push(r.to_vec());
|
||||
while self.xs.len() > self.m + 1 {
|
||||
self.xs.remove(0);
|
||||
self.rs.remove(0);
|
||||
}
|
||||
let k = self.xs.len() - 1;
|
||||
if self.m == 0 || k == 0 {
|
||||
return x.iter().zip(r).map(|(a, b)| a + b).collect();
|
||||
}
|
||||
// Differences ΔR_i = r_{i+1} − r_i, ΔX_i = x_{i+1} − x_i, i = 0..k.
|
||||
let n = x.len();
|
||||
let dr: Vec<Vec<f64>> = (0..k)
|
||||
.map(|i| (0..n).map(|j| self.rs[i + 1][j] - self.rs[i][j]).collect())
|
||||
.collect();
|
||||
let dx: Vec<Vec<f64>> = (0..k)
|
||||
.map(|i| (0..n).map(|j| self.xs[i + 1][j] - self.xs[i][j]).collect())
|
||||
.collect();
|
||||
// Normal equations (ΔRᵀΔR) γ = ΔRᵀ r, Tikhonov-regularised.
|
||||
let mut a = vec![vec![0.0; k]; k];
|
||||
let mut b = vec![0.0; k];
|
||||
let mut trace = 0.0;
|
||||
for i in 0..k {
|
||||
for l in 0..k {
|
||||
a[i][l] = dr[i].iter().zip(&dr[l]).map(|(p, q)| p * q).sum();
|
||||
}
|
||||
trace += a[i][i];
|
||||
b[i] = dr[i].iter().zip(r).map(|(p, q)| p * q).sum();
|
||||
}
|
||||
for i in 0..k {
|
||||
a[i][i] += 1e-10 * trace.max(1e-300);
|
||||
}
|
||||
let gamma = solve_small(a, b);
|
||||
(0..n)
|
||||
.map(|j| {
|
||||
let mut v = x[j] + r[j];
|
||||
for i in 0..k {
|
||||
v -= gamma[i] * (dx[i][j] + dr[i][j]);
|
||||
}
|
||||
v
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gaussian elimination with partial pivoting on a tiny dense system.
|
||||
fn solve_small(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Vec<f64> {
|
||||
let k = b.len();
|
||||
for col in 0..k {
|
||||
let piv = (col..k)
|
||||
.max_by(|&i, &j| a[i][col].abs().partial_cmp(&a[j][col].abs()).unwrap())
|
||||
.unwrap();
|
||||
a.swap(col, piv);
|
||||
b.swap(col, piv);
|
||||
let d = a[col][col];
|
||||
if d.abs() <= 1e-300 {
|
||||
continue;
|
||||
}
|
||||
for row in col + 1..k {
|
||||
let f = a[row][col] / d;
|
||||
for c in col..k {
|
||||
a[row][c] -= f * a[col][c];
|
||||
}
|
||||
b[row] -= f * b[col];
|
||||
}
|
||||
}
|
||||
let mut x = vec![0.0; k];
|
||||
for row in (0..k).rev() {
|
||||
let mut v = b[row];
|
||||
for c in row + 1..k {
|
||||
v -= a[row][c] * x[c];
|
||||
}
|
||||
x[row] = if a[row][row].abs() > 1e-300 {
|
||||
v / a[row][row]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
impl OversetPisoSolver {
|
||||
/// Compose a configured background (sides, sources, boundary data) and
|
||||
/// a configured patch (its `Inner` side velocity via
|
||||
/// `set_side_velocity`) on a background of `nx × ny` cells, spacing
|
||||
/// `dx, dy`. Builds the overlap, installs the hole/fringe mask on the
|
||||
/// background and the acceptor ring on the patch.
|
||||
pub fn new(
|
||||
mut background: EmbeddedPisoSolver,
|
||||
mut patch: CurvilinearPisoSolver,
|
||||
grid: (usize, usize, f64, f64),
|
||||
params: OversetParameters,
|
||||
) -> CfdResult<Self> {
|
||||
let (nx, ny, dx, dy) = grid;
|
||||
let overlap = OverlapMap::build(patch.mesh(), nx, ny, dx, dy, params.overlap_rows)?;
|
||||
background.set_overlap(overlap.background_mask(), overlap.fringe_flags());
|
||||
// Each background solve must be accurate below the Schwarz stop, or
|
||||
// the exchanged values never settle (measured at the default 1e-2).
|
||||
background.set_inner_stop_factor(0.1 * params.schwarz_tolerance);
|
||||
patch.set_acceptor_ring(true);
|
||||
Ok(Self {
|
||||
background,
|
||||
patch,
|
||||
overlap,
|
||||
params,
|
||||
grid,
|
||||
pending: None,
|
||||
acceptor_warm: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The background solver.
|
||||
pub fn background(&self) -> &EmbeddedPisoSolver {
|
||||
&self.background
|
||||
}
|
||||
/// The patch solver.
|
||||
pub fn patch(&self) -> &CurvilinearPisoSolver {
|
||||
&self.patch
|
||||
}
|
||||
/// The current overlap map.
|
||||
pub fn overlap(&self) -> &OverlapMap {
|
||||
&self.overlap
|
||||
}
|
||||
/// Parameters.
|
||||
pub fn parameters(&self) -> &OversetParameters {
|
||||
&self.params
|
||||
}
|
||||
/// The patch's time (the background's agrees).
|
||||
pub fn time(&self) -> f64 {
|
||||
self.patch.time()
|
||||
}
|
||||
|
||||
/// Name the patch geometry the next step ends on (see
|
||||
/// `CurvilinearPisoSolver::set_mesh`; same topology; a fraction of a
|
||||
/// background cell per step).
|
||||
pub fn set_patch_mesh(&mut self, next: PatchMesh) -> CfdResult<()> {
|
||||
self.patch.set_mesh(next.clone())?;
|
||||
self.pending = Some(next);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stamp both exchanges from the current fields and initialise the
|
||||
/// background (boundary data at the current time). Call once after the
|
||||
/// fields are set.
|
||||
pub fn initialize(&mut self, field: &mut OversetField) -> CfdResult<()> {
|
||||
self.exchange(field);
|
||||
self.background.initialize(&mut field.background)
|
||||
}
|
||||
|
||||
/// The exchange for the next step: prescribed background faces and
|
||||
/// fringe `p` from the patch's cell field; acceptor `u, v, p` from the
|
||||
/// background.
|
||||
fn exchange(&self, field: &mut OversetField) {
|
||||
self.overlap
|
||||
.stamp_fringe_faces(&mut field.background, &field.patch.u, &field.patch.v);
|
||||
let p = self.overlap.fringe_cell_values(&field.patch.p);
|
||||
self.overlap.stamp_fringe_cells(&mut field.background.p, &p);
|
||||
let acc = self
|
||||
.overlap
|
||||
.acceptor_values(&field.background, &field.background.p);
|
||||
self.patch.stamp_acceptors(&mut field.patch, &acc);
|
||||
}
|
||||
|
||||
/// Capture the state.
|
||||
pub fn snapshot(&self) -> OversetSolverState {
|
||||
OversetSolverState {
|
||||
background: self.background.snapshot(),
|
||||
patch: self.patch.snapshot(),
|
||||
overlap: self.overlap.clone(),
|
||||
pending: self.pending.clone(),
|
||||
acceptor_warm: self.acceptor_warm.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore a captured state.
|
||||
pub fn restore(&mut self, state: &OversetSolverState) {
|
||||
self.background.restore(&state.background);
|
||||
self.patch.restore(&state.patch);
|
||||
self.overlap = state.overlap.clone();
|
||||
self.pending = state.pending.clone();
|
||||
self.acceptor_warm = state.acceptor_warm.clone();
|
||||
// The background's mask and fringe flags come back with its own
|
||||
// state; its fringe Dirichlet data are transient (set every round).
|
||||
}
|
||||
|
||||
/// Advance both meshes one step of `dt`.
|
||||
pub async fn advance(&mut self, field: &mut OversetField, dt: f64) -> CfdResult<OversetResult> {
|
||||
let (nx, ny, dx, dy) = self.grid;
|
||||
|
||||
// 1. If the patch moves, the overlap follows the NEXT mesh before any
|
||||
// predictor runs: the background is reclassified (fresh cells
|
||||
// filled from neighbours), and the fringe is re-stamped from the
|
||||
// patch's previous CORRECTED cell field through the new donors —
|
||||
// the same numbers as the static path's exchange when the mesh did
|
||||
// not move, so a stationary patch through this path is the static
|
||||
// path to the bit.
|
||||
let mut reclassified = 0usize;
|
||||
let mut fresh = 0usize;
|
||||
if self.pending.take().is_some() {
|
||||
let next = self
|
||||
.patch
|
||||
.next_mesh()
|
||||
.expect("set_mesh named the next mesh");
|
||||
let new = OverlapMap::build(next, nx, ny, dx, dy, self.params.overlap_rows)?;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let (was, now) = (self.overlap.class(j, i), new.class(j, i));
|
||||
if was != now {
|
||||
reclassified += 1;
|
||||
if was == CellClass::Hole && now == CellClass::Active {
|
||||
fresh += 1;
|
||||
// Fill from the neighbours that already carry a
|
||||
// pressure (the embedded solver's fresh-cell rule).
|
||||
let (mut sum, mut count) = (0.0, 0usize);
|
||||
for (jj, ii) in [
|
||||
(j, i + 1),
|
||||
(j, i.wrapping_sub(1)),
|
||||
(j + 1, i),
|
||||
(j.wrapping_sub(1), i),
|
||||
] {
|
||||
if jj < ny
|
||||
&& ii < nx
|
||||
&& self.overlap.class(jj, ii) != CellClass::Hole
|
||||
{
|
||||
sum += field.background.p[(jj, ii)];
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
field.background.p[(j, i)] = sum / count as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.overlap = new;
|
||||
self.background
|
||||
.set_overlap(self.overlap.background_mask(), self.overlap.fringe_flags());
|
||||
self.overlap
|
||||
.stamp_fringe_faces(&mut field.background, &field.patch.u, &field.patch.v);
|
||||
let p = self.overlap.fringe_cell_values(&field.patch.p);
|
||||
self.overlap.stamp_fringe_cells(&mut field.background.p, &p);
|
||||
}
|
||||
|
||||
// 1b. Patch predictor (swaps in the pending mesh).
|
||||
let patch_start = self.patch.begin_step(&mut field.patch, dt)?;
|
||||
|
||||
// 2. Background predictor.
|
||||
let bg_start = self.background.begin_step(&mut field.background, dt)?;
|
||||
|
||||
// 3. Correctors: alternating Schwarz on the acceptor p' vector `a`
|
||||
// (patch solve with Dirichlet a → fringe p' → background solve →
|
||||
// G(a)), Anderson-accelerated, warm-started in the first
|
||||
// corrector from the previous step's converged `a`.
|
||||
let fringe_cells: Vec<(usize, usize)> = self
|
||||
.overlap
|
||||
.fringe_cells
|
||||
.iter()
|
||||
.map(|e| (e.j, e.i))
|
||||
.collect();
|
||||
let n_acc = self.overlap.acceptors.len();
|
||||
let mut rounds = Vec::with_capacity(self.params.corrector_steps);
|
||||
let mut schwarz_converged = true;
|
||||
let mut residual_history = Vec::new();
|
||||
let mut final_residual = f64::INFINITY;
|
||||
let mut patch_iterations = 0usize;
|
||||
let mut patch_converged = true;
|
||||
let trace_rounds = std::env::var("RTX_OVERSET_ROUNDS").is_ok();
|
||||
// The stop is relative to the STEP's pressure-correction scale (the
|
||||
// first corrector's): a later corrector's own p' is a mop-up of the
|
||||
// size of the inner solver's absolute-stop noise (≈ 1e-9 in flux
|
||||
// units is ≈ 1e-9/dt² in pressure — 0.02 here, the whole second
|
||||
// correction), so a stop relative to its own magnitude cannot be
|
||||
// met and would only feed noise to the acceleration.
|
||||
let mut step_scale = 0.0_f64;
|
||||
for corrector in 0..self.params.corrector_steps.max(1) {
|
||||
let mut a: Vec<f64> = if corrector == 0 && self.acceptor_warm.len() == n_acc {
|
||||
self.acceptor_warm.clone()
|
||||
} else {
|
||||
vec![0.0; n_acc]
|
||||
};
|
||||
let mut anderson = Anderson::new(self.params.anderson_depth);
|
||||
let mut patch_pc = vec![0.0; self.patch.mesh().cell_count()];
|
||||
let mut done = false;
|
||||
let mut used = 0usize;
|
||||
for round in 0..self.params.max_rounds.max(1) {
|
||||
used = round + 1;
|
||||
// Patch with Dirichlet a.
|
||||
self.patch.set_acceptor_correction(&a);
|
||||
match self.patch.solve_correction(&field.patch, dt) {
|
||||
Some((pc, out)) => {
|
||||
patch_iterations += out.iterations;
|
||||
patch_converged &= out.converged;
|
||||
patch_pc = pc;
|
||||
}
|
||||
None => patch_pc.iter_mut().for_each(|v| *v = 0.0),
|
||||
}
|
||||
// Background with the fringe p' from the patch.
|
||||
let fringe_vals = self.overlap.fringe_cell_values(&patch_pc);
|
||||
self.background
|
||||
.set_fringe_correction(&fringe_cells, &fringe_vals);
|
||||
self.background.solve_correction(
|
||||
&mut field.background,
|
||||
dt,
|
||||
corrector == 0 || round > 0,
|
||||
)?;
|
||||
let g = self.overlap.acceptor_scalar(&field.background.p_prime);
|
||||
let r: Vec<f64> = g.iter().zip(&a).map(|(x, y)| x - y).collect();
|
||||
let g_max = g.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
|
||||
step_scale = step_scale.max(g_max);
|
||||
let scale = step_scale.max(1e-300);
|
||||
let change = r.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
|
||||
if trace_rounds {
|
||||
println!(
|
||||
" corrector {corrector} round {round}: |G(a) − a| {change:.3e} / step scale {scale:.3e} = {:.3e} (max|G(a)| {g_max:.3e})",
|
||||
change / scale
|
||||
);
|
||||
}
|
||||
if change <= self.params.schwarz_tolerance * scale {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
let next = anderson.next(&a, &r);
|
||||
// Guard: an extrapolation far beyond the data is noise-driven;
|
||||
// take the plain Schwarz step instead.
|
||||
let next_max = next.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
|
||||
a = if next_max > 4.0 * g_max.max(scale) {
|
||||
g.clone()
|
||||
} else {
|
||||
next
|
||||
};
|
||||
}
|
||||
schwarz_converged &= done;
|
||||
rounds.push(used);
|
||||
if corrector == 0 {
|
||||
self.acceptor_warm = a.clone();
|
||||
}
|
||||
// One application on each mesh (the patch with the a it solved).
|
||||
let residual = self.background.apply_correction(&mut field.background, dt);
|
||||
residual_history.push(residual);
|
||||
final_residual = residual;
|
||||
self.patch
|
||||
.apply_correction_pub(&mut field.patch, &patch_pc, dt);
|
||||
if corrector + 1 < self.params.corrector_steps.max(1) {
|
||||
field.background.copy_to_starred();
|
||||
}
|
||||
}
|
||||
let patch_max_div = self.patch.max_divergence_pub(&field.patch.flux);
|
||||
|
||||
// 4. The exchange for the next step, then the clocks.
|
||||
self.exchange(field);
|
||||
let outer = self.overlap.acceptor_outer_velocity(&field.background);
|
||||
let (patch_defect, scale) = self.patch.acceptor_mass_defect(&field.patch, &outer);
|
||||
let bg_defect = self.overlap.background_mass_defect(&field.background);
|
||||
let bg = self.background.end_step(
|
||||
&mut field.background,
|
||||
&bg_start,
|
||||
residual_history,
|
||||
rounds.len(),
|
||||
final_residual,
|
||||
);
|
||||
self.patch.end_step(
|
||||
&patch_start,
|
||||
rounds.len(),
|
||||
patch_max_div,
|
||||
patch_iterations,
|
||||
patch_converged,
|
||||
);
|
||||
if (self.patch.time() - self.background.time()).abs()
|
||||
> 1e-12 * self.patch.time().abs().max(1.0)
|
||||
{
|
||||
return Err(CfdError::invalid_parameter(
|
||||
"overset: background and patch clocks disagree".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(OversetResult {
|
||||
rounds,
|
||||
schwarz_converged,
|
||||
background_residual: bg.solver_result.final_residual,
|
||||
patch_max_divergence: patch_max_div,
|
||||
patch_poisson_iterations: patch_iterations,
|
||||
patch_converged,
|
||||
background_mass_defect: bg_defect,
|
||||
patch_mass_defect: patch_defect,
|
||||
overlap_flux_scale: scale,
|
||||
reclassified_cells: reclassified,
|
||||
fresh_cells: fresh,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
//! The overlap between the fixed background grid and the curvilinear patch
|
||||
//! (overset A-P2, `docs/overset_metal_campaign.md` §2.1, §5.9).
|
||||
//!
|
||||
//! Background cells are classified from the patch's own indices — no
|
||||
//! signed-distance field: a cell whose centre lies inside the body (the
|
||||
//! patch's inner ring, when periodic) or inside a patch cell with
|
||||
//! `k ≤ nn − 1 − overlap_rows` is a HOLE; a non-hole cell 4-adjacent to a
|
||||
//! hole is FRINGE (no continuity equation; `p` and `p'` Dirichlet from the
|
||||
//! patch; its faces that are not shared with an active cell prescribed
|
||||
//! from the patch); everything else is ACTIVE. The patch's outer row of
|
||||
//! cells (`k = nn − 1`) are ACCEPTORS: `u, v, p` bilinear from the
|
||||
//! background's staggered lattices, no momentum or continuity equation.
|
||||
//!
|
||||
//! Patch → background interpolation is bilinear in the DUAL quad — the four
|
||||
//! cell centres `(k,i) (k,i+1) (k+1,i+1) (k+1,i)` — by inverse bilinear
|
||||
//! mapping (Newton); background → patch is bilinear on each staggered
|
||||
//! lattice. Both second order (`tests/overset_interp.rs`).
|
||||
//!
|
||||
//! Two invariants are asserted at build, so a thin patch fails loudly
|
||||
//! instead of coupling acceptors to acceptors: every fringe donor quad
|
||||
//! uses patch cells `k ≤ nn − 2` (never an acceptor), and every acceptor
|
||||
//! donor lattice node is an active cell / a fluid face. The depth budget
|
||||
//! behind `overlap_rows`: from the patch's outer boundary inward, the
|
||||
//! acceptor centre sits ½ outer cell in, its bilinear stencil reaches one
|
||||
//! background cell further, the fringe ring is one background cell thick,
|
||||
//! and the outer curve's wobble adds its amplitude — about 2.9 h with the
|
||||
//! outer spacing ≈ h. Three overlap rows (≈ 2.5 h with a 3× stretch) were
|
||||
//! measured to fail exactly there (acceptor 32's p donor landed on a fringe
|
||||
//! cell); four rows (≈ 3.2 h) is the default.
|
||||
|
||||
use crate::error::{CfdError, CfdResult};
|
||||
use crate::mesh::PatchMesh;
|
||||
use crate::solvers::incompressible::embedded_body::{EmbeddedMask, FaceKind};
|
||||
use crate::solvers::incompressible::flow_field::FlowField;
|
||||
|
||||
/// Background cell class.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CellClass {
|
||||
/// Carries continuity; its pressure is an unknown.
|
||||
Active,
|
||||
/// Ring around the hole: Dirichlet `p`, prescribed outer faces.
|
||||
Fringe,
|
||||
/// Under the patch (or in the body): never read.
|
||||
Hole,
|
||||
}
|
||||
|
||||
/// Bilinear weights on four patch cells (a dual quad).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DualDonor {
|
||||
/// The four cells, in the dual quad's order.
|
||||
pub cells: [usize; 4],
|
||||
/// Their weights (sum to one).
|
||||
pub w: [f64; 4],
|
||||
}
|
||||
|
||||
/// Bilinear weights on four lattice nodes of a staggered field.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LatticeDonor {
|
||||
/// Row and column of the lower-left node.
|
||||
pub j0: usize,
|
||||
/// See `j0`.
|
||||
pub i0: usize,
|
||||
/// Weights for `(j0,i0) (j0,i0+1) (j0+1,i0) (j0+1,i0+1)`.
|
||||
pub w: [f64; 4],
|
||||
}
|
||||
|
||||
impl LatticeDonor {
|
||||
#[inline]
|
||||
fn value(&self, m: &nalgebra::DMatrix<f64>) -> f64 {
|
||||
let (j, i) = (self.j0, self.i0);
|
||||
self.w[0] * m[(j, i)]
|
||||
+ self.w[1] * m[(j, i + 1)]
|
||||
+ self.w[2] * m[(j + 1, i)]
|
||||
+ self.w[3] * m[(j + 1, i + 1)]
|
||||
}
|
||||
}
|
||||
|
||||
/// A background fringe cell or face with its patch donor.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FringeEntry {
|
||||
/// Row.
|
||||
pub j: usize,
|
||||
/// Column.
|
||||
pub i: usize,
|
||||
/// Donor.
|
||||
pub donor: DualDonor,
|
||||
}
|
||||
|
||||
/// A patch acceptor cell with its background donors.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Acceptor {
|
||||
/// Patch cell index.
|
||||
pub cell: usize,
|
||||
/// Donor on the u lattice `(i dx, (j + ½) dy)`.
|
||||
pub u: LatticeDonor,
|
||||
/// Donor on the v lattice `((i + ½) dx, j dy)`.
|
||||
pub v: LatticeDonor,
|
||||
/// Donor on the cell-centre lattice.
|
||||
pub p: LatticeDonor,
|
||||
/// Donors of the background velocity at the acceptor's OUTER face
|
||||
/// centre (u and v lattices), for the mass-defect measure.
|
||||
pub outer_u: LatticeDonor,
|
||||
/// See `outer_u`.
|
||||
pub outer_v: LatticeDonor,
|
||||
}
|
||||
|
||||
/// The classification and the donors of one patch position.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OverlapMap {
|
||||
nx: usize,
|
||||
ny: usize,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
class: Vec<CellClass>,
|
||||
/// Fringe cells (Dirichlet `p`, `p'`).
|
||||
pub fringe_cells: Vec<FringeEntry>,
|
||||
/// Prescribed u faces with donors.
|
||||
pub fringe_u: Vec<FringeEntry>,
|
||||
/// Prescribed v faces with donors.
|
||||
pub fringe_v: Vec<FringeEntry>,
|
||||
/// Acceptor cells on the patch's outer row.
|
||||
pub acceptors: Vec<Acceptor>,
|
||||
/// Patch rows searched for fringe donors (`nn − 1 − overlap_rows − 1 ..= nn − 2`).
|
||||
pub donor_rows: (usize, usize),
|
||||
hole_cells: usize,
|
||||
}
|
||||
|
||||
/// Number of patch rows below the acceptor row that stay non-hole.
|
||||
pub const DEFAULT_OVERLAP_ROWS: usize = 4;
|
||||
|
||||
impl OverlapMap {
|
||||
/// Classify the `nx × ny` background of spacing `dx, dy` against
|
||||
/// `patch`, with `overlap_rows` patch rows (below the acceptor row)
|
||||
/// kept non-hole. Errors when a fringe cell has no interior donor or an
|
||||
/// acceptor's donors are not all active (the patch is too thin or too
|
||||
/// close to the domain boundary).
|
||||
pub fn build(
|
||||
patch: &PatchMesh,
|
||||
nx: usize,
|
||||
ny: usize,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
overlap_rows: usize,
|
||||
) -> CfdResult<Self> {
|
||||
let (ns, nn) = (patch.ns(), patch.nn());
|
||||
if nn < overlap_rows + 3 {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: patch needs nn >= overlap_rows + 3 = {}, got {nn}",
|
||||
overlap_rows + 3
|
||||
)));
|
||||
}
|
||||
let hole_row_max = nn - 1 - overlap_rows; // k <= this is hole
|
||||
let primal = QuadIndex::primal(patch);
|
||||
let body = body_polygon(patch);
|
||||
|
||||
// 1. Cells.
|
||||
let mut class = vec![CellClass::Active; nx * ny];
|
||||
let mut hole_cells = 0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let x = (i as f64 + 0.5) * dx;
|
||||
let y = (j as f64 + 0.5) * dy;
|
||||
let in_hole = match primal.locate(patch, x, y) {
|
||||
Some(c) => patch.cell_ki(c).0 <= hole_row_max,
|
||||
None => body
|
||||
.as_ref()
|
||||
.is_some_and(|poly| point_in_polygon(poly, x, y)),
|
||||
};
|
||||
if in_hole {
|
||||
class[j * nx + i] = CellClass::Hole;
|
||||
hole_cells += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if class[j * nx + i] != CellClass::Hole {
|
||||
let hole = |jj: usize, ii: usize| class[jj * nx + ii] == CellClass::Hole;
|
||||
if (i > 0 && hole(j, i - 1))
|
||||
|| (i + 1 < nx && hole(j, i + 1))
|
||||
|| (j > 0 && hole(j - 1, i))
|
||||
|| (j + 1 < ny && hole(j + 1, i))
|
||||
{
|
||||
class[j * nx + i] = CellClass::Fringe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let c = class[j * nx + i];
|
||||
if c != CellClass::Active && (i == 0 || j == 0 || i + 1 == nx || j + 1 == ny) {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: {c:?} cell ({j}, {i}) touches the domain boundary"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fringe donors in the dual quads of rows k ∈ [k_lo, nn − 2].
|
||||
let k_hi = nn - 2;
|
||||
let k_lo = hole_row_max.saturating_sub(1);
|
||||
let dual = QuadIndex::dual(patch, k_lo, k_hi);
|
||||
let is_active = |jj: usize, ii: usize| class[jj * nx + ii] == CellClass::Active;
|
||||
let mut fringe_cells = Vec::new();
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if class[j * nx + i] == CellClass::Fringe {
|
||||
let x = (i as f64 + 0.5) * dx;
|
||||
let y = (j as f64 + 0.5) * dy;
|
||||
let donor = dual.dual_donor(patch, x, y).ok_or_else(|| {
|
||||
CfdError::mesh(format!(
|
||||
"overset: fringe cell ({j}, {i}) at ({x:.4}, {y:.4}) has no interior \
|
||||
patch donor in rows {k_lo}..={k_hi} — patch too thin"
|
||||
))
|
||||
})?;
|
||||
fringe_cells.push(FringeEntry { j, i, donor });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Prescribed faces: interior faces with no active neighbour, that
|
||||
// have a donor in the band (deeper ones are never read).
|
||||
let mut fringe_u = Vec::new();
|
||||
for j in 0..ny {
|
||||
for i in 1..nx {
|
||||
if !is_active(j, i - 1) && !is_active(j, i) {
|
||||
let touches_fringe = class[j * nx + i - 1] == CellClass::Fringe
|
||||
|| class[j * nx + i] == CellClass::Fringe;
|
||||
let (x, y) = (i as f64 * dx, (j as f64 + 0.5) * dy);
|
||||
match dual.dual_donor(patch, x, y) {
|
||||
Some(donor) => fringe_u.push(FringeEntry { j, i, donor }),
|
||||
None if touches_fringe => {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: fringe u face ({j}, {i}) has no interior patch donor"
|
||||
)));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut fringe_v = Vec::new();
|
||||
for j in 1..ny {
|
||||
for i in 0..nx {
|
||||
if !is_active(j - 1, i) && !is_active(j, i) {
|
||||
let touches_fringe = class[(j - 1) * nx + i] == CellClass::Fringe
|
||||
|| class[j * nx + i] == CellClass::Fringe;
|
||||
let (x, y) = ((i as f64 + 0.5) * dx, j as f64 * dy);
|
||||
match dual.dual_donor(patch, x, y) {
|
||||
Some(donor) => fringe_v.push(FringeEntry { j, i, donor }),
|
||||
None if touches_fringe => {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: fringe v face ({j}, {i}) has no interior patch donor"
|
||||
)));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Acceptors: patch row nn − 1, lattice donors on the background.
|
||||
let u_fluid = |jj: usize, ii: usize| {
|
||||
// A u face is fluid unless both adjacent cells are non-active.
|
||||
ii == 0 || ii == nx || is_active(jj, ii - 1) || is_active(jj, ii)
|
||||
};
|
||||
let v_fluid = |jj: usize, ii: usize| {
|
||||
jj == 0 || jj == ny || is_active(jj - 1, ii) || is_active(jj, ii)
|
||||
};
|
||||
let mut acceptors = Vec::with_capacity(ns);
|
||||
for i in 0..ns {
|
||||
let cell = patch.cell(nn - 1, i);
|
||||
let xy = patch.centre(cell);
|
||||
let u = lattice_donor(xy, 0.0, 0.5, dx, dy, nx + 1, ny)?;
|
||||
let v = lattice_donor(xy, 0.5, 0.0, dx, dy, nx, ny + 1)?;
|
||||
let p = lattice_donor(xy, 0.5, 0.5, dx, dy, nx, ny)?;
|
||||
let oc = patch.faces()[patch.nface(nn, i)].centre;
|
||||
let outer_u = lattice_donor(oc, 0.0, 0.5, dx, dy, nx + 1, ny)?;
|
||||
let outer_v = lattice_donor(oc, 0.5, 0.0, dx, dy, nx, ny + 1)?;
|
||||
for (dj, di) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
|
||||
if !u_fluid(u.j0 + dj, u.i0 + di) {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: acceptor {i} u donor ({}, {}) is a prescribed face",
|
||||
u.j0 + dj,
|
||||
u.i0 + di
|
||||
)));
|
||||
}
|
||||
if !v_fluid(v.j0 + dj, v.i0 + di) {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: acceptor {i} v donor ({}, {}) is a prescribed face",
|
||||
v.j0 + dj,
|
||||
v.i0 + di
|
||||
)));
|
||||
}
|
||||
if !is_active(p.j0 + dj, p.i0 + di) {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: acceptor {i} p donor cell ({}, {}) is not active",
|
||||
p.j0 + dj,
|
||||
p.i0 + di
|
||||
)));
|
||||
}
|
||||
}
|
||||
acceptors.push(Acceptor {
|
||||
cell,
|
||||
u,
|
||||
v,
|
||||
p,
|
||||
outer_u,
|
||||
outer_v,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
nx,
|
||||
ny,
|
||||
dx,
|
||||
dy,
|
||||
class,
|
||||
fringe_cells,
|
||||
fringe_u,
|
||||
fringe_v,
|
||||
acceptors,
|
||||
donor_rows: (k_lo, k_hi),
|
||||
hole_cells,
|
||||
})
|
||||
}
|
||||
|
||||
/// Class of background cell `(j, i)`.
|
||||
pub fn class(&self, j: usize, i: usize) -> CellClass {
|
||||
self.class[j * self.nx + i]
|
||||
}
|
||||
/// Number of hole cells.
|
||||
pub fn hole_cells(&self) -> usize {
|
||||
self.hole_cells
|
||||
}
|
||||
/// Number of fringe cells.
|
||||
pub fn fringe_count(&self) -> usize {
|
||||
self.fringe_cells.len()
|
||||
}
|
||||
/// Grid dimensions `(nx, ny, dx, dy)`.
|
||||
pub fn grid(&self) -> (usize, usize, f64, f64) {
|
||||
(self.nx, self.ny, self.dx, self.dy)
|
||||
}
|
||||
|
||||
/// The background mask for the embedded solver: active cells fluid;
|
||||
/// a face is `Fluid` unless both adjacent cells are non-active, in
|
||||
/// which case it is prescribed (`Ghost`, with no reconstruction data —
|
||||
/// its value is stamped from the patch).
|
||||
pub fn background_mask(&self) -> EmbeddedMask {
|
||||
let (nx, ny) = (self.nx, self.ny);
|
||||
let cell_fluid: Vec<bool> = self.class.iter().map(|&c| c == CellClass::Active).collect();
|
||||
let active = |j: usize, i: usize| cell_fluid[j * nx + i];
|
||||
let mut u_kind = vec![FaceKind::Fluid; ny * (nx + 1)];
|
||||
for j in 0..ny {
|
||||
for i in 1..nx {
|
||||
if !active(j, i - 1) && !active(j, i) {
|
||||
u_kind[j * (nx + 1) + i] = FaceKind::Ghost;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut v_kind = vec![FaceKind::Fluid; (ny + 1) * nx];
|
||||
for j in 1..ny {
|
||||
for i in 0..nx {
|
||||
if !active(j - 1, i) && !active(j, i) {
|
||||
v_kind[j * nx + i] = FaceKind::Ghost;
|
||||
}
|
||||
}
|
||||
}
|
||||
EmbeddedMask::from_classification(nx, ny, self.dx, self.dy, cell_fluid, u_kind, v_kind)
|
||||
}
|
||||
|
||||
/// Per-cell Dirichlet flags for the background projection: `true` on
|
||||
/// fringe cells.
|
||||
pub fn fringe_flags(&self) -> Vec<bool> {
|
||||
self.class.iter().map(|&c| c == CellClass::Fringe).collect()
|
||||
}
|
||||
|
||||
/// Interpolate a patch cell field to the fringe cells (order of
|
||||
/// `fringe_cells`).
|
||||
pub fn fringe_cell_values(&self, patch_vals: &[f64]) -> Vec<f64> {
|
||||
self.fringe_cells
|
||||
.iter()
|
||||
.map(|e| dual_value(&e.donor, patch_vals))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Stamp `values` (from [`Self::fringe_cell_values`]) onto a
|
||||
/// background cell field.
|
||||
pub fn stamp_fringe_cells(&self, target: &mut nalgebra::DMatrix<f64>, values: &[f64]) {
|
||||
for (e, &v) in self.fringe_cells.iter().zip(values) {
|
||||
target[(e.j, e.i)] = v;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stamp the prescribed u and v faces of the background from the patch
|
||||
/// cell velocities.
|
||||
pub fn stamp_fringe_faces(&self, field: &mut FlowField, patch_u: &[f64], patch_v: &[f64]) {
|
||||
for e in &self.fringe_u {
|
||||
field.u[(e.j, e.i)] = dual_value(&e.donor, patch_u);
|
||||
}
|
||||
for e in &self.fringe_v {
|
||||
field.v[(e.j, e.i)] = dual_value(&e.donor, patch_v);
|
||||
}
|
||||
}
|
||||
|
||||
/// `(u, v, p)` at every acceptor from the background field (order of
|
||||
/// `acceptors`); `p_source` selects which cell field supplies the
|
||||
/// pressure-like value.
|
||||
pub fn acceptor_values(
|
||||
&self,
|
||||
field: &FlowField,
|
||||
p_source: &nalgebra::DMatrix<f64>,
|
||||
) -> Vec<(f64, f64, f64)> {
|
||||
self.acceptors
|
||||
.iter()
|
||||
.map(|a| {
|
||||
(
|
||||
a.u.value(&field.u),
|
||||
a.v.value(&field.v),
|
||||
a.p.value(p_source),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The background velocity at every acceptor's outer face centre.
|
||||
pub fn acceptor_outer_velocity(&self, field: &FlowField) -> Vec<(f64, f64)> {
|
||||
self.acceptors
|
||||
.iter()
|
||||
.map(|a| (a.outer_u.value(&field.u), a.outer_v.value(&field.v)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A cell-centred background scalar (e.g. `p'`) at every acceptor.
|
||||
pub fn acceptor_scalar(&self, m: &nalgebra::DMatrix<f64>) -> Vec<f64> {
|
||||
self.acceptors.iter().map(|a| a.p.value(m)).collect()
|
||||
}
|
||||
|
||||
/// Background-side overlap mass defect: `Σ_fringe |Σ_f sign F_f|`
|
||||
/// (volume flux), the continuity the fringe cells do not enforce.
|
||||
pub fn background_mass_defect(&self, field: &FlowField) -> f64 {
|
||||
let (dx, dy) = (self.dx, self.dy);
|
||||
self.fringe_cells
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let (j, i) = (e.j, e.i);
|
||||
((field.u[(j, i + 1)] - field.u[(j, i)]) * dy
|
||||
+ (field.v[(j + 1, i)] - field.v[(j, i)]) * dx)
|
||||
.abs()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn dual_value(d: &DualDonor, vals: &[f64]) -> f64 {
|
||||
d.w[0] * vals[d.cells[0]]
|
||||
+ d.w[1] * vals[d.cells[1]]
|
||||
+ d.w[2] * vals[d.cells[2]]
|
||||
+ d.w[3] * vals[d.cells[3]]
|
||||
}
|
||||
|
||||
/// Bilinear donor of point `xy` on a lattice whose node `(j, i)` sits at
|
||||
/// `((i + ox) dx, (j + oy) dy)`, with `cols × rows` nodes.
|
||||
fn lattice_donor(
|
||||
xy: [f64; 2],
|
||||
ox: f64,
|
||||
oy: f64,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
) -> CfdResult<LatticeDonor> {
|
||||
let fx = xy[0] / dx - ox;
|
||||
let fy = xy[1] / dy - oy;
|
||||
if fx < 0.0 || fy < 0.0 || fx >= (cols - 1) as f64 || fy >= (rows - 1) as f64 {
|
||||
return Err(CfdError::mesh(format!(
|
||||
"overset: acceptor at ({:.4}, {:.4}) lies outside the background lattice",
|
||||
xy[0], xy[1]
|
||||
)));
|
||||
}
|
||||
let i0 = fx.floor() as usize;
|
||||
let j0 = fy.floor() as usize;
|
||||
let (a, b) = (fx - i0 as f64, fy - j0 as f64);
|
||||
Ok(LatticeDonor {
|
||||
j0,
|
||||
i0,
|
||||
w: [(1.0 - a) * (1.0 - b), a * (1.0 - b), (1.0 - a) * b, a * b],
|
||||
})
|
||||
}
|
||||
|
||||
/// The patch's inner ring as a closed polygon (periodic patches only).
|
||||
fn body_polygon(patch: &PatchMesh) -> Option<Vec<[f64; 2]>> {
|
||||
patch.periodic()?;
|
||||
Some(
|
||||
(0..patch.ns())
|
||||
.map(|i| patch.node_xy(patch.node(0, i)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Even–odd point-in-polygon.
|
||||
fn point_in_polygon(poly: &[[f64; 2]], x: f64, y: f64) -> bool {
|
||||
let mut inside = false;
|
||||
let n = poly.len();
|
||||
for a in 0..n {
|
||||
let (p, q) = (poly[a], poly[(a + 1) % n]);
|
||||
if (p[1] > y) != (q[1] > y) {
|
||||
let xi = p[0] + (y - p[1]) / (q[1] - p[1]) * (q[0] - p[0]);
|
||||
if x < xi {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
inside
|
||||
}
|
||||
|
||||
/// Is `xy` inside the convex quad `q` (counter-clockwise)?
|
||||
fn point_in_quad(q: &[[f64; 2]; 4], x: f64, y: f64, tol: f64) -> bool {
|
||||
(0..4).all(|a| {
|
||||
let (p, r) = (q[a], q[(a + 1) % 4]);
|
||||
(r[0] - p[0]) * (y - p[1]) - (r[1] - p[1]) * (x - p[0]) >= -tol
|
||||
})
|
||||
}
|
||||
|
||||
/// Bilinear weights of `xy` in the quad `q` (corners in the order
|
||||
/// `(0,0) (1,0) (1,1) (0,1)`), by Newton on the inverse map; `None` if
|
||||
/// Newton does not converge in 12 steps.
|
||||
pub fn inverse_bilinear(q: &[[f64; 2]; 4], x: f64, y: f64) -> Option<[f64; 4]> {
|
||||
let (mut s, mut t) = (0.5, 0.5);
|
||||
let scale = (0..4)
|
||||
.map(|a| (q[a][0] - q[0][0]).abs().max((q[a][1] - q[0][1]).abs()))
|
||||
.fold(0.0, f64::max)
|
||||
.max(1e-300);
|
||||
for _ in 0..12 {
|
||||
let n = [(1.0 - s) * (1.0 - t), s * (1.0 - t), s * t, (1.0 - s) * t];
|
||||
let px = (0..4).map(|a| n[a] * q[a][0]).sum::<f64>() - x;
|
||||
let py = (0..4).map(|a| n[a] * q[a][1]).sum::<f64>() - y;
|
||||
if px.abs().max(py.abs()) <= 1e-14 * scale {
|
||||
return Some(n);
|
||||
}
|
||||
// Jacobian d(px,py)/d(s,t).
|
||||
let dxs = -(1.0 - t) * q[0][0] + (1.0 - t) * q[1][0] + t * q[2][0] - t * q[3][0];
|
||||
let dys = -(1.0 - t) * q[0][1] + (1.0 - t) * q[1][1] + t * q[2][1] - t * q[3][1];
|
||||
let dxt = -(1.0 - s) * q[0][0] - s * q[1][0] + s * q[2][0] + (1.0 - s) * q[3][0];
|
||||
let dyt = -(1.0 - s) * q[0][1] - s * q[1][1] + s * q[2][1] + (1.0 - s) * q[3][1];
|
||||
let det = dxs * dyt - dxt * dys;
|
||||
if det.abs() <= 1e-300 {
|
||||
return None;
|
||||
}
|
||||
s -= (px * dyt - dxt * py) / det;
|
||||
t -= (dxs * py - px * dys) / det;
|
||||
}
|
||||
let n = [(1.0 - s) * (1.0 - t), s * (1.0 - t), s * t, (1.0 - s) * t];
|
||||
let px = (0..4).map(|a| n[a] * q[a][0]).sum::<f64>() - x;
|
||||
let py = (0..4).map(|a| n[a] * q[a][1]).sum::<f64>() - y;
|
||||
(px.abs().max(py.abs()) <= 1e-12 * scale).then_some(n)
|
||||
}
|
||||
|
||||
/// Uniform bins over a set of quads for point location.
|
||||
struct QuadIndex {
|
||||
quads: Vec<([[f64; 2]; 4], [usize; 4])>,
|
||||
x0: f64,
|
||||
y0: f64,
|
||||
bw: f64,
|
||||
bh: f64,
|
||||
nbx: usize,
|
||||
nby: usize,
|
||||
bins: Vec<Vec<usize>>,
|
||||
tol: f64,
|
||||
}
|
||||
|
||||
impl QuadIndex {
|
||||
/// The primal cells: corners are nodes, payload the cell index (×4).
|
||||
fn primal(patch: &PatchMesh) -> Self {
|
||||
let (ns, nn) = (patch.ns(), patch.nn());
|
||||
let mut quads = Vec::with_capacity(ns * nn);
|
||||
for k in 0..nn {
|
||||
for i in 0..ns {
|
||||
let n = [
|
||||
patch.node(k, i),
|
||||
patch.node(k, i + 1),
|
||||
patch.node(k + 1, i + 1),
|
||||
patch.node(k + 1, i),
|
||||
];
|
||||
let c = patch.cell(k, i);
|
||||
quads.push((n.map(|nd| patch.node_xy(nd)), [c; 4]));
|
||||
}
|
||||
}
|
||||
Self::new(quads)
|
||||
}
|
||||
|
||||
/// The dual quads of rows `k_lo..=k_hi` (corners are cell centres,
|
||||
/// payload the four cells), periodic wrap in `i`.
|
||||
fn dual(patch: &PatchMesh, k_lo: usize, k_hi: usize) -> Self {
|
||||
let (ns, nn) = (patch.ns(), patch.nn());
|
||||
let shift = patch.periodic();
|
||||
let cols = if shift.is_some() { ns } else { ns - 1 };
|
||||
let mut quads = Vec::new();
|
||||
for k in k_lo..=k_hi.min(nn - 2) {
|
||||
for i in 0..cols {
|
||||
let i1 = (i + 1) % ns;
|
||||
let wrap = shift.filter(|_| i1 == 0).unwrap_or([0.0; 2]);
|
||||
let cells = [
|
||||
patch.cell(k, i),
|
||||
patch.cell(k, i1),
|
||||
patch.cell(k + 1, i1),
|
||||
patch.cell(k + 1, i),
|
||||
];
|
||||
let mut pts = cells.map(|c| patch.centre(c));
|
||||
pts[1] = [pts[1][0] + wrap[0], pts[1][1] + wrap[1]];
|
||||
pts[2] = [pts[2][0] + wrap[0], pts[2][1] + wrap[1]];
|
||||
quads.push((pts, cells));
|
||||
}
|
||||
}
|
||||
Self::new(quads)
|
||||
}
|
||||
|
||||
fn new(quads: Vec<([[f64; 2]; 4], [usize; 4])>) -> Self {
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (
|
||||
f64::INFINITY,
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
);
|
||||
let mut hmax = 0.0_f64;
|
||||
for (q, _) in &quads {
|
||||
for p in q {
|
||||
x0 = x0.min(p[0]);
|
||||
y0 = y0.min(p[1]);
|
||||
x1 = x1.max(p[0]);
|
||||
y1 = y1.max(p[1]);
|
||||
}
|
||||
for a in 0..4 {
|
||||
let (p, r) = (q[a], q[(a + 1) % 4]);
|
||||
hmax = hmax.max(((r[0] - p[0]).powi(2) + (r[1] - p[1]).powi(2)).sqrt());
|
||||
}
|
||||
}
|
||||
let n = quads.len().max(1);
|
||||
let side = ((n as f64).sqrt().ceil() as usize).max(1);
|
||||
let bw = ((x1 - x0) / side as f64).max(1e-300);
|
||||
let bh = ((y1 - y0) / side as f64).max(1e-300);
|
||||
let mut bins = vec![Vec::new(); side * side];
|
||||
for (idx, (q, _)) in quads.iter().enumerate() {
|
||||
let (mut bx0, mut by0, mut bx1, mut by1) = (usize::MAX, usize::MAX, 0, 0);
|
||||
for p in q {
|
||||
let bx = (((p[0] - x0) / bw).floor() as usize).min(side - 1);
|
||||
let by = (((p[1] - y0) / bh).floor() as usize).min(side - 1);
|
||||
bx0 = bx0.min(bx);
|
||||
by0 = by0.min(by);
|
||||
bx1 = bx1.max(bx);
|
||||
by1 = by1.max(by);
|
||||
}
|
||||
for by in by0..=by1 {
|
||||
for bx in bx0..=bx1 {
|
||||
bins[by * side + bx].push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
quads,
|
||||
x0,
|
||||
y0,
|
||||
bw,
|
||||
bh,
|
||||
nbx: side,
|
||||
nby: side,
|
||||
bins,
|
||||
tol: 1e-12 * hmax * hmax,
|
||||
}
|
||||
}
|
||||
|
||||
fn candidates(&self, x: f64, y: f64) -> &[usize] {
|
||||
let fx = (x - self.x0) / self.bw;
|
||||
let fy = (y - self.y0) / self.bh;
|
||||
if fx < 0.0 || fy < 0.0 || fx >= self.nbx as f64 || fy >= self.nby as f64 {
|
||||
return &[];
|
||||
}
|
||||
&self.bins[(fy as usize) * self.nbx + fx as usize]
|
||||
}
|
||||
|
||||
/// The primal cell containing `(x, y)`.
|
||||
fn locate(&self, _patch: &PatchMesh, x: f64, y: f64) -> Option<usize> {
|
||||
self.candidates(x, y)
|
||||
.iter()
|
||||
.find(|&&q| point_in_quad(&self.quads[q].0, x, y, self.tol))
|
||||
.map(|&q| self.quads[q].1[0])
|
||||
}
|
||||
|
||||
/// The dual donor of `(x, y)`.
|
||||
fn dual_donor(&self, _patch: &PatchMesh, x: f64, y: f64) -> Option<DualDonor> {
|
||||
for &q in self.candidates(x, y) {
|
||||
let (pts, cells) = &self.quads[q];
|
||||
if point_in_quad(pts, x, y, self.tol) {
|
||||
let w = inverse_bilinear(pts, x, y)?;
|
||||
return Some(DualDonor { cells: *cells, w });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user