CI / Format Check (push) Failing after 6s
Performance Benchmarks / Run Benchmarks (push) Failing after 6s
CI / Clippy Check (push) Failing after 5s
CI / Build (ubuntu-latest) (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 7s
Documentation / Build API Documentation (push) Failing after 25s
CI / Build CPU-Only (Explicit) (push) Failing after 1m30s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
1035 lines
40 KiB
Rust
1035 lines
40 KiB
Rust
//! 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>,
|
||
/// Hole cells within two cells of the fringe that have a patch donor in
|
||
/// the widened band: ghost pressures for the momentum-residual
|
||
/// diagnostic (never read by the solver).
|
||
pub hole_p: Vec<FringeEntry>,
|
||
/// Hole–hole u faces the solver never stamps, with a widened-band
|
||
/// donor: ghost velocities for the diagnostic.
|
||
pub ghost_u: Vec<FringeEntry>,
|
||
/// See `ghost_u`.
|
||
pub ghost_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. A centre outside the patch's node bounding box lies in
|
||
// no patch cell and outside the body (which the patch encloses):
|
||
// Active without a point location (PERF-2 P1.2, the same class
|
||
// the search would return).
|
||
let bbox = patch.bounding_box();
|
||
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;
|
||
if x < bbox[0] || x > bbox[1] || y < bbox[2] || y > bbox[3] {
|
||
continue;
|
||
}
|
||
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 });
|
||
}
|
||
}
|
||
}
|
||
// Diagnostic ghosts (never read by the solver): hole cells within
|
||
// two cells of the fringe and the hole–hole faces around them, with
|
||
// donors from a band widened three rows into the hole, so every
|
||
// fringe–hole face's momentum stencil reads a patch value.
|
||
let wide = QuadIndex::dual(patch, k_lo.saturating_sub(3), k_hi);
|
||
let near_fringe = |j: usize, i: usize| {
|
||
let lo_j = j.saturating_sub(2);
|
||
let lo_i = i.saturating_sub(2);
|
||
(lo_j..=(j + 2).min(ny - 1)).any(|jj| {
|
||
(lo_i..=(i + 2).min(nx - 1)).any(|ii| class[jj * nx + ii] == CellClass::Fringe)
|
||
})
|
||
};
|
||
let mut hole_p = Vec::new();
|
||
for j in 0..ny {
|
||
for i in 0..nx {
|
||
if class[j * nx + i] == CellClass::Hole && near_fringe(j, i) {
|
||
let (x, y) = ((i as f64 + 0.5) * dx, (j as f64 + 0.5) * dy);
|
||
if let Some(donor) = wide.dual_donor(patch, x, y) {
|
||
hole_p.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 => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let hole = |jj: usize, ii: usize| class[jj * nx + ii] == CellClass::Hole;
|
||
let stamped_u: std::collections::HashSet<(usize, usize)> =
|
||
fringe_u.iter().map(|e| (e.j, e.i)).collect();
|
||
let stamped_v: std::collections::HashSet<(usize, usize)> =
|
||
fringe_v.iter().map(|e| (e.j, e.i)).collect();
|
||
let mut ghost_u = Vec::new();
|
||
for j in 0..ny {
|
||
for i in 1..nx {
|
||
if hole(j, i - 1) && hole(j, i) && (near_fringe(j, i - 1) || near_fringe(j, i)) {
|
||
let (x, y) = (i as f64 * dx, (j as f64 + 0.5) * dy);
|
||
if stamped_u.contains(&(j, i)) {
|
||
continue;
|
||
}
|
||
if let Some(donor) = wide.dual_donor(patch, x, y) {
|
||
ghost_u.push(FringeEntry { j, i, donor });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let mut ghost_v = Vec::new();
|
||
for j in 1..ny {
|
||
for i in 0..nx {
|
||
if hole(j - 1, i) && hole(j, i) && (near_fringe(j - 1, i) || near_fringe(j, i)) {
|
||
let (x, y) = ((i as f64 + 0.5) * dx, j as f64 * dy);
|
||
if stamped_v.contains(&(j, i)) {
|
||
continue;
|
||
}
|
||
if let Some(donor) = wide.dual_donor(patch, x, y) {
|
||
ghost_v.push(FringeEntry { j, i, donor });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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,
|
||
hole_p,
|
||
ghost_u,
|
||
ghost_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()
|
||
}
|
||
|
||
/// Interpolate a patch cell field to the hole ghost cells (order of
|
||
/// `hole_p`).
|
||
pub fn hole_p_values(&self, patch_vals: &[f64]) -> Vec<f64> {
|
||
self.hole_p
|
||
.iter()
|
||
.map(|e| dual_value(&e.donor, patch_vals))
|
||
.collect()
|
||
}
|
||
|
||
/// Stamp `values` (from [`Self::hole_p_values`]) onto a background
|
||
/// cell field.
|
||
pub fn stamp_hole_p(&self, target: &mut nalgebra::DMatrix<f64>, values: &[f64]) {
|
||
for (e, &v) in self.hole_p.iter().zip(values) {
|
||
target[(e.j, e.i)] = v;
|
||
}
|
||
}
|
||
|
||
/// Stamp the diagnostic ghost faces (`ghost_u`, `ghost_v`) from the
|
||
/// patch cell velocities, onto both `u`/`v` and `u_old`/`v_old`.
|
||
pub fn stamp_ghost_faces(&self, field: &mut FlowField, patch_u: &[f64], patch_v: &[f64]) {
|
||
for e in &self.ghost_u {
|
||
let v = dual_value(&e.donor, patch_u);
|
||
field.u[(e.j, e.i)] = v;
|
||
field.u_old[(e.j, e.i)] = v;
|
||
}
|
||
for e in &self.ghost_v {
|
||
let v = dual_value(&e.donor, patch_v);
|
||
field.v[(e.j, e.i)] = v;
|
||
field.v_old[(e.j, e.i)] = v;
|
||
}
|
||
}
|
||
|
||
/// 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()
|
||
}
|
||
|
||
/// Flux balance at the fringe (Chesshire–Henshaw in spirit): make every
|
||
/// fringe cell divergence-free by adjusting only its PRESCRIBED faces
|
||
/// (never a face shared with an active cell), spreading each cell's
|
||
/// imbalance over them by face length, in Gauss–Seidel sweeps (a face
|
||
/// shared by two fringe cells is corrected by both) until the largest
|
||
/// fringe-cell imbalance is below `tol` (volume flux) or `max_sweeps`
|
||
/// is reached. Returns `(sweeps, worst imbalance)`. This is what removes
|
||
/// the reclassification impulse of A-P3: with the fringe ring a
|
||
/// staircase of the interpolated velocities' mass defect, every
|
||
/// row flip injected that defect in one step (§5.10).
|
||
pub fn balance_fringe_fluxes(
|
||
&self,
|
||
field: &mut FlowField,
|
||
tol: f64,
|
||
max_sweeps: usize,
|
||
) -> (usize, f64) {
|
||
let (nx, ny, dx, dy) = (self.nx, self.ny, self.dx, self.dy);
|
||
let is_prescribed_u: std::collections::HashSet<(usize, usize)> =
|
||
self.fringe_u.iter().map(|e| (e.j, e.i)).collect();
|
||
let is_prescribed_v: std::collections::HashSet<(usize, usize)> =
|
||
self.fringe_v.iter().map(|e| (e.j, e.i)).collect();
|
||
let _ = (nx, ny);
|
||
let mut worst = f64::INFINITY;
|
||
let mut sweeps = 0usize;
|
||
while sweeps < max_sweeps && worst > tol {
|
||
sweeps += 1;
|
||
worst = 0.0;
|
||
for e in &self.fringe_cells {
|
||
let (j, i) = (e.j, e.i);
|
||
let div = (field.u[(j, i + 1)] - field.u[(j, i)]) * dy
|
||
+ (field.v[(j + 1, i)] - field.v[(j, i)]) * dx;
|
||
// Prescribed faces of this cell with their outward sign and length.
|
||
let mut faces: Vec<(bool, usize, usize, f64, f64)> = Vec::with_capacity(4);
|
||
if is_prescribed_u.contains(&(j, i + 1)) {
|
||
faces.push((true, j, i + 1, 1.0, dy));
|
||
}
|
||
if is_prescribed_u.contains(&(j, i)) {
|
||
faces.push((true, j, i, -1.0, dy));
|
||
}
|
||
if is_prescribed_v.contains(&(j + 1, i)) {
|
||
faces.push((false, j + 1, i, 1.0, dx));
|
||
}
|
||
if is_prescribed_v.contains(&(j, i)) {
|
||
faces.push((false, j, i, -1.0, dx));
|
||
}
|
||
let total_len: f64 = faces.iter().map(|f| f.4).sum();
|
||
if total_len == 0.0 {
|
||
worst = worst.max(div.abs());
|
||
continue;
|
||
}
|
||
for (is_u, jj, ii, sign, len) in faces {
|
||
// outward flux change on this face = −div · len / total_len
|
||
let dvel = -sign * div / total_len;
|
||
if is_u {
|
||
field.u[(jj, ii)] += dvel;
|
||
} else {
|
||
field.v[(jj, ii)] += dvel;
|
||
}
|
||
let _ = len;
|
||
}
|
||
}
|
||
for e in &self.fringe_cells {
|
||
let (j, i) = (e.j, e.i);
|
||
let div = (field.u[(j, i + 1)] - field.u[(j, i)]) * dy
|
||
+ (field.v[(j + 1, i)] - field.v[(j, i)]) * dx;
|
||
worst = worst.max(div.abs());
|
||
}
|
||
}
|
||
(sweeps, worst)
|
||
}
|
||
|
||
/// 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.
|
||
/// The force the background transmits INTO the region of cells whose
|
||
/// class satisfies `inside` — the sum over the region's boundary faces
|
||
/// of `sigma·n − rho u (u·n)` with `n` pointing out of the region, the
|
||
/// control-volume formula of `EmbeddedMask::control_volume_force`
|
||
/// without the unsteady term (a settled-state diagnostic). Values are
|
||
/// taken from non-hole cells only: a face next to a hole cell uses the
|
||
/// one-sided stencil from its valid side, so the hole boundary itself
|
||
/// (`inside = Hole`) is evaluated from the fringe's stamped values.
|
||
///
|
||
/// Two regions make the P4 momentum-defect measurement: `Fringe |
|
||
/// Hole` (what the active region passes to the ring) and `Hole` (what
|
||
/// the ring passes on); their difference is the fringe ring's momentum
|
||
/// defect, and the hole boundary against the patch's wall force is the
|
||
/// patch region's.
|
||
pub fn region_force(
|
||
&self,
|
||
field: &FlowField,
|
||
rho: f64,
|
||
mu: f64,
|
||
inside: impl Fn(CellClass) -> bool,
|
||
) -> (f64, f64) {
|
||
let (nx, ny, dx, dy) = (self.nx, self.ny, self.dx, self.dy);
|
||
let valid = |j: isize, i: isize| -> bool {
|
||
j >= 0
|
||
&& i >= 0
|
||
&& (j as usize) < ny
|
||
&& (i as usize) < nx
|
||
&& self.class(j as usize, i as usize) != CellClass::Hole
|
||
};
|
||
let is_in = |j: isize, i: isize| -> bool {
|
||
j >= 0
|
||
&& i >= 0
|
||
&& (j as usize) < ny
|
||
&& (i as usize) < nx
|
||
&& inside(self.class(j as usize, i as usize))
|
||
};
|
||
// Face-located u is valid when either adjacent cell is; likewise v.
|
||
let uf_valid = |j: isize, i: isize| valid(j, i - 1) || valid(j, i);
|
||
let vf_valid = |j: isize, i: isize| valid(j - 1, i) || valid(j, i);
|
||
let u = |j: isize, i: isize| field.u[(j as usize, i as usize)];
|
||
let v = |j: isize, i: isize| field.v[(j as usize, i as usize)];
|
||
let p = |j: isize, i: isize| field.p[(j as usize, i as usize)];
|
||
// Cell-centred v and u (averages of the cell's two faces).
|
||
let v_c = |j: isize, i: isize| 0.5 * (v(j, i) + v(j + 1, i));
|
||
let u_c = |j: isize, i: isize| 0.5 * (u(j, i) + u(j, i + 1));
|
||
// Average of the valid members of a pair, or `None`.
|
||
let pair = |a: Option<f64>, b: Option<f64>| match (a, b) {
|
||
(Some(a), Some(b)) => Some(0.5 * (a + b)),
|
||
(Some(a), None) | (None, Some(a)) => Some(a),
|
||
(None, None) => None,
|
||
};
|
||
// Derivative across `x0 → x1 → x2` (spacing `h`): central when both
|
||
// ends are valid, one-sided otherwise, zero when nothing is.
|
||
let deriv = |m: Option<f64>, c: f64, pl: Option<f64>, h: f64| match (m, pl) {
|
||
(Some(m), Some(pl)) => (pl - m) / (2.0 * h),
|
||
(Some(m), None) => (c - m) / h,
|
||
(None, Some(pl)) => (pl - c) / h,
|
||
(None, None) => 0.0,
|
||
};
|
||
let (mut fx, mut fy) = (0.0, 0.0);
|
||
for jc in 0..ny as isize {
|
||
for ic in 0..nx as isize {
|
||
if !is_in(jc, ic) {
|
||
continue;
|
||
}
|
||
// Vertical faces: west (u face ic, n = −x) and east (ic + 1, +x).
|
||
for (i, sign, nj, ni) in [(ic, -1.0, jc, ic - 1), (ic + 1, 1.0, jc, ic + 1)] {
|
||
if is_in(nj, ni) || nj < 0 || ni < 0 || ni >= nx as isize {
|
||
continue;
|
||
}
|
||
let j = jc;
|
||
let un = u(j, i);
|
||
let p_f = pair(
|
||
valid(j, i - 1).then(|| p(j, i - 1)),
|
||
valid(j, i).then(|| p(j, i)),
|
||
)
|
||
.unwrap_or(0.0);
|
||
let dudx = deriv(
|
||
uf_valid(j, i - 1).then(|| u(j, i - 1)),
|
||
un,
|
||
(i < nx as isize && uf_valid(j, i + 1)).then(|| u(j, i + 1)),
|
||
dx,
|
||
);
|
||
let dudy = deriv(
|
||
(j >= 1 && uf_valid(j - 1, i)).then(|| u(j - 1, i)),
|
||
un,
|
||
(j + 1 < ny as isize && uf_valid(j + 1, i)).then(|| u(j + 1, i)),
|
||
dy,
|
||
);
|
||
let vw = valid(j, i - 1).then(|| v_c(j, i - 1));
|
||
let ve = valid(j, i).then(|| v_c(j, i));
|
||
let dvdx = match (vw, ve) {
|
||
(Some(a), Some(b)) => (b - a) / dx,
|
||
(Some(a), None) => {
|
||
(a - if valid(j, i - 2) { v_c(j, i - 2) } else { a }) / dx
|
||
}
|
||
(None, Some(b)) => {
|
||
((if valid(j, i + 1) { v_c(j, i + 1) } else { b }) - b) / dx
|
||
}
|
||
(None, None) => 0.0,
|
||
};
|
||
let v_f = pair(vw, ve).unwrap_or(0.0);
|
||
let sxx = -p_f + 2.0 * mu * dudx;
|
||
let sxy = mu * (dudy + dvdx);
|
||
fx += sign * (sxx - rho * un * un) * dy;
|
||
fy += sign * (sxy - rho * v_f * un) * dy;
|
||
}
|
||
// Horizontal faces: south (v face jc, n = −y) and north (jc + 1, +y).
|
||
for (j, sign, nj, ni) in [(jc, -1.0, jc - 1, ic), (jc + 1, 1.0, jc + 1, ic)] {
|
||
if is_in(nj, ni) || nj < 0 || nj >= ny as isize {
|
||
continue;
|
||
}
|
||
let i = ic;
|
||
let vn = v(j, i);
|
||
let p_f = pair(
|
||
valid(j - 1, i).then(|| p(j - 1, i)),
|
||
valid(j, i).then(|| p(j, i)),
|
||
)
|
||
.unwrap_or(0.0);
|
||
let dvdy = deriv(
|
||
vf_valid(j - 1, i).then(|| v(j - 1, i)),
|
||
vn,
|
||
(j < ny as isize && vf_valid(j + 1, i)).then(|| v(j + 1, i)),
|
||
dy,
|
||
);
|
||
let dvdx = deriv(
|
||
(i >= 1 && vf_valid(j, i - 1)).then(|| v(j, i - 1)),
|
||
vn,
|
||
(i + 1 < nx as isize && vf_valid(j, i + 1)).then(|| v(j, i + 1)),
|
||
dx,
|
||
);
|
||
let us = valid(j - 1, i).then(|| u_c(j - 1, i));
|
||
let un_ = valid(j, i).then(|| u_c(j, i));
|
||
let dudy = match (us, un_) {
|
||
(Some(a), Some(b)) => (b - a) / dy,
|
||
(Some(a), None) => {
|
||
(a - if valid(j - 2, i) { u_c(j - 2, i) } else { a }) / dy
|
||
}
|
||
(None, Some(b)) => {
|
||
((if valid(j + 1, i) { u_c(j + 1, i) } else { b }) - b) / dy
|
||
}
|
||
(None, None) => 0.0,
|
||
};
|
||
let u_f = pair(us, un_).unwrap_or(0.0);
|
||
let syy = -p_f + 2.0 * mu * dvdy;
|
||
let sxy = mu * (dudy + dvdx);
|
||
fx += sign * (sxy - rho * u_f * vn) * dx;
|
||
fy += sign * (syy - rho * vn * vn) * dx;
|
||
}
|
||
}
|
||
}
|
||
(fx, fy)
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|