Files
rustytorch/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/wall.rs
T
Omar SobhandClaude Fable 5.1 b80af59ca4
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Build CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 6s
Documentation / Build User Guide (push) Successful in 5s
CI / Format Check (push) Failing after 10s
CI / Build (ubuntu-latest) (push) Failing after 1m30s
CI / Clippy Check (push) Failing after 1m50s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m15s
embedded3 S2-7: oblique instrument gains registration sweep / slope subset / z-flow skip / merged-cell count (RTX_E3_OBLIQUE_{C0_SHIFTS,SLOPES,ZFLOW,NS}) and the operator probe (oblique_operator_probe: predictor acceleration on the exact centroid-valued field by aperture band, cut-cell divergence under four valuations, spurious pressure; z-flow control); two host prototypes, default off, device refuses them: RTX_E3_CV_SIDES=exact (the momentum CV's side apertures from the interpolant on the half faces / cell-centre planes) and RTX_E3_WALL_ORDER2=centroid (the quadratic wall gradient's second point at the neighbour's centroid distance); quad_fraction / tri_area_fraction lifted to module fns (default path digit-identical: oblique record, host suite 21/21, device cut tests)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 22:26:37 -05:00

706 lines
24 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).
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,
/// 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,
cv_sides_exact: false,
wall_order2_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,
}
}
#[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
}
}