Files
rustytorch/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/wall.rs
T
Omar SobhandClaude Fable 5.1 0efb8be745
Documentation / Build API Documentation (push) Failing after 5s
CI / Build CPU-Only (Explicit) (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 5s
CI / Format Check (push) Failing after 17s
CI / Build (ubuntu-latest) (push) Failing after 2m18s
CI / Clippy Check (push) Failing after 2m42s
Performance Benchmarks / Run Benchmarks (push) Successful in 3m6s
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
embedded3 PERF-3 P1-5 (c) part 2 + (d): the device tables persist across steps and are updated in place within the band — one DeviceCut with both phases' aperture / open / activity buffers (the predictor phase is a flag); the faces of the changed cells for apertures, distances, shifts and open flags, the imposition band's faces for the surface velocities, the changed cells for activity and owner, the changed and wall cells for the wall flux, the merged-cell CSR whole; RTX_E3_BAND_CHECK=1 compares every table against a full build (passed); slab CSV byte-identical, device moving/cg green; ny 124 rebuild block 1,910 → 1,578 ms per step (tables 442 → 132)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-20 18:17:28 -05:00

734 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The binary ghost wall — the 2D `EmbeddedMask` on the 3D grid. Cells are
//! fluid where φ(centre) > 0; an interior face between two cells that are
//! not both fluid is a ghost (within 1.5 h of the surface) or solid; a
//! ghost face is prescribed the least-squares linear reconstruction
//! through the fluid nodes of the trilinear stencil around the probe (one
//! cell beyond the face's mirror image along the normal) plus the foot of
//! the normal with its surface velocity — exact for linear fields — with
//! the profile along the normal as the fallback; the ghost faces bounding
//! fluid cells share a flux compatibility correction.
use super::Grid;
use super::body::Body;
use super::cut::CutGeometry;
use super::step::{Boundaries, Side};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaceKind {
Fluid,
Ghost,
Solid,
}
/// The wall treatment of an embedded body.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WallScheme {
/// The 2D binary mask ported (this module).
#[default]
GhostBinary,
/// The apertured cut-cell wall (item 10).
CutCell,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct StencilNode {
pub(crate) idx: usize,
pub(crate) x: f64,
pub(crate) y: f64,
pub(crate) z: f64,
/// The trilinear weight (the fallback profile's interpolation).
pub(crate) weight: f64,
/// The z-direction weight alone: the least-squares weight of the node,
/// so that a z-invariant field fits exactly as the 2D four-node fit
/// (the two planes share the in-plane nodes' unit weight).
pub(crate) zw: f64,
pub(crate) fallback: Option<f64>,
}
#[derive(Debug, Clone)]
pub(super) struct Ghost {
pub(super) idx: usize,
x: f64,
y: f64,
z: f64,
foot: (f64, f64, f64),
u_surface: f64,
s_face: f64,
s_probe: f64,
nodes: Vec<StencilNode>,
/// Outward-from-fluid sign for the compatibility correction (0 when no
/// fluid cell is adjacent).
pub(super) flux_sign: f64,
}
#[derive(Clone)]
pub struct Mask {
pub(super) grid: Grid,
pub(super) periodic_z: bool,
pub(super) cell_fluid: Vec<bool>,
pub(super) u_kind: Vec<FaceKind>,
pub(super) v_kind: Vec<FaceKind>,
pub(super) w_kind: Vec<FaceKind>,
pub(super) u_ghosts: Vec<Ghost>,
pub(super) v_ghosts: Vec<Ghost>,
pub(super) w_ghosts: Vec<Ghost>,
pub(super) anchor: usize,
pub(super) fluid_cells: usize,
/// The cut geometry of the apertured wall (`WallScheme::CutCell`,
/// `cutwall.rs`); `None` on the binary ghost wall.
pub(super) cut: Option<CutGeometry>,
/// The step-averaged apertures `½(αⁿ + αⁿ⁺¹)` of a moving cut wall
/// (the space-time continuity: a cell's volume change over the step
/// equals the flux through the apertures it had during it); `None` =
/// the instantaneous ones.
pub(super) step_apertures: Option<(Vec<f64>, Vec<f64>, Vec<f64>)>,
/// The projection's space-time classification on a moving cut wall:
/// a face is an unknown where its step-averaged aperture is positive,
/// a cell has an equation where it holds fluid at either end of the
/// step (a dying cell empties through the apertures it had); `None` =
/// the instantaneous kinds.
pub(super) step_open: Option<(Vec<bool>, Vec<bool>, Vec<bool>, Vec<bool>)>,
/// Virtual merging (item 10b): the master cell of every small cell
/// (`usize::MAX` = its own row) — a small cell shares its pressure
/// unknown with its largest active face neighbour in the projection.
pub(super) merge_master: Vec<usize>,
/// The predictor's convection scheme (the exchange route replicates
/// its limited fluxes on the faces next to prescribed ones).
pub(super) scheme: crate::solvers::incompressible::ConvectionScheme,
/// The fluid's density (the exchange route's convective flux).
pub(super) density: f64,
/// The cut wall's shear closure order (S2-4).
pub(super) wall_order: u8,
/// The oblique wall distance of the cut faces (S2-5).
pub(super) wall_distance_oblique: bool,
/// The transverse centroid correction and the fine distance floor (S2-6).
pub(super) diffusion_transverse: bool,
pub(super) distance_floor_fine: bool,
pub(super) wall_advancing: bool,
pub(super) exchange_convection_off: bool,
/// The axis-distance implicit wall exchange (S2-5).
pub(super) wall_exchange_axis: bool,
/// S2-7: the momentum control volumes' side apertures from the
/// interpolant on the sides' own corners (host prototype).
/// P1-4: the cells whose operator rows may differ from the previous
/// step's (touched by either mask's build, dilated by one), ascending;
/// `None` on a wall at rest or before the first step.
pub(super) changed_cells: Option<Vec<usize>>,
pub(super) cv_sides_exact: bool,
/// S2-7: the quadratic wall gradient's second point at the neighbour's
/// own centroid distance (host prototype).
pub(super) wall_order2_centroid: bool,
/// S2-7b host prototypes: the axis foot's wall velocity in the solid
/// exchange; the convective sides from the sides' own apertures.
pub(super) wall_exchange_foot: bool,
pub(super) conv_sides_exact: bool,
pub(super) wall_flux_true_normal: bool,
pub(super) wall_foot_centroid: bool,
/// The centroid prototype's pressure-gradient weights per u / v / w face.
/// The centroid-distance cross diffusion (S2-5).
pub(super) diffusion_centroid: bool,
/// The open-part centroid shifts per u / v / w face, three components
/// interleaved (built with `diffusion_centroid`).
pub(super) face_shifts: Option<[Vec<f64>; 3]>,
pub(super) grad_weights: Option<(Vec<f64>, Vec<f64>, Vec<f64>)>,
}
/// The z lattice position of a query: the lower plane index, the upper
/// plane index and the weight of the upper plane. Periodic z wraps; a wall
/// clamps (and a query outside the lattice takes the nearest plane).
pub(crate) fn z_planes(gz: f64, planes: usize, periodic: bool) -> (usize, usize, f64) {
if planes <= 1 {
return (0, 0, 0.0);
}
if periodic {
let n = planes as f64;
let w = gz.rem_euclid(n);
let k0 = w.floor() as usize % planes;
((k0) % planes, (k0 + 1) % planes, w - w.floor())
} else {
let k0 = gz.floor().clamp(0.0, (planes - 2) as f64) as usize;
(k0, k0 + 1, (gz - k0 as f64).clamp(0.0, 1.0))
}
}
/// The nodes of velocity component `c` (0 u, 1 v, 2 w) around `point`
/// with trilinear weights (a single plane of nodes in a direction
/// collapses to one node), clamped in x and y, wrapped or clamped in z;
/// `fallback(idx)` supplies the value of a node that is not a fluid face.
pub(crate) fn stencil_nodes(
point: (f64, f64, f64),
c: usize,
g: Grid,
periodic_z: bool,
fallback: impl Fn(usize) -> Option<f64>,
) -> Vec<StencilNode> {
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
// w faces on a periodic grid: the plane k = nz is the plane 0.
let (gx, gy, gz, max_i, max_j, planes_k) = match c {
0 => (
point.0 / dx,
point.1 / dy - 0.5,
point.2 / dz - 0.5,
nx,
ny - 1,
nz,
),
1 => (
point.0 / dx - 0.5,
point.1 / dy,
point.2 / dz - 0.5,
nx - 1,
ny,
nz,
),
_ => (
point.0 / dx - 0.5,
point.1 / dy - 0.5,
point.2 / dz,
nx - 1,
ny - 1,
if periodic_z { nz } else { nz + 1 },
),
};
let i0 = gx.floor().clamp(0.0, (max_i.max(1) - 1) as f64) as usize;
let j0 = gy.floor().clamp(0.0, (max_j.max(1) - 1) as f64) as usize;
let fx = (gx - i0 as f64).clamp(0.0, 1.0);
let fy = (gy - j0 as f64).clamp(0.0, 1.0);
let (k0, k1, fz) = z_planes(gz, planes_k, periodic_z);
let pos = |k: usize, j: usize, i: usize| match c {
0 => (i as f64 * dx, (j as f64 + 0.5) * dy, (k as f64 + 0.5) * dz),
1 => ((i as f64 + 0.5) * dx, j as f64 * dy, (k as f64 + 0.5) * dz),
_ => ((i as f64 + 0.5) * dx, (j as f64 + 0.5) * dy, k as f64 * dz),
};
let index = |k: usize, j: usize, i: usize| match c {
0 => g.uface(k, j, i),
1 => g.vface(k, j, i),
_ => g.wface(k, j, i),
};
let dirs = |m: usize, f: f64| {
if m <= 1 {
vec![(0usize, 1.0)]
} else {
vec![(0, 1.0 - f), (1, f)]
}
};
let mut out = Vec::with_capacity(8);
let kz = |dk: usize| if dk == 0 { k0 } else { k1 };
for (dk, wk) in dirs(planes_k, fz) {
for (dj, wj) in dirs(max_j, fy) {
for (di, wi) in dirs(max_i, fx) {
let (k, j, i) = (kz(dk), j0 + dj, i0 + di);
let (x, y, z) = pos(k, j, i);
let idx = index(k, j, i);
out.push(StencilNode {
idx,
x,
y,
z,
weight: wi * wj * wk,
zw: wk,
fallback: fallback(idx),
});
}
}
}
out
}
/// Weighted least-squares fit of `a + b (xx0) + c (yy0) + d (zz0)`
/// through `pts` (each `(x, y, z, value, weight)`; zero-weight points drop
/// out) evaluated at `at`; directions in which every weighted point has
/// the same coordinate are dropped. `None` if the normal matrix is
/// singular relative to its scale.
pub(crate) fn linear_fit(pts_w: &[(f64, f64, f64, f64, f64)], at: (f64, f64, f64)) -> Option<f64> {
let pts: Vec<(f64, f64, f64, f64, f64)> = pts_w.iter().copied().filter(|p| p.4 > 0.0).collect();
let spread = |f: &dyn Fn(&(f64, f64, f64, f64, f64)) -> f64| {
let (lo, hi) = pts
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
(lo.min(f(p)), hi.max(f(p)))
});
hi - lo > 1e-12 * (hi.abs() + lo.abs() + 1e-300)
};
let on = [true, spread(&|p| p.0), spread(&|p| p.1), spread(&|p| p.2)];
let cols: Vec<usize> = (0..4).filter(|&c| on[c]).collect();
let m = cols.len();
if pts.len() < m {
return None;
}
let mut a = vec![vec![0.0; m + 1]; m];
for p in &pts {
let full = [1.0, p.0 - at.0, p.1 - at.1, p.2 - at.2];
let r: Vec<f64> = cols.iter().map(|&c| full[c]).collect();
for i in 0..m {
for j in 0..m {
a[i][j] += p.4 * r[i] * r[j];
}
a[i][m] += p.4 * r[i] * p.3;
}
}
let scale: f64 = (0..m).map(|i| a[i][i]).product();
if scale <= 0.0 {
return None;
}
let mut det = 1.0;
for col in 0..m {
let piv = (col..m)
.max_by(|&p, &q| a[p][col].abs().partial_cmp(&a[q][col].abs()).unwrap())
.unwrap();
a.swap(col, piv);
let d = a[col][col];
det *= d;
if d.abs() <= 1e-14 * scale.powf(1.0 / m as f64) {
return None;
}
for r in col + 1..m {
let f = a[r][col] / d;
for c in col..=m {
a[r][c] -= f * a[col][c];
}
}
}
if det.abs() <= 1e-10 * scale {
return None;
}
let mut x = vec![0.0; m];
for i in (0..m).rev() {
let mut s = a[i][m];
for j in i + 1..m {
s -= a[i][j] * x[j];
}
x[i] = s / a[i][i];
}
Some(x[0])
}
impl Ghost {
pub(super) fn reconstruct(&self, values: &[f64]) -> f64 {
let mut pts: Vec<(f64, f64, f64, f64, f64)> = self
.nodes
.iter()
.filter(|n| n.fallback.is_none())
.map(|n| (n.x, n.y, n.z, values[n.idx], n.zw))
.collect();
pts.push((self.foot.0, self.foot.1, self.foot.2, self.u_surface, 1.0));
if let Some(val) = linear_fit(&pts, (self.x, self.y, self.z)) {
return val;
}
let mut probe = 0.0;
for n in &self.nodes {
probe += n.weight * n.fallback.unwrap_or_else(|| values[n.idx]);
}
self.u_surface + (probe - self.u_surface) * (self.s_face / self.s_probe)
}
}
impl Mask {
/// Classify the grid against `body` at `t`. A solid cell on a domain
/// side is refused unless that side is a Velocity, SlipWall or Periodic
/// side (an outlet may not be blocked).
pub fn build(body: &Body, g: Grid, t: f64, b: Boundaries) -> Result<Self, String> {
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
let periodic = b.z0 == Side::Periodic;
let xc = |i: usize| (i as f64 + 0.5) * dx;
let yc = |j: usize| (j as f64 + 0.5) * dy;
let zc = |k: usize| (k as f64 + 0.5) * dz;
let mut cell_fluid = vec![true; g.cells()];
let mut fluid_cells = 0;
let mut anchor = None;
let allowed = |side: Side| matches!(side, Side::Velocity | Side::Periodic | Side::SlipWall);
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let fluid = body.phi(xc(i), yc(j), zc(k), t) > 0.0;
let idx = g.cell(k, j, i);
cell_fluid[idx] = fluid;
if fluid {
fluid_cells += 1;
if anchor.is_none() {
anchor = Some(idx);
}
} else {
let touches = (i == 0 && !allowed(b.x0))
|| (i + 1 == nx && !allowed(b.x1))
|| (j == 0 && !allowed(b.y0))
|| (j + 1 == ny && !allowed(b.y1))
|| (k == 0 && !allowed(b.z0))
|| (k + 1 == nz && !allowed(b.z1));
if touches {
return Err(format!(
"embedded body reaches a domain side that is not a Velocity/Periodic side at cell ({k}, {j}, {i})"
));
}
}
}
}
}
let Some(anchor) = anchor else {
return Err("embedded body covers the whole domain".into());
};
let h_min = dx.min(dy).min(dz);
let reach = 1.5 * h_min;
let eps = 1e-6 * h_min;
let is_fluid = |k: usize, j: usize, i: usize| cell_fluid[g.cell(k, j, i)];
let mut u_kind = vec![FaceKind::Fluid; g.n_ufaces()];
let mut v_kind = vec![FaceKind::Fluid; g.n_vfaces()];
let mut w_kind = vec![FaceKind::Fluid; g.n_wfaces()];
let kind_of = |phi: f64| {
if phi > -reach {
FaceKind::Ghost
} else {
FaceKind::Solid
}
};
for k in 0..nz {
for j in 0..ny {
for i in 1..nx {
if !(is_fluid(k, j, i - 1) && is_fluid(k, j, i)) {
u_kind[g.uface(k, j, i)] =
kind_of(body.phi(i as f64 * dx, yc(j), zc(k), t));
}
}
}
for j in 1..ny {
for i in 0..nx {
if !(is_fluid(k, j - 1, i) && is_fluid(k, j, i)) {
v_kind[g.vface(k, j, i)] =
kind_of(body.phi(xc(i), j as f64 * dy, zc(k), t));
}
}
}
}
let w_range = if periodic { 0..nz } else { 1..nz };
for k in w_range.clone() {
let below = if k > 0 { k - 1 } else { nz - 1 };
for j in 0..ny {
for i in 0..nx {
if !(is_fluid(below, j, i) && is_fluid(k, j, i)) {
w_kind[g.wface(k, j, i)] =
kind_of(body.phi(xc(i), yc(j), k as f64 * dz, t));
}
}
}
}
if periodic {
for j in 0..ny {
for i in 0..nx {
w_kind[g.wface(nz, j, i)] = w_kind[g.wface(0, j, i)];
}
}
}
let face_pos = |c: usize, k: usize, j: usize, i: usize| match c {
0 => (i as f64 * dx, yc(j), zc(k)),
1 => (xc(i), j as f64 * dy, zc(k)),
_ => (xc(i), yc(j), k as f64 * dz),
};
let kji_of = |c: usize, idx: usize| -> (usize, usize, usize) {
match c {
0 => (idx / ((nx + 1) * ny), (idx / (nx + 1)) % ny, idx % (nx + 1)),
1 => (idx / (nx * (ny + 1)), (idx / nx) % (ny + 1), idx % nx),
_ => (idx / (nx * ny), (idx / nx) % ny, idx % nx),
}
};
let build_ghost = |c: usize,
kinds: &[FaceKind],
idx: usize,
(x, y, z): (f64, f64, f64),
flux_sign: f64|
-> Ghost {
let s_face = body.phi(x, y, z, t);
let (n1, n2, n3) = body.normal(x, y, z, t, eps);
let foot = (x - s_face * n1, y - s_face * n2, z - s_face * n3);
let s_probe = s_face.abs() + h_min;
let probe = (
foot.0 + s_probe * n1,
foot.1 + s_probe * n2,
foot.2 + s_probe * n3,
);
let vel = body.surface_velocity(foot.0, foot.1, foot.2, t);
let u_surface = [vel.0, vel.1, vel.2][c];
let nodes = stencil_nodes(probe, c, g, periodic, |nidx| {
if kinds[nidx] == FaceKind::Fluid {
None
} else {
let (kk, jj, ii) = kji_of(c, nidx);
let (px, py, pz) = face_pos(c, kk, jj, ii);
let s = body.phi(px, py, pz, t);
let (m1, m2, m3) = body.normal(px, py, pz, t, eps);
let f = body.surface_velocity(px - s * m1, py - s * m2, pz - s * m3, t);
Some([f.0, f.1, f.2][c])
}
});
Ghost {
idx,
x,
y,
z,
foot,
u_surface,
s_face,
s_probe,
nodes,
flux_sign,
}
};
let mut u_ghosts = Vec::new();
let mut v_ghosts = Vec::new();
let mut w_ghosts = Vec::new();
for k in 0..nz {
for j in 0..ny {
for i in 1..nx {
let idx = g.uface(k, j, i);
if u_kind[idx] == FaceKind::Ghost {
let sign = if is_fluid(k, j, i - 1) {
1.0
} else if is_fluid(k, j, i) {
-1.0
} else {
0.0
};
u_ghosts.push(build_ghost(0, &u_kind, idx, face_pos(0, k, j, i), sign));
}
}
}
for j in 1..ny {
for i in 0..nx {
let idx = g.vface(k, j, i);
if v_kind[idx] == FaceKind::Ghost {
let sign = if is_fluid(k, j - 1, i) {
1.0
} else if is_fluid(k, j, i) {
-1.0
} else {
0.0
};
v_ghosts.push(build_ghost(1, &v_kind, idx, face_pos(1, k, j, i), sign));
}
}
}
}
for k in w_range {
let below = if k > 0 { k - 1 } else { nz - 1 };
for j in 0..ny {
for i in 0..nx {
let idx = g.wface(k, j, i);
if w_kind[idx] == FaceKind::Ghost {
let sign = if is_fluid(below, j, i) {
1.0
} else if is_fluid(k, j, i) {
-1.0
} else {
0.0
};
w_ghosts.push(build_ghost(2, &w_kind, idx, face_pos(2, k, j, i), sign));
}
}
}
}
Ok(Self {
grid: g,
periodic_z: periodic,
cell_fluid,
u_kind,
v_kind,
w_kind,
u_ghosts,
v_ghosts,
w_ghosts,
anchor,
fluid_cells,
cut: None,
step_apertures: None,
step_open: None,
merge_master: Vec::new(),
scheme: crate::solvers::incompressible::ConvectionScheme::Upwind,
density: 1.0,
wall_order: 1,
wall_distance_oblique: false,
diffusion_transverse: false,
distance_floor_fine: false,
wall_advancing: false,
exchange_convection_off: false,
wall_exchange_axis: false,
changed_cells: None,
cv_sides_exact: false,
wall_order2_centroid: false,
wall_exchange_foot: false,
conv_sides_exact: false,
wall_flux_true_normal: false,
wall_foot_centroid: false,
grad_weights: None,
diffusion_centroid: false,
face_shifts: None,
})
}
/// The step-averaged apertures of a moving cut wall (`None` at rest).
#[must_use]
pub fn step_apertures(&self) -> Option<&(Vec<f64>, Vec<f64>, Vec<f64>)> {
self.step_apertures.as_ref()
}
/// The master of a virtually merged small cell.
#[inline]
#[must_use]
pub fn master(&self, idx: usize) -> Option<usize> {
match self.merge_master.get(idx) {
Some(&m) if m != usize::MAX => Some(m),
_ => None,
}
}
/// The space-time open flags per u / v / w face (a moving cut wall).
#[must_use]
pub fn step_open_flags(&self) -> Option<[&[bool]; 3]> {
self.step_open.as_ref().map(|o| [o.0.as_slice(), o.1.as_slice(), o.2.as_slice()])
}
/// The cells whose operator rows may have changed since the previous
/// step (P1-4), or `None` when every row must be rebuilt.
#[must_use]
pub fn changed_cells(&self) -> Option<&[usize]> {
self.changed_cells.as_deref()
}
#[must_use]
pub fn merged_cells(&self) -> usize {
self.merge_master
.iter()
.filter(|&&m| m != usize::MAX)
.count()
}
// The projection's unknowns (the instantaneous kinds at rest).
#[inline]
#[must_use]
pub fn u_open(&self, idx: usize) -> bool {
self.step_open
.as_ref()
.map_or(self.u_kind[idx] == FaceKind::Fluid, |o| o.0[idx])
}
#[inline]
#[must_use]
pub fn v_open(&self, idx: usize) -> bool {
self.step_open
.as_ref()
.map_or(self.v_kind[idx] == FaceKind::Fluid, |o| o.1[idx])
}
#[inline]
#[must_use]
pub fn w_open(&self, idx: usize) -> bool {
self.step_open
.as_ref()
.map_or(self.w_kind[idx] == FaceKind::Fluid, |o| o.2[idx])
}
#[inline]
#[must_use]
pub fn cell_active(&self, idx: usize) -> bool {
self.step_open
.as_ref()
.map_or(self.cell_fluid[idx], |o| o.3[idx])
}
/// The step-averaged aperture of a u / v / w face (the instantaneous
/// one for a wall at rest).
#[inline]
#[must_use]
pub fn au_step(&self, idx: usize) -> f64 {
self.step_apertures
.as_ref()
.map_or_else(|| self.a_u(idx), |a| a.0[idx])
}
#[inline]
#[must_use]
pub fn av_step(&self, idx: usize) -> f64 {
self.step_apertures
.as_ref()
.map_or_else(|| self.a_v(idx), |a| a.1[idx])
}
#[inline]
#[must_use]
pub fn aw_step(&self, idx: usize) -> f64 {
self.step_apertures
.as_ref()
.map_or_else(|| self.a_w(idx), |a| a.2[idx])
}
/// The cut geometry (apertured wall only).
#[must_use]
pub fn cut(&self) -> Option<&CutGeometry> {
self.cut.as_ref()
}
/// Fluid area fraction of a u / v / w face (1 on the binary wall).
#[inline]
#[must_use]
pub fn a_u(&self, idx: usize) -> f64 {
self.cut.as_ref().map_or(1.0, |c| c.a_u[idx])
}
#[inline]
#[must_use]
pub fn a_v(&self, idx: usize) -> f64 {
self.cut.as_ref().map_or(1.0, |c| c.a_v[idx])
}
#[inline]
#[must_use]
pub fn a_w(&self, idx: usize) -> f64 {
self.cut.as_ref().map_or(1.0, |c| c.a_w[idx])
}
/// Fluid volume fraction of a cell (1 on the binary wall).
#[inline]
#[must_use]
pub fn grad_weight(&self, c: usize, idx: usize) -> f64 {
self.grad_weights.as_ref().map_or(1.0, |w| match c {
0 => w.0[idx],
1 => w.1[idx],
_ => w.2[idx],
})
}
#[inline]
#[must_use]
pub fn vol(&self, idx: usize) -> f64 {
self.cut.as_ref().map_or(1.0, |c| c.vol[idx])
}
#[inline]
#[must_use]
pub fn is_fluid_cell(&self, idx: usize) -> bool {
self.cell_fluid[idx]
}
#[inline]
#[must_use]
pub fn u_kind(&self, idx: usize) -> FaceKind {
self.u_kind[idx]
}
#[inline]
#[must_use]
pub fn v_kind(&self, idx: usize) -> FaceKind {
self.v_kind[idx]
}
#[inline]
#[must_use]
pub fn w_kind(&self, idx: usize) -> FaceKind {
self.w_kind[idx]
}
#[must_use]
pub fn anchor(&self) -> usize {
self.anchor
}
#[must_use]
pub fn fluid_cells(&self) -> usize {
self.fluid_cells
}
#[must_use]
pub fn ghost_faces(&self) -> usize {
self.u_ghosts.len() + self.v_ghosts.len() + self.w_ghosts.len()
}
#[must_use]
pub fn grid(&self) -> Grid {
self.grid
}
#[must_use]
pub fn periodic_z(&self) -> bool {
self.periodic_z
}
}