Documentation / Build API Documentation (push) Failing after 4s
CI / Clippy Check (push) Failing after 33s
Performance Benchmarks / Run Benchmarks (push) Failing after 7s
CI / Build CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 5s
CI / Format Check (push) Failing after 13s
CI / Build (ubuntu-latest) (push) Failing after 1m13s
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]>
571 lines
24 KiB
Rust
571 lines
24 KiB
Rust
//! The two load routes of the 2D solver in 3D. Route A, surface stress
|
||
//! reconstruction: at each surface sample the pressure is linearly
|
||
//! extrapolated to the wall from probes at `h` and `2h` along the normal;
|
||
//! the wall-normal derivatives of the normal and two tangential velocity
|
||
//! components from the same probes with the surface velocity at the wall
|
||
//! (quadratic fit); the tangential derivatives of the normal velocity from
|
||
//! the surface-velocity function; `σ·n = (−p + 2μ ∂ₙuₙ) n + μ Σₜ (∂ₙuₜ +
|
||
//! ∂ₜuₙ) t`. Route B, a momentum balance over a box of whole cells
|
||
//! enclosing the body — it reads no near-wall value; the two are unrelated
|
||
//! readings of one solution.
|
||
|
||
use super::body::Body;
|
||
use super::field::Field;
|
||
use super::wall::{FaceKind, Mask, linear_fit, stencil_nodes, z_planes};
|
||
|
||
/// Minimum face aperture for a velocity node to enter a probe's
|
||
/// interpolation (`RTX_E3_PROBE_MIN_APERTURE`, default 0.5; 0 keeps every
|
||
/// fluid face, the reading before S2-1's fix).
|
||
fn probe_min_aperture() -> f64 {
|
||
std::env::var("RTX_E3_PROBE_MIN_APERTURE")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(0.5)
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub struct SurfaceForce {
|
||
pub f: [f64; 3],
|
||
pub samples: usize,
|
||
pub skipped: usize,
|
||
}
|
||
|
||
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||
[
|
||
a[1] * b[2] - a[2] * b[1],
|
||
a[2] * b[0] - a[0] * b[2],
|
||
a[0] * b[1] - a[1] * b[0],
|
||
]
|
||
}
|
||
|
||
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||
}
|
||
|
||
/// Two unit tangents orthonormal to `n`.
|
||
fn tangents(n: [f64; 3]) -> ([f64; 3], [f64; 3]) {
|
||
let a = if n[0].abs() < 0.9 {
|
||
[1.0, 0.0, 0.0]
|
||
} else {
|
||
[0.0, 1.0, 0.0]
|
||
};
|
||
let t1 = cross(n, a);
|
||
let l = dot(t1, t1).sqrt();
|
||
let t1 = [t1[0] / l, t1[1] / l, t1[2] / l];
|
||
(t1, cross(n, t1))
|
||
}
|
||
|
||
impl Mask {
|
||
/// Pressure at a point from the cell centres: trilinear when the
|
||
/// surrounding cells are fluid, else the least-squares fit through the
|
||
/// fluid ones; `None` if degenerate.
|
||
pub fn pressure_at(&self, p: &[f64], x: f64, y: f64, z: f64) -> Option<f64> {
|
||
let g = self.grid();
|
||
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
||
let (gx, gy, gz) = (x / dx - 0.5, y / dy - 0.5, z / dz - 0.5);
|
||
let i0 = gx.floor().clamp(0.0, (nx - 2) as f64) as usize;
|
||
let j0 = gy.floor().clamp(0.0, (ny - 2) 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, nz, self.periodic_z());
|
||
// A query on a single weighted plane lives on it (the fit has no
|
||
// z variation to determine).
|
||
let z = if nz == 1 || fz == 0.0 {
|
||
(k0 as f64 + 0.5) * dz
|
||
} else if fz == 1.0 {
|
||
(k1 as f64 + 0.5) * dz
|
||
} else {
|
||
z
|
||
};
|
||
let dirs = |m: usize, f: f64| {
|
||
if m <= 1 {
|
||
vec![(0usize, 1.0)]
|
||
} else {
|
||
vec![(0, 1.0 - f), (1, f)]
|
||
}
|
||
};
|
||
let kz = |dk: usize| if dk == 0 { k0 } else { k1 };
|
||
let mut nodes = Vec::with_capacity(8);
|
||
for (dk, wk) in dirs(nz, fz) {
|
||
for (dj, wj) in dirs(ny, fy) {
|
||
for (di, wi) in dirs(nx, fx) {
|
||
nodes.push((g.cell(kz(dk), j0 + dj, i0 + di), wi * wj * wk));
|
||
}
|
||
}
|
||
}
|
||
// A virtually merged small cell carries its master's pressure
|
||
// unknown, not its own: it is dropped from the fit.
|
||
let usable = |idx: usize| self.is_fluid_cell(idx) && self.master(idx).is_none();
|
||
if nodes.iter().all(|&(idx, _)| usable(idx)) {
|
||
return Some(nodes.iter().map(|&(idx, w)| w * p[idx]).sum());
|
||
}
|
||
// Weighted by the z-direction weight (a z-invariant field then fits
|
||
// as the 2D four-cell fit); the z weight of node `(dk, dj, di)` is
|
||
// `wk`, recovered from the trilinear product.
|
||
let zw = |wk: f64| wk;
|
||
let mut pts: Vec<(f64, f64, f64, f64, f64)> = Vec::new();
|
||
let mut it = nodes.iter();
|
||
for (dk, wk) in dirs(nz, fz) {
|
||
for _ in 0..dirs(ny, fy).len() * dirs(nx, fx).len() {
|
||
let &(idx, _) = it.next().expect("node");
|
||
let _ = dk;
|
||
if usable(idx) {
|
||
let (k, j, i) = g.kji(idx);
|
||
pts.push((
|
||
(i as f64 + 0.5) * dx,
|
||
(j as f64 + 0.5) * dy,
|
||
(k as f64 + 0.5) * dz,
|
||
p[idx],
|
||
zw(wk),
|
||
));
|
||
}
|
||
}
|
||
}
|
||
linear_fit(&pts, (x, y, z))
|
||
}
|
||
|
||
/// Velocity at a point: trilinear over a component's nodes when all are
|
||
/// fluid faces, else the fit through the fluid ones and the point's own
|
||
/// boundary intercept with its surface velocity.
|
||
pub fn velocity_at(
|
||
&self,
|
||
body: &Body,
|
||
f: &Field,
|
||
x: f64,
|
||
y: f64,
|
||
z: f64,
|
||
t: f64,
|
||
) -> Option<[f64; 3]> {
|
||
let g = self.grid();
|
||
let eps = 1e-6 * g.dx.min(g.dy).min(g.dz);
|
||
let s = body.phi(x, y, z, t);
|
||
let (n1, n2, n3) = body.normal(x, y, z, t, eps);
|
||
let foot = (x - s * n1, y - s * n2, z - s * n3);
|
||
let vf = body.surface_velocity(foot.0, foot.1, foot.2, t);
|
||
let vel_foot = [vf.0, vf.1, vf.2];
|
||
let mut out = [0.0; 3];
|
||
for c in 0..3 {
|
||
// A query on a single weighted plane of this component's nodes
|
||
// lives on it (the fit has no z variation to determine).
|
||
let (planes, offset) = if c == 2 {
|
||
(if self.periodic_z() { g.nz } else { g.nz + 1 }, 0.0)
|
||
} else {
|
||
(g.nz, 0.5)
|
||
};
|
||
let gz = z / g.dz - offset;
|
||
let (k0, k1, fz) = z_planes(gz, planes, self.periodic_z());
|
||
let zq = if planes <= 1 || fz == 0.0 {
|
||
(k0 as f64 + offset) * g.dz
|
||
} else if fz == 1.0 {
|
||
(k1 as f64 + offset) * g.dz
|
||
} else {
|
||
z
|
||
};
|
||
let foot_c = (foot.0, foot.1, if zq == z { foot.2 } else { zq });
|
||
let nodes = stencil_nodes((x, y, zq), c, g, self.periodic_z(), |_| None);
|
||
let values: &[f64] = [&f.u, &f.v, &f.w][c];
|
||
// A cut face's velocity lives on the open part of the face, not
|
||
// at its centre: a node whose centre lies in the body would put
|
||
// the wall profile's value h/2 too deep and bias the wall
|
||
// gradient at O(1). Nodes below the minimum aperture are dropped
|
||
// and the point's own wall intercept takes their place.
|
||
let min_aperture = probe_min_aperture();
|
||
let fluid = |idx: usize| match c {
|
||
0 => self.u_kind(idx) == FaceKind::Fluid && self.a_u(idx) >= min_aperture,
|
||
1 => self.v_kind(idx) == FaceKind::Fluid && self.a_v(idx) >= min_aperture,
|
||
_ => self.w_kind(idx) == FaceKind::Fluid && self.a_w(idx) >= min_aperture,
|
||
};
|
||
if nodes.iter().all(|n| fluid(n.idx)) {
|
||
out[c] = nodes.iter().map(|n| n.weight * values[n.idx]).sum();
|
||
} else {
|
||
let mut pts: Vec<(f64, f64, f64, f64, f64)> = nodes
|
||
.iter()
|
||
.filter(|n| fluid(n.idx))
|
||
.map(|n| (n.x, n.y, n.z, values[n.idx], n.zw))
|
||
.collect();
|
||
pts.push((foot_c.0, foot_c.1, foot_c.2, vel_foot[c], 1.0));
|
||
out[c] = linear_fit(&pts, (x, y, zq))?;
|
||
}
|
||
}
|
||
Some(out)
|
||
}
|
||
|
||
/// The reconstructed traction at one surface point with outward normal `n`.
|
||
pub fn traction_at(
|
||
&self,
|
||
body: &Body,
|
||
f: &Field,
|
||
mu: f64,
|
||
t: f64,
|
||
x: [f64; 3],
|
||
n: [f64; 3],
|
||
) -> Option<[f64; 3]> {
|
||
let g = self.grid();
|
||
let h = g.dx.min(g.dy).min(g.dz);
|
||
let (d1, d2) = (h, 2.0 * h);
|
||
let at = |d: f64| [x[0] + d * n[0], x[1] + d * n[1], x[2] + d * n[2]];
|
||
let (x1, x2) = (at(d1), at(d2));
|
||
let p1 = self.pressure_at(&f.p, x1[0], x1[1], x1[2])?;
|
||
let p2 = self.pressure_at(&f.p, x2[0], x2[1], x2[2])?;
|
||
let u1 = self.velocity_at(body, f, x1[0], x1[1], x1[2], t)?;
|
||
let u2 = self.velocity_at(body, f, x2[0], x2[1], x2[2], t)?;
|
||
let p_wall = p1 + (p1 - p2) * d1 / (d2 - d1);
|
||
let (t1, t2) = tangents(n);
|
||
let us = body.surface_velocity(x[0], x[1], x[2], t);
|
||
let us = [us.0, us.1, us.2];
|
||
let wall_gradient =
|
||
|f1: f64, f2: f64| (f1 * d2 * d2 - f2 * d1 * d1) / (d1 * d2 * (d2 - d1));
|
||
let dn =
|
||
|dir: [f64; 3]| wall_gradient(dot(u1, dir) - dot(us, dir), dot(u2, dir) - dot(us, dir));
|
||
let eps = 1e-6 * h;
|
||
let dt_un = |dir: [f64; 3]| {
|
||
let p = body.surface_velocity(
|
||
x[0] + eps * dir[0],
|
||
x[1] + eps * dir[1],
|
||
x[2] + eps * dir[2],
|
||
t,
|
||
);
|
||
let m = body.surface_velocity(
|
||
x[0] - eps * dir[0],
|
||
x[1] - eps * dir[1],
|
||
x[2] - eps * dir[2],
|
||
t,
|
||
);
|
||
((p.0 - m.0) * n[0] + (p.1 - m.1) * n[1] + (p.2 - m.2) * n[2]) / (2.0 * eps)
|
||
};
|
||
let traction_n = -p_wall + 2.0 * mu * dn(n);
|
||
let traction_t1 = mu * (dn(t1) + dt_un(t1));
|
||
let traction_t2 = mu * (dn(t2) + dt_un(t2));
|
||
Some([
|
||
traction_n * n[0] + traction_t1 * t1[0] + traction_t2 * t2[0],
|
||
traction_n * n[1] + traction_t1 * t1[1] + traction_t2 * t2[1],
|
||
traction_n * n[2] + traction_t1 * t1[2] + traction_t2 * t2[2],
|
||
])
|
||
}
|
||
|
||
/// Route A: the surface integral of the reconstructed traction over the
|
||
/// body's samples at spacing `ds`.
|
||
pub fn surface_force(&self, body: &Body, f: &Field, mu: f64, t: f64, ds: f64) -> SurfaceForce {
|
||
let samples = body.surface_samples(ds);
|
||
let mut force = [0.0; 3];
|
||
let mut skipped = 0;
|
||
for s in &samples {
|
||
match self.traction_at(body, f, mu, t, [s.x, s.y, s.z], [s.nx, s.ny, s.nz]) {
|
||
Some(tr) => {
|
||
for c in 0..3 {
|
||
force[c] += tr[c] * s.area;
|
||
}
|
||
}
|
||
None => skipped += 1,
|
||
}
|
||
}
|
||
SurfaceForce {
|
||
f: force,
|
||
samples: samples.len(),
|
||
skipped,
|
||
}
|
||
}
|
||
|
||
/// Route B: the momentum balance over the box of whole cells
|
||
/// `[i0, i1) × [j0, j1) × [k0, k1)` (in the fluid on its boundary):
|
||
/// `F = Σ_outer (σ·n − ρ u (u·n)) A − d/dt ∫ ρ u dV + ∫ f dV`.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn control_volume_force(
|
||
&self,
|
||
f: &Field,
|
||
dt: f64,
|
||
rho: f64,
|
||
mu: f64,
|
||
source: Option<&dyn Fn(f64, f64, f64) -> (f64, f64, f64)>,
|
||
bx: (usize, usize, usize, usize, usize, usize),
|
||
) -> [f64; 3] {
|
||
self.control_volume_force_with_walls(f, dt, rho, mu, source, bx, false)
|
||
}
|
||
|
||
/// Route B on a box spanning the whole z range between NO-SLIP z walls
|
||
/// (`no_slip_z`): the walls' shear on the fluid inside the box (against
|
||
/// a wall at rest) is an outer-face stress and enters the balance;
|
||
/// without it (slip or periodic sides) the z faces carry nothing.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn control_volume_force_with_walls(
|
||
&self,
|
||
f: &Field,
|
||
dt: f64,
|
||
rho: f64,
|
||
mu: f64,
|
||
source: Option<&dyn Fn(f64, f64, f64) -> (f64, f64, f64)>,
|
||
bx: (usize, usize, usize, usize, usize, usize),
|
||
no_slip_z: bool,
|
||
) -> [f64; 3] {
|
||
self.control_volume_force_moving(f, dt, rho, mu, source, bx, no_slip_z, None)
|
||
}
|
||
|
||
/// The box route on a moving body: the unsteady term is the change of
|
||
/// the momentum `ρ V u` over the step with the previous step's cell
|
||
/// volumes `vol_old` (`Solver::previous_volumes`), so the fluid the
|
||
/// wall sweeps counts (`Σ ρ V (u − uⁿ)/dt` misses `ρ (V − Vⁿ) uⁿ/dt`).
|
||
/// `None` falls back to the fixed-volume form.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn control_volume_force_moving(
|
||
&self,
|
||
f: &Field,
|
||
dt: f64,
|
||
rho: f64,
|
||
mu: f64,
|
||
source: Option<&dyn Fn(f64, f64, f64) -> (f64, f64, f64)>,
|
||
(i0, i1, j0, j1, k0, k1): (usize, usize, usize, usize, usize, usize),
|
||
no_slip_z: bool,
|
||
vol_old: Option<&[f64]>,
|
||
) -> [f64; 3] {
|
||
let g = self.grid();
|
||
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
||
let (u, v, w, p) = (&f.u, &f.v, &f.w, &f.p);
|
||
let mut force = [0.0; 3];
|
||
let uc =
|
||
|k: usize, j: usize, i: usize| 0.5 * (u[g.uface(k, j, i)] + u[g.uface(k, j, i + 1)]);
|
||
let vc =
|
||
|k: usize, j: usize, i: usize| 0.5 * (v[g.vface(k, j, i)] + v[g.vface(k, j + 1, i)]);
|
||
let wc =
|
||
|k: usize, j: usize, i: usize| 0.5 * (w[g.wface(k, j, i)] + w[g.wface(k + 1, j, i)]);
|
||
// Central inside, one-sided at the domain edge.
|
||
let dd = |prev: Option<f64>, here: f64, next: Option<f64>, h: f64| match (prev, next) {
|
||
(Some(a), Some(b)) => (b - a) / (2.0 * h),
|
||
(None, Some(b)) => (b - here) / h,
|
||
(Some(a), None) => (here - a) / h,
|
||
(None, None) => 0.0,
|
||
};
|
||
// x faces: u lives there.
|
||
for k in k0..k1 {
|
||
for j in j0..j1 {
|
||
for (i, sign) in [(i1, 1.0), (i0, -1.0)] {
|
||
let un = u[g.uface(k, j, i)];
|
||
let p_f = 0.5 * (p[g.cell(k, j, i - 1)] + p[g.cell(k, j, i)]);
|
||
let dudx = (u[g.uface(k, j, i + 1)] - u[g.uface(k, j, i - 1)]) / (2.0 * dx);
|
||
let dvdx = (vc(k, j, i) - vc(k, j, i - 1)) / dx;
|
||
let dwdx = (wc(k, j, i) - wc(k, j, i - 1)) / dx;
|
||
let dudy = dd(
|
||
(j > 0).then(|| u[g.uface(k, j - 1, i)]),
|
||
un,
|
||
(j + 1 < ny).then(|| u[g.uface(k, j + 1, i)]),
|
||
dy,
|
||
);
|
||
let dudz = dd(
|
||
(k > 0).then(|| u[g.uface(k - 1, j, i)]),
|
||
un,
|
||
(k + 1 < nz).then(|| u[g.uface(k + 1, j, i)]),
|
||
dz,
|
||
);
|
||
let v_f = 0.5 * (vc(k, j, i - 1) + vc(k, j, i));
|
||
let w_f = 0.5 * (wc(k, j, i - 1) + wc(k, j, i));
|
||
let a = dy * dz;
|
||
force[0] += sign * ((-p_f + 2.0 * mu * dudx) - rho * un * un) * a;
|
||
force[1] += sign * (mu * (dudy + dvdx) - rho * v_f * un) * a;
|
||
force[2] += sign * (mu * (dudz + dwdx) - rho * w_f * un) * a;
|
||
}
|
||
}
|
||
}
|
||
// y faces: v lives there.
|
||
for k in k0..k1 {
|
||
for i in i0..i1 {
|
||
for (j, sign) in [(j1, 1.0), (j0, -1.0)] {
|
||
let vn = v[g.vface(k, j, i)];
|
||
let p_f = 0.5 * (p[g.cell(k, j - 1, i)] + p[g.cell(k, j, i)]);
|
||
let dvdy = (v[g.vface(k, j + 1, i)] - v[g.vface(k, j - 1, i)]) / (2.0 * dy);
|
||
let dudy = (uc(k, j, i) - uc(k, j - 1, i)) / dy;
|
||
let dwdy = (wc(k, j, i) - wc(k, j - 1, i)) / dy;
|
||
let dvdx = dd(
|
||
(i > 0).then(|| v[g.vface(k, j, i - 1)]),
|
||
vn,
|
||
(i + 1 < nx).then(|| v[g.vface(k, j, i + 1)]),
|
||
dx,
|
||
);
|
||
let dvdz = dd(
|
||
(k > 0).then(|| v[g.vface(k - 1, j, i)]),
|
||
vn,
|
||
(k + 1 < nz).then(|| v[g.vface(k + 1, j, i)]),
|
||
dz,
|
||
);
|
||
let u_f = 0.5 * (uc(k, j - 1, i) + uc(k, j, i));
|
||
let w_f = 0.5 * (wc(k, j - 1, i) + wc(k, j, i));
|
||
let a = dx * dz;
|
||
force[0] += sign * (mu * (dudy + dvdx) - rho * u_f * vn) * a;
|
||
force[1] += sign * ((-p_f + 2.0 * mu * dvdy) - rho * vn * vn) * a;
|
||
force[2] += sign * (mu * (dvdz + dwdy) - rho * w_f * vn) * a;
|
||
}
|
||
}
|
||
}
|
||
// z faces: w lives there. A box spanning the whole z range has its
|
||
// z faces on the domain's z sides: no momentum flux and no shear on
|
||
// a slip wall, and the two faces cancel on a periodic pair — skipped.
|
||
let full_span = k0 == 0 && k1 == nz;
|
||
if full_span && no_slip_z {
|
||
let a = dx * dy;
|
||
for j in j0..j1 {
|
||
for i in i0..i1 {
|
||
for k in [0, nz - 1] {
|
||
if !self.is_fluid_cell(g.cell(k, j, i)) {
|
||
continue;
|
||
}
|
||
force[0] -= mu * uc(k, j, i) / (0.5 * dz) * a;
|
||
force[1] -= mu * vc(k, j, i) / (0.5 * dz) * a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for j in (j0..j1).filter(|_| !full_span) {
|
||
for i in i0..i1 {
|
||
for (k, sign) in [(k1, 1.0), (k0, -1.0)] {
|
||
let wn = w[g.wface(k, j, i)];
|
||
let p_f = 0.5 * (p[g.cell(k - 1, j, i)] + p[g.cell(k, j, i)]);
|
||
let dwdz = (w[g.wface(k + 1, j, i)] - w[g.wface(k - 1, j, i)]) / (2.0 * dz);
|
||
let dudz = (uc(k, j, i) - uc(k - 1, j, i)) / dz;
|
||
let dvdz = (vc(k, j, i) - vc(k - 1, j, i)) / dz;
|
||
let dwdx = dd(
|
||
(i > 0).then(|| w[g.wface(k, j, i - 1)]),
|
||
wn,
|
||
(i + 1 < nx).then(|| w[g.wface(k, j, i + 1)]),
|
||
dx,
|
||
);
|
||
let dwdy = dd(
|
||
(j > 0).then(|| w[g.wface(k, j - 1, i)]),
|
||
wn,
|
||
(j + 1 < ny).then(|| w[g.wface(k, j + 1, i)]),
|
||
dy,
|
||
);
|
||
let u_f = 0.5 * (uc(k - 1, j, i) + uc(k, j, i));
|
||
let v_f = 0.5 * (vc(k - 1, j, i) + vc(k, j, i));
|
||
let a = dx * dy;
|
||
force[0] += sign * (mu * (dudz + dwdx) - rho * u_f * wn) * a;
|
||
force[1] += sign * (mu * (dvdz + dwdy) - rho * v_f * wn) * a;
|
||
force[2] += sign * ((-p_f + 2.0 * mu * dwdz) - rho * wn * wn) * a;
|
||
}
|
||
}
|
||
}
|
||
// Unsteady term and source over the fluid cells of the box.
|
||
let dv = dx * dy * dz;
|
||
for k in k0..k1 {
|
||
for j in j0..j1 {
|
||
for i in i0..i1 {
|
||
let idx = g.cell(k, j, i);
|
||
let v_new = if self.is_fluid_cell(idx) {
|
||
self.vol(idx)
|
||
} else {
|
||
0.0
|
||
};
|
||
let v_old = vol_old.map_or(v_new, |vo| vo[idx]);
|
||
if v_new == 0.0 && v_old == 0.0 {
|
||
continue;
|
||
}
|
||
let (fu0, fu1) = (g.uface(k, j, i), g.uface(k, j, i + 1));
|
||
let (fv0, fv1) = (g.vface(k, j, i), g.vface(k, j + 1, i));
|
||
let (fw0, fw1) = (g.wface(k, j, i), g.wface(k + 1, j, i));
|
||
let cen = |a: &[f64], b: &[f64], f0: usize, f1: usize| {
|
||
(0.5 * (a[f0] + a[f1]), 0.5 * (b[f0] + b[f1]))
|
||
};
|
||
let (un, uo) = cen(u, &f.u_old, fu0, fu1);
|
||
let (vn, vo) = cen(v, &f.v_old, fv0, fv1);
|
||
let (wn, wo) = cen(w, &f.w_old, fw0, fw1);
|
||
force[0] -= rho * (v_new * un - v_old * uo) / dt * dv;
|
||
force[1] -= rho * (v_new * vn - v_old * vo) / dt * dv;
|
||
force[2] -= rho * (v_new * wn - v_old * wo) / dt * dv;
|
||
let dv = dv * v_new;
|
||
if let Some(s) = source {
|
||
let (sx, sy, sz) = s(
|
||
(i as f64 + 0.5) * dx,
|
||
(j as f64 + 0.5) * dy,
|
||
(k as f64 + 0.5) * dz,
|
||
);
|
||
force[0] += sx * dv;
|
||
force[1] += sy * dv;
|
||
force[2] += sz * dv;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
force
|
||
}
|
||
}
|
||
|
||
impl Mask {
|
||
/// S2-3d: the rate of change of the momentum the SCHEME carries in a
|
||
/// box — on the face control volumes, `Σ_f ρ h A (α_f u_f − α_fⁿ u_fⁿ)/dt`
|
||
/// (faces on the box's own surfaces at half weight) — against the
|
||
/// cell-volume form of the box route's unsteady term, returned second.
|
||
/// Near an accelerating wall the two differ at O(h) in the cut cells.
|
||
#[must_use]
|
||
pub fn box_momentum_rates(
|
||
&self,
|
||
f: &Field,
|
||
dt: f64,
|
||
rho: f64,
|
||
(i0, i1, j0, j1, k0, k1): (usize, usize, usize, usize, usize, usize),
|
||
apertures_old: Option<&[Vec<f64>; 3]>,
|
||
vol_old: Option<&[f64]>,
|
||
) -> ([f64; 3], [f64; 3]) {
|
||
let g = self.grid();
|
||
let dv = g.dx * g.dy * g.dz;
|
||
let mut faces = [0.0; 3];
|
||
let mut cells = [0.0; 3];
|
||
let old = |c: usize, idx: usize, new: f64| apertures_old.map_or(new, |a| a[c][idx]);
|
||
for k in k0..k1 {
|
||
for j in j0..j1 {
|
||
for i in i0..=i1 {
|
||
let idx = g.uface(k, j, i);
|
||
let w = if i == i0 || i == i1 { 0.5 } else { 1.0 };
|
||
let a = self.a_u(idx);
|
||
faces[0] += w * rho * dv * (a * f.u[idx] - old(0, idx, a) * f.u_old[idx]) / dt;
|
||
}
|
||
}
|
||
for j in j0..=j1 {
|
||
for i in i0..i1 {
|
||
let idx = g.vface(k, j, i);
|
||
let w = if j == j0 || j == j1 { 0.5 } else { 1.0 };
|
||
let a = self.a_v(idx);
|
||
faces[1] += w * rho * dv * (a * f.v[idx] - old(1, idx, a) * f.v_old[idx]) / dt;
|
||
}
|
||
}
|
||
}
|
||
for k in k0..=k1 {
|
||
for j in j0..j1 {
|
||
for i in i0..i1 {
|
||
let kk = if k == g.nz && self.periodic_z() { 0 } else { k };
|
||
let idx = g.wface(kk, j, i);
|
||
let w = if (k == k0 || k == k1) && !self.periodic_z() {
|
||
0.5
|
||
} else if k == k1 {
|
||
0.0
|
||
} else {
|
||
1.0
|
||
};
|
||
let a = self.a_w(idx);
|
||
faces[2] += w * rho * dv * (a * f.w[idx] - old(2, idx, a) * f.w_old[idx]) / dt;
|
||
}
|
||
}
|
||
}
|
||
for k in k0..k1 {
|
||
for j in j0..j1 {
|
||
for i in i0..i1 {
|
||
let idx = g.cell(k, j, i);
|
||
let v_new = if self.is_fluid_cell(idx) {
|
||
self.vol(idx)
|
||
} else {
|
||
0.0
|
||
};
|
||
let v_old = vol_old.map_or(v_new, |vo| vo[idx]);
|
||
let c = |a: &[f64], f0: usize, f1: usize| 0.5 * (a[f0] + a[f1]);
|
||
let (fu0, fu1) = (g.uface(k, j, i), g.uface(k, j, i + 1));
|
||
let (fv0, fv1) = (g.vface(k, j, i), g.vface(k, j + 1, i));
|
||
let (fw0, fw1) = (g.wface(k, j, i), g.wface(k + 1, j, i));
|
||
cells[0] +=
|
||
rho * dv * (v_new * c(&f.u, fu0, fu1) - v_old * c(&f.u_old, fu0, fu1)) / dt;
|
||
cells[1] +=
|
||
rho * dv * (v_new * c(&f.v, fv0, fv1) - v_old * c(&f.v_old, fv0, fv1)) / dt;
|
||
cells[2] +=
|
||
rho * dv * (v_new * c(&f.w, fw0, fw1) - v_old * c(&f.w_old, fw0, fw1)) / dt;
|
||
}
|
||
}
|
||
}
|
||
(faces, cells)
|
||
}
|
||
}
|