//! The cut wall's closures beyond the first form (S2-4, S2-5): the //! open-part centroid shifts of the cut faces and the spacing they give //! the cross-direction diffusion (the default since S2-5), the exchange //! distance toward solid neighbours, and the quadratic wall gradient. use super::cutwall::CvGeometry; use super::wall::Mask; impl Mask { /// The shift of a face's open-part centroid from the face centre: /// `½h(1 − α)` along the wall normal's in-plane part, away from the /// body (zero for a full face or without the centroid diffusion). Read /// from the tables of [`Self::compute_face_shifts`]. pub(super) fn face_shift(&self, c: usize, p: [i64; 3]) -> [f64; 3] { let (Some(t), Some(f)) = (self.face_shifts.as_ref(), self.lattice().face(c, p)) else { return [0.0; 3]; }; [t[c][3 * f], t[c][3 * f + 1], t[c][3 * f + 2]] } /// The per-face shift tables (three components interleaved). #[must_use] pub fn face_shift_tables(&self) -> Option<&[Vec; 3]> { self.face_shifts.as_ref() } /// Build the open-part centroid shifts of every cut face (S2-5). pub fn compute_face_shifts(&mut self) { let g = self.grid; let h = [g.dx, g.dy, g.dz]; let lat = self.lattice(); let sizes = [g.n_ufaces(), g.n_vfaces(), g.n_wfaces()]; let mut tables = [ vec![0.0; 3 * sizes[0]], vec![0.0; 3 * sizes[1]], vec![0.0; 3 * sizes[2]], ]; for c in 0..3 { let (ni, nj, nk) = ( g.nx + usize::from(c == 0), g.ny + usize::from(c == 1), g.nz + usize::from(c == 2), ); for k in 0..nk { for j in 0..nj { for i in 0..ni { let p = [i as i64, j as i64, k as i64]; let Some(f) = lat.face(c, p) else { continue }; let Some(alpha) = self.aperture(c, p) else { continue; }; if alpha <= 0.0 || alpha >= 1.0 { continue; } // Interior faces only (a control volume needs both cells). let on_side = p[c] == 0 || p[c] as usize == [g.nx, g.ny, g.nz][c]; if on_side && !(c == 2 && self.periodic_z) { continue; } let cv = self.cv_geometry(c, p); let mut n = cv.wall; n[c] = 0.0; let a = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt(); if a == 0.0 { continue; } for d in 0..3 { // `wall` points into the body: the open part lies the other way. tables[c][3 * f + d] = -0.5 * h[d] * (1.0 - alpha) * n[d] / a; } } } } } self.face_shifts = Some(tables); } /// The distance over which a fluid face exchanges momentum with a solid /// neighbour face along `d`: the full spacing, or (S2-5) the axis /// distance from the open part's centroid to the wall, `min(h, d_f/|n_d|)`. pub(super) fn exchange_delta(&self, cv: &CvGeometry, d: usize) -> f64 { let h = [self.grid.dx, self.grid.dy, self.grid.dz][d]; if !self.wall_exchange_axis { return h; } let a_w = (cv.wall[0] * cv.wall[0] + cv.wall[1] * cv.wall[1] + cv.wall[2] * cv.wall[2]).sqrt(); if a_w == 0.0 { return h; } let n_d = cv.wall[d].abs() / a_w; if n_d < 1e-12 { return h; } (cv.distance / n_d).min(h) } /// The wall-gradient coefficients of the unknown face of component /// `c` at `p` with control volume `cv`: `u'(0) = c_1 (u_f − U_b) + c_2 /// (u_n − U_b)` with `u_n` the face returned (one lattice step away /// from the body along the wall normal's dominant axis). Order 1, or /// no open neighbour: `(1/d_f, 0, None)`. pub(super) fn wall_gradient( &self, c: usize, p: [i64; 3], cv: &CvGeometry, ) -> (f64, f64, Option) { let linear = (1.0 / cv.distance, 0.0, None); if self.wall_order < 2 { return linear; } let a_w = (cv.wall[0] * cv.wall[0] + cv.wall[1] * cv.wall[1] + cv.wall[2] * cv.wall[2]).sqrt(); if a_w == 0.0 { return linear; } let n = [cv.wall[0] / a_w, cv.wall[1] / a_w, cv.wall[2] / a_w]; let mut d = 0; for k in 1..3 { if n[k].abs() > n[d].abs() { d = k; } } // `n` points into the body: step the other way. let mut q = p; q[d] -= if n[d] > 0.0 { 1 } else { -1 }; let open = self.aperture(c, q).is_some_and(|a| a > 0.0); if !open { return linear; } let f = self.lattice().face(c, q).expect("open face"); let h = [self.grid.dx, self.grid.dy, self.grid.dz]; let d1 = cv.distance; let d2 = d1 + h[d] * n[d].abs(); (d2 / (d1 * (d2 - d1)), -d1 / (d2 * (d2 - d1)), Some(f)) } }