embedded3 item 11: moving bodies (end-of-step mask, fresh-cell refill, space-time cut cell: step-averaged apertures, GCL wall flux, Reynolds-transport momentum), the 3D fresh-cell falsifier (plate / circle / stadium, wall + control-volume routes) and the Lipschitz sweep; ghost wall reproduces the 2D falsifier to the digit; cut wall 5–14× smoother on the circle, gates not met (fresh cell's first step); wall.rs split (impose.rs)
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 / Clippy Check (push) Failing after 3s
CI / Build (ubuntu-latest) (push) Failing after 4s
CI / Format Check (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
CI / Build CPU-Only (Explicit) (push) Failing after 1m6s
Documentation / Build API Documentation (push) Failing after 1m9s
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 / Clippy Check (push) Failing after 3s
CI / Build (ubuntu-latest) (push) Failing after 4s
CI / Format Check (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
CI / Build CPU-Only (Explicit) (push) Failing after 1m6s
Documentation / Build API Documentation (push) Failing after 1m9s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0e4c97ed24
commit
5b1621e6ad
@@ -188,9 +188,34 @@ impl Mask {
|
||||
anchor,
|
||||
fluid_cells,
|
||||
cut: Some(cut),
|
||||
step_apertures: None,
|
||||
step_open: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the step-averaged apertures and the space-time classification
|
||||
/// from the previous mask's geometry.
|
||||
pub fn set_step_apertures(&mut self, old: &Mask) {
|
||||
let (Some(cut), Some(old_cut)) = (self.cut.as_ref(), old.cut.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
let avg = |a: &[f64], b: &[f64]| -> Vec<f64> {
|
||||
a.iter().zip(b).map(|(x, y)| 0.5 * (x + y)).collect()
|
||||
};
|
||||
let au = avg(&cut.a_u, &old_cut.a_u);
|
||||
let av = avg(&cut.a_v, &old_cut.a_v);
|
||||
let aw = avg(&cut.a_w, &old_cut.a_w);
|
||||
let open = |a: &[f64]| -> Vec<bool> { a.iter().map(|&x| x > 0.0).collect() };
|
||||
let active = self
|
||||
.cell_fluid
|
||||
.iter()
|
||||
.zip(&old.cell_fluid)
|
||||
.map(|(&n, &o)| n || o)
|
||||
.collect();
|
||||
self.step_open = Some((open(&au), open(&av), open(&aw), active));
|
||||
self.step_apertures = Some((au, av, aw));
|
||||
}
|
||||
|
||||
pub(super) fn lattice(&self) -> Lattice {
|
||||
Lattice {
|
||||
g: self.grid,
|
||||
@@ -319,6 +344,51 @@ impl Mask {
|
||||
(table, correction)
|
||||
}
|
||||
|
||||
/// The moving rigid body's wall fluxes by the discrete geometric
|
||||
/// conservation law: `(V_c^{n+1} − V_c^n)/dt` per active cell (a dying
|
||||
/// cell's remaining volume leaves through its step-averaged apertures),
|
||||
/// the net (the cut geometry's closure defect) redistributed over the
|
||||
/// wall cells by wall area.
|
||||
pub fn gcl_flux_table(&self, old: &Mask, dt: f64) -> (Vec<f64>, f64) {
|
||||
let mut table = vec![0.0; self.grid.cells()];
|
||||
let (Some(cut), Some(old_cut)) = (self.cut.as_ref(), old.cut.as_ref()) else {
|
||||
return (table, 0.0);
|
||||
};
|
||||
let g = self.grid;
|
||||
let dv = g.dx * g.dy * g.dz;
|
||||
let (mut net, mut area) = (0.0, 0.0);
|
||||
for (idx, entry) in table.iter_mut().enumerate() {
|
||||
if !self.cell_active(idx) {
|
||||
continue;
|
||||
}
|
||||
*entry = (cut.vol[idx] - old_cut.vol[idx]) * dv / dt;
|
||||
net += *entry;
|
||||
let w = cut.wall[idx];
|
||||
area += (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt();
|
||||
}
|
||||
if std::env::var_os("RTX_E3_DEBUG").is_some() {
|
||||
let dead = (0..table.len())
|
||||
.filter(|&i| !self.cell_fluid[i] && old.cell_fluid[i])
|
||||
.count();
|
||||
let fresh = (0..table.len())
|
||||
.filter(|&i| self.cell_fluid[i] && !old.cell_fluid[i])
|
||||
.count();
|
||||
let (vn, vn1): (f64, f64) = (old_cut.vol.iter().sum(), cut.vol.iter().sum());
|
||||
eprintln!(
|
||||
" gcl: dead {dead} fresh {fresh} net {net:.3e} area {area:.3e} ΣV old {vn:.6} new {vn1:.6} (Δ {:.3e})",
|
||||
vn1 - vn
|
||||
);
|
||||
}
|
||||
let correction = if area > 0.0 { net / area } else { 0.0 };
|
||||
if correction != 0.0 {
|
||||
for (idx, w) in cut.wall.iter().enumerate() {
|
||||
let a = (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt();
|
||||
table[idx] -= correction * a;
|
||||
}
|
||||
}
|
||||
(table, correction)
|
||||
}
|
||||
|
||||
/// The volume flux of the surface velocity through a cell's wall into
|
||||
/// the body, `U_b·W_c`, uncorrected. Zero without a cut geometry.
|
||||
pub fn wall_flux(&self, body: &Body, idx: usize, t: f64) -> f64 {
|
||||
@@ -348,14 +418,27 @@ impl Mask {
|
||||
/// shear `Σ_f μ A_w (u_f − U_b)/d_f` over the unknown faces. `None`
|
||||
/// without a cut geometry.
|
||||
pub fn cut_wall_force(&self, body: &Body, f: &Field, mu: f64, t: f64) -> Option<[f64; 3]> {
|
||||
let (p, s) = self.cut_wall_force_parts(body, f, mu, t)?;
|
||||
Some([p[0] + s[0], p[1] + s[1], p[2] + s[2]])
|
||||
}
|
||||
|
||||
/// The cut-cell load route split into its pressure and shear parts.
|
||||
pub fn cut_wall_force_parts(
|
||||
&self,
|
||||
body: &Body,
|
||||
f: &Field,
|
||||
mu: f64,
|
||||
t: f64,
|
||||
) -> Option<([f64; 3], [f64; 3])> {
|
||||
let cut = self.cut.as_ref()?;
|
||||
let g = self.grid;
|
||||
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
|
||||
let mut pressure = [0.0; 3];
|
||||
let mut force = [0.0; 3];
|
||||
for (idx, w) in cut.wall.iter().enumerate() {
|
||||
if self.cell_fluid[idx] {
|
||||
for c in 0..3 {
|
||||
force[c] += f.p[idx] * w[c];
|
||||
pressure[c] += f.p[idx] * w[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,6 +478,6 @@ impl Mask {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(force)
|
||||
Some((pressure, force))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//! The wall's imposition on the velocity field (`impl Mask` continued
|
||||
//! from `wall.rs`, split for the file-size rule): prescribed faces take
|
||||
//! the surface velocity, ghost faces their reconstruction from the source
|
||||
//! field minus the shared flux compatibility correction.
|
||||
|
||||
use super::body::Body;
|
||||
use super::wall::{FaceKind, Mask};
|
||||
|
||||
impl Mask {
|
||||
/// Impose the wall on `(u, v, w)` from the same field.
|
||||
pub fn impose(&self, body: &Body, u: &mut [f64], v: &mut [f64], w: &mut [f64], t: f64) -> f64 {
|
||||
let (us, vs, ws) = (u.to_vec(), v.to_vec(), w.to_vec());
|
||||
self.impose_from(body, &us, &vs, &ws, u, v, w, t)
|
||||
}
|
||||
|
||||
/// Solid faces: the surface velocity; ghost faces: the reconstruction
|
||||
/// from the SOURCE field, minus the shared flux compatibility
|
||||
/// correction over the flux-carrying ghosts. Returns the correction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn impose_from(
|
||||
&self,
|
||||
body: &Body,
|
||||
u_src: &[f64],
|
||||
v_src: &[f64],
|
||||
w_src: &[f64],
|
||||
u: &mut [f64],
|
||||
v: &mut [f64],
|
||||
w: &mut [f64],
|
||||
t: f64,
|
||||
) -> f64 {
|
||||
let g = self.grid;
|
||||
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 1..nx {
|
||||
let idx = g.uface(k, j, i);
|
||||
if self.u_kind[idx] == FaceKind::Solid {
|
||||
u[idx] = body
|
||||
.surface_velocity(
|
||||
i as f64 * dx,
|
||||
(j as f64 + 0.5) * dy,
|
||||
(k as f64 + 0.5) * dz,
|
||||
t,
|
||||
)
|
||||
.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
for j in 1..ny {
|
||||
for i in 0..nx {
|
||||
let idx = g.vface(k, j, i);
|
||||
if self.v_kind[idx] == FaceKind::Solid {
|
||||
v[idx] = body
|
||||
.surface_velocity(
|
||||
(i as f64 + 0.5) * dx,
|
||||
j as f64 * dy,
|
||||
(k as f64 + 0.5) * dz,
|
||||
t,
|
||||
)
|
||||
.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for k in 0..=nz {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let idx = g.wface(k, j, i);
|
||||
if self.w_kind[idx] == FaceKind::Solid {
|
||||
w[idx] = body
|
||||
.surface_velocity(
|
||||
(i as f64 + 0.5) * dx,
|
||||
(j as f64 + 0.5) * dy,
|
||||
k as f64 * dz,
|
||||
t,
|
||||
)
|
||||
.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let u_vals: Vec<f64> = self
|
||||
.u_ghosts
|
||||
.iter()
|
||||
.map(|gh| gh.reconstruct(u_src))
|
||||
.collect();
|
||||
let v_vals: Vec<f64> = self
|
||||
.v_ghosts
|
||||
.iter()
|
||||
.map(|gh| gh.reconstruct(v_src))
|
||||
.collect();
|
||||
let w_vals: Vec<f64> = self
|
||||
.w_ghosts
|
||||
.iter()
|
||||
.map(|gh| gh.reconstruct(w_src))
|
||||
.collect();
|
||||
let (au, av, aw) = (dy * dz, dx * dz, dx * dy);
|
||||
let mut net = 0.0;
|
||||
let mut area = 0.0;
|
||||
for (gh, &val) in self.u_ghosts.iter().zip(&u_vals) {
|
||||
if gh.flux_sign != 0.0 {
|
||||
net += gh.flux_sign * val * au;
|
||||
area += au;
|
||||
}
|
||||
}
|
||||
for (gh, &val) in self.v_ghosts.iter().zip(&v_vals) {
|
||||
if gh.flux_sign != 0.0 {
|
||||
net += gh.flux_sign * val * av;
|
||||
area += av;
|
||||
}
|
||||
}
|
||||
for (gh, &val) in self.w_ghosts.iter().zip(&w_vals) {
|
||||
if gh.flux_sign != 0.0 {
|
||||
net += gh.flux_sign * val * aw;
|
||||
area += aw;
|
||||
}
|
||||
}
|
||||
let correction = if area > 0.0 { net / area } else { 0.0 };
|
||||
for (gh, &val) in self.u_ghosts.iter().zip(&u_vals) {
|
||||
u[gh.idx] = val - gh.flux_sign * correction;
|
||||
}
|
||||
for (gh, &val) in self.v_ghosts.iter().zip(&v_vals) {
|
||||
v[gh.idx] = val - gh.flux_sign * correction;
|
||||
}
|
||||
for (gh, &val) in self.w_ghosts.iter().zip(&w_vals) {
|
||||
w[gh.idx] = val - gh.flux_sign * correction;
|
||||
}
|
||||
// The periodic seam: the w face at k = nz is the face at k = 0.
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let (f0, fn_) = (g.wface(0, j, i), g.wface(nz, j, i));
|
||||
if self.w_kind[f0] != FaceKind::Fluid && self.w_kind[fn_] == self.w_kind[f0] {
|
||||
w[fn_] = w[f0];
|
||||
}
|
||||
}
|
||||
}
|
||||
correction
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod cut;
|
||||
pub mod cutwall;
|
||||
pub mod field;
|
||||
pub mod grid;
|
||||
pub mod impose;
|
||||
pub mod loads;
|
||||
pub mod poisson;
|
||||
pub mod step;
|
||||
|
||||
+9
-4
@@ -4,8 +4,9 @@
|
||||
//! the wall; its faces carry the mass fluxes averaged from the two adjacent
|
||||
//! cells (the 2D face velocities when every aperture is 1), upwind plus the
|
||||
//! TVD correction as the 2D predictor, apertured diffusion, the pressure
|
||||
//! force `−(p₊ − p₋) α A` (the projection's gradient), the wall's momentum
|
||||
//! flux `m_w U_b` with `m_w = −Σ m_f` (so a uniform field stays uniform),
|
||||
//! force `−(p₊ − p₋) α A` (the projection's gradient), the net mass flux
|
||||
//! times the face's own value (Reynolds transport; a uniform field stays
|
||||
//! uniform on any wall motion),
|
||||
//! and the implicit wall shear `μ A_w (u − U_b)/d_f`; the time derivative
|
||||
//! carries the inertia floor.
|
||||
|
||||
@@ -195,8 +196,12 @@ impl Solver {
|
||||
}
|
||||
};
|
||||
}
|
||||
// The wall's momentum flux closes the mass balance exactly.
|
||||
conv -= mass_out * ub;
|
||||
// Reynolds transport over a volume whose wall moves with the fluid
|
||||
// on it: `ρV du/dt = −Σ m (u_face − u)` — the net mass flux of the
|
||||
// control volume (zero for a body at rest, the swept rate
|
||||
// otherwise) multiplies the face's own value, so a uniform field
|
||||
// stays uniform on any wall motion.
|
||||
conv -= mass_out * u0;
|
||||
let p_plus = lat.cell(cell_plus).map_or(0.0, |ci| field.p[ci]);
|
||||
let p_minus = lat.cell(cell_minus).map_or(0.0, |ci| field.p[ci]);
|
||||
let pressure = -(p_plus - p_minus) * cv.alpha * area[c];
|
||||
|
||||
@@ -550,6 +550,7 @@ impl DeviceStep {
|
||||
tm.cg_iterations += cg_iterations as u64;
|
||||
}
|
||||
StepResult {
|
||||
fresh_cells: 0,
|
||||
converged: final_residual < self.solver.params.tolerance,
|
||||
corrector_steps_performed: total,
|
||||
final_residual,
|
||||
|
||||
@@ -99,6 +99,8 @@ pub struct StepResult {
|
||||
pub final_residual: f64,
|
||||
/// CG iterations summed over the step's projections.
|
||||
pub poisson_iterations: usize,
|
||||
/// Cells that became fluid on this step (a moving body).
|
||||
pub fresh_cells: usize,
|
||||
}
|
||||
|
||||
type Vec3Fn = Box<dyn Fn(f64, f64, f64, f64) -> (f64, f64, f64) + Send + Sync>;
|
||||
@@ -109,6 +111,8 @@ pub struct Solver {
|
||||
pub(super) momentum_source: Option<Vec3Fn>,
|
||||
boundary_velocity: Option<Vec3Fn>,
|
||||
body: Option<Body>,
|
||||
/// The body moves: the mask is rebuilt at every step's new time.
|
||||
moving: bool,
|
||||
mask: Option<Mask>,
|
||||
last_ghost_correction: f64,
|
||||
wall_fluxes: Vec<f64>,
|
||||
@@ -137,6 +141,7 @@ impl Solver {
|
||||
momentum_source: None,
|
||||
boundary_velocity: None,
|
||||
body: None,
|
||||
moving: false,
|
||||
mask: None,
|
||||
last_ghost_correction: 0.0,
|
||||
wall_fluxes: Vec::new(),
|
||||
@@ -164,9 +169,27 @@ impl Solver {
|
||||
/// A static embedded body (the mask is built at initialisation).
|
||||
pub fn set_body(&mut self, body: Body) {
|
||||
self.body = Some(body);
|
||||
self.moving = false;
|
||||
self.mask = None;
|
||||
}
|
||||
|
||||
/// A moving embedded body: the mask is rebuilt at every step's
|
||||
/// end-of-step geometry (the 2D solver's order — predictor on the old
|
||||
/// mask, projection on the new one).
|
||||
pub fn set_moving_body(&mut self, body: Body) {
|
||||
self.body = Some(body);
|
||||
self.moving = true;
|
||||
self.mask = None;
|
||||
}
|
||||
|
||||
fn build_mask(&self, body: &Body, g: Grid, t: f64) -> Mask {
|
||||
match self.params.wall_scheme {
|
||||
WallScheme::GhostBinary => Mask::build(body, g, t, self.params.boundaries),
|
||||
WallScheme::CutCell => Mask::build_cut(body, g, t, self.params.boundaries),
|
||||
}
|
||||
.expect("embedded mask")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn body(&self) -> Option<&Body> {
|
||||
self.body.as_ref()
|
||||
@@ -230,24 +253,52 @@ impl Solver {
|
||||
.is_none_or(|m| m.is_fluid_cell(m.grid().cell(k, j, i)))
|
||||
}
|
||||
|
||||
// The apertures (1 without a cut geometry).
|
||||
// The projection's unknowns and equations (space-time on a moving cut
|
||||
// wall, the fluid predicates otherwise).
|
||||
#[inline]
|
||||
pub(super) fn u_is_unknown(&self, k: usize, j: usize, i: usize) -> bool {
|
||||
self.mask
|
||||
.as_ref()
|
||||
.is_none_or(|m| m.u_open(m.grid().uface(k, j, i)))
|
||||
}
|
||||
#[inline]
|
||||
pub(super) fn v_is_unknown(&self, k: usize, j: usize, i: usize) -> bool {
|
||||
self.mask
|
||||
.as_ref()
|
||||
.is_none_or(|m| m.v_open(m.grid().vface(k, j, i)))
|
||||
}
|
||||
#[inline]
|
||||
pub(super) fn w_is_unknown(&self, k: usize, j: usize, i: usize) -> bool {
|
||||
self.mask
|
||||
.as_ref()
|
||||
.is_none_or(|m| m.w_open(m.grid().wface(k, j, i)))
|
||||
}
|
||||
#[inline]
|
||||
pub(super) fn cell_is_active(&self, k: usize, j: usize, i: usize) -> bool {
|
||||
self.mask
|
||||
.as_ref()
|
||||
.is_none_or(|m| m.cell_active(m.grid().cell(k, j, i)))
|
||||
}
|
||||
|
||||
// The projection's apertures: step-averaged on a moving cut wall
|
||||
// (1 without a cut geometry).
|
||||
#[inline]
|
||||
pub(super) fn au(&self, k: usize, j: usize, i: usize) -> f64 {
|
||||
self.mask
|
||||
.as_ref()
|
||||
.map_or(1.0, |m| m.a_u(m.grid().uface(k, j, i)))
|
||||
.map_or(1.0, |m| m.au_step(m.grid().uface(k, j, i)))
|
||||
}
|
||||
#[inline]
|
||||
pub(super) fn av(&self, k: usize, j: usize, i: usize) -> f64 {
|
||||
self.mask
|
||||
.as_ref()
|
||||
.map_or(1.0, |m| m.a_v(m.grid().vface(k, j, i)))
|
||||
.map_or(1.0, |m| m.av_step(m.grid().vface(k, j, i)))
|
||||
}
|
||||
#[inline]
|
||||
pub(super) fn aw(&self, k: usize, j: usize, i: usize) -> f64 {
|
||||
self.mask
|
||||
.as_ref()
|
||||
.map_or(1.0, |m| m.a_w(m.grid().wface(k, j, i)))
|
||||
.map_or(1.0, |m| m.aw_step(m.grid().wface(k, j, i)))
|
||||
}
|
||||
/// The surface velocity's compatible flux through the cell's wall at
|
||||
/// the step's new time (cut wall only; the table is rebuilt per step).
|
||||
@@ -402,15 +453,7 @@ impl Solver {
|
||||
let t = self.time;
|
||||
if let Some(body) = &self.body {
|
||||
if self.mask.is_none() {
|
||||
let mask = match self.params.wall_scheme {
|
||||
WallScheme::GhostBinary => {
|
||||
Mask::build(body, field.grid, t, self.params.boundaries)
|
||||
}
|
||||
WallScheme::CutCell => {
|
||||
Mask::build_cut(body, field.grid, t, self.params.boundaries)
|
||||
}
|
||||
};
|
||||
self.mask = Some(mask.expect("embedded mask"));
|
||||
self.mask = Some(self.build_mask(body, field.grid, t));
|
||||
}
|
||||
}
|
||||
self.apply_boundary_normals(field, t);
|
||||
@@ -434,13 +477,50 @@ impl Solver {
|
||||
field.update_old_values();
|
||||
self.momentum_predictor(field, dt, t_old);
|
||||
self.apply_boundary_normals(field, t_new);
|
||||
// A moving body: the mask at the end-of-step geometry, the pressure
|
||||
// of the cells that just became fluid refilled from their
|
||||
// neighbours (fluid in both masks), the new mask's prescribed and
|
||||
// ghost values imposed from the previous corrected field.
|
||||
let mut fresh_cells = 0;
|
||||
if self.moving {
|
||||
if let Some(body) = &self.body {
|
||||
let mut new_mask = self.build_mask(body, field.grid, t_new);
|
||||
if let Some(old_mask) = &self.mask {
|
||||
fresh_cells = refill_fresh_cells(old_mask, &new_mask, field);
|
||||
new_mask.set_step_apertures(old_mask);
|
||||
}
|
||||
new_mask.impose_from(
|
||||
body,
|
||||
&field.u_old,
|
||||
&field.v_old,
|
||||
&field.w_old,
|
||||
&mut field.u,
|
||||
&mut field.v,
|
||||
&mut field.w,
|
||||
t_new,
|
||||
);
|
||||
if new_mask.cut().is_some() {
|
||||
let (table, correction) = match &self.mask {
|
||||
Some(old_mask) => new_mask.gcl_flux_table(old_mask, dt),
|
||||
None => new_mask.wall_flux_table(body, t_new),
|
||||
};
|
||||
self.wall_fluxes = table;
|
||||
self.last_ghost_correction = correction;
|
||||
}
|
||||
self.mask = Some(new_mask);
|
||||
}
|
||||
}
|
||||
field.copy_to_starred();
|
||||
let mut cut_correction = None;
|
||||
if let (Some(body), Some(mask)) = (&self.body, &self.mask) {
|
||||
if mask.cut().is_some() {
|
||||
let (table, correction) = mask.wall_flux_table(body, t_new);
|
||||
self.wall_fluxes = table;
|
||||
cut_correction = Some(correction);
|
||||
if self.moving {
|
||||
cut_correction = Some(self.last_ghost_correction);
|
||||
} else {
|
||||
let (table, correction) = mask.wall_flux_table(body, t_new);
|
||||
self.wall_fluxes = table;
|
||||
cut_correction = Some(correction);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut total = 0;
|
||||
@@ -450,6 +530,12 @@ impl Solver {
|
||||
let sol = self.solve_correction(field, dt, corrector == 0);
|
||||
poisson_iterations += sol.iterations;
|
||||
let mass_residual = self.apply_correction(field, dt);
|
||||
if std::env::var_os("RTX_E3_DEBUG").is_some() {
|
||||
eprintln!(
|
||||
" corrector {corrector}: CG {} it (converged {}), residual {:.3e}, mass {:.3e}",
|
||||
sol.iterations, sol.converged, sol.residual, mass_residual
|
||||
);
|
||||
}
|
||||
final_residual = mass_residual;
|
||||
total += 1;
|
||||
if mass_residual < self.params.tolerance {
|
||||
@@ -468,6 +554,65 @@ impl Solver {
|
||||
corrector_steps_performed: total,
|
||||
final_residual,
|
||||
poisson_iterations,
|
||||
fresh_cells,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Refill the pressure of the cells fluid in `new` and not in `old` from
|
||||
/// their face neighbours fluid in both; returns their count.
|
||||
fn refill_fresh_cells(old: &Mask, new: &Mask, field: &mut Field) -> usize {
|
||||
let g = field.grid;
|
||||
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
|
||||
let periodic = new.periodic_z();
|
||||
let mut fresh = 0;
|
||||
let mut refills = Vec::new();
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let idx = g.cell(k, j, i);
|
||||
if !(new.is_fluid_cell(idx) && !old.is_fluid_cell(idx)) {
|
||||
continue;
|
||||
}
|
||||
fresh += 1;
|
||||
let mut sum = 0.0;
|
||||
let mut count = 0usize;
|
||||
let mut visit = |nb: usize| {
|
||||
if new.is_fluid_cell(nb) && old.is_fluid_cell(nb) {
|
||||
sum += field.p[nb];
|
||||
count += 1;
|
||||
}
|
||||
};
|
||||
if i + 1 < nx {
|
||||
visit(g.cell(k, j, i + 1));
|
||||
}
|
||||
if i > 0 {
|
||||
visit(g.cell(k, j, i - 1));
|
||||
}
|
||||
if j + 1 < ny {
|
||||
visit(g.cell(k, j + 1, i));
|
||||
}
|
||||
if j > 0 {
|
||||
visit(g.cell(k, j - 1, i));
|
||||
}
|
||||
if k + 1 < nz {
|
||||
visit(g.cell(k + 1, j, i));
|
||||
} else if periodic && nz > 1 {
|
||||
visit(g.cell(0, j, i));
|
||||
}
|
||||
if k > 0 {
|
||||
visit(g.cell(k - 1, j, i));
|
||||
} else if periodic && nz > 1 {
|
||||
visit(g.cell(nz - 1, j, i));
|
||||
}
|
||||
if count > 0 {
|
||||
refills.push((idx, sum / count as f64));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (idx, p) in refills {
|
||||
field.p[idx] = p;
|
||||
}
|
||||
fresh
|
||||
}
|
||||
|
||||
+14
-14
@@ -29,7 +29,7 @@ impl Solver {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let idx = g.cell(k, j, i);
|
||||
if !self.cell_is_fluid(k, j, i) {
|
||||
if !self.cell_is_active(k, j, i) {
|
||||
problem.active[idx] = false;
|
||||
continue;
|
||||
}
|
||||
@@ -38,42 +38,42 @@ impl Solver {
|
||||
if b.x1 == outlet {
|
||||
extra += ae_outlet;
|
||||
}
|
||||
} else if self.u_is_fluid(k, j, i + 1) {
|
||||
} else if self.u_is_unknown(k, j, i + 1) {
|
||||
problem.ae[idx] = ae_interior * self.au(k, j, i + 1);
|
||||
}
|
||||
if i == 0 {
|
||||
if b.x0 == outlet {
|
||||
extra += ae_outlet;
|
||||
}
|
||||
} else if self.u_is_fluid(k, j, i) {
|
||||
} else if self.u_is_unknown(k, j, i) {
|
||||
problem.aw[idx] = ae_interior * self.au(k, j, i);
|
||||
}
|
||||
if j + 1 == ny {
|
||||
if b.y1 == outlet {
|
||||
extra += an_outlet;
|
||||
}
|
||||
} else if self.v_is_fluid(k, j + 1, i) {
|
||||
} else if self.v_is_unknown(k, j + 1, i) {
|
||||
problem.an[idx] = an_interior * self.av(k, j + 1, i);
|
||||
}
|
||||
if j == 0 {
|
||||
if b.y0 == outlet {
|
||||
extra += an_outlet;
|
||||
}
|
||||
} else if self.v_is_fluid(k, j, i) {
|
||||
} else if self.v_is_unknown(k, j, i) {
|
||||
problem.as_[idx] = an_interior * self.av(k, j, i);
|
||||
}
|
||||
if k + 1 == nz && !periodic {
|
||||
if b.z1 == outlet {
|
||||
extra += at_outlet;
|
||||
}
|
||||
} else if self.w_is_fluid((k + 1) % nz, j, i) {
|
||||
} else if self.w_is_unknown((k + 1) % nz, j, i) {
|
||||
problem.at[idx] = at_interior * self.aw((k + 1) % nz, j, i);
|
||||
}
|
||||
if k == 0 && !periodic {
|
||||
if b.z0 == outlet {
|
||||
extra += at_outlet;
|
||||
}
|
||||
} else if self.w_is_fluid(k, j, i) {
|
||||
} else if self.w_is_unknown(k, j, i) {
|
||||
problem.ab[idx] = at_interior * self.aw(k, j, i);
|
||||
}
|
||||
problem.extra_diag[idx] = extra;
|
||||
@@ -252,7 +252,7 @@ impl Solver {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let idx = g.cell(k, j, i);
|
||||
if !self.cell_is_fluid(k, j, i) {
|
||||
if !self.cell_is_active(k, j, i) {
|
||||
field.sp[idx] = 0.0;
|
||||
continue;
|
||||
}
|
||||
@@ -281,7 +281,7 @@ impl Solver {
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if self.cell_is_fluid(k, j, i) {
|
||||
if self.cell_is_active(k, j, i) {
|
||||
let idx = g.cell(k, j, i);
|
||||
p_prime[idx] = field.p_prime[idx];
|
||||
}
|
||||
@@ -328,7 +328,7 @@ impl Solver {
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 1..nx {
|
||||
if self.u_is_fluid(k, j, i) {
|
||||
if self.u_is_unknown(k, j, i) {
|
||||
let dp_dx = (pp[g.cell(k, j, i)] - pp[g.cell(k, j, i - 1)]) / dx;
|
||||
let f = g.uface(k, j, i);
|
||||
field.u[f] = field.u_star[f] - (dt / rho) * dp_dx;
|
||||
@@ -347,7 +347,7 @@ impl Solver {
|
||||
}
|
||||
for i in 0..nx {
|
||||
for j in 1..ny {
|
||||
if self.v_is_fluid(k, j, i) {
|
||||
if self.v_is_unknown(k, j, i) {
|
||||
let dp_dy = (pp[g.cell(k, j, i)] - pp[g.cell(k, j - 1, i)]) / dy;
|
||||
let f = g.vface(k, j, i);
|
||||
field.v[f] = field.v_star[f] - (dt / rho) * dp_dy;
|
||||
@@ -369,7 +369,7 @@ impl Solver {
|
||||
for i in 0..nx {
|
||||
let k_range = if periodic { 0..nz } else { 1..nz };
|
||||
for k in k_range {
|
||||
if self.w_is_fluid(k, j, i) {
|
||||
if self.w_is_unknown(k, j, i) {
|
||||
let below = if k > 0 { k - 1 } else { nz - 1 };
|
||||
let dp_dz = (pp[g.cell(k, j, i)] - pp[g.cell(below, j, i)]) / dz;
|
||||
let f = g.wface(k, j, i);
|
||||
@@ -394,7 +394,7 @@ impl Solver {
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if self.cell_is_fluid(k, j, i) {
|
||||
if self.cell_is_active(k, j, i) {
|
||||
let idx = g.cell(k, j, i);
|
||||
field.p[idx] += pp[idx];
|
||||
}
|
||||
@@ -405,7 +405,7 @@ impl Solver {
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if !self.cell_is_fluid(k, j, i) {
|
||||
if !self.cell_is_active(k, j, i) {
|
||||
continue;
|
||||
}
|
||||
let idx = g.cell(k, j, i);
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) struct StencilNode {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Ghost {
|
||||
idx: usize,
|
||||
pub(super) idx: usize,
|
||||
x: f64,
|
||||
y: f64,
|
||||
z: f64,
|
||||
@@ -58,7 +58,7 @@ pub(super) struct Ghost {
|
||||
nodes: Vec<StencilNode>,
|
||||
/// Outward-from-fluid sign for the compatibility correction (0 when no
|
||||
/// fluid cell is adjacent).
|
||||
flux_sign: f64,
|
||||
pub(super) flux_sign: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -77,6 +77,17 @@ pub struct Mask {
|
||||
/// 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>)>,
|
||||
}
|
||||
|
||||
/// The z lattice position of a query: the lower plane index, the upper
|
||||
@@ -250,7 +261,7 @@ pub(crate) fn linear_fit(pts_w: &[(f64, f64, f64, f64, f64)], at: (f64, f64, f64
|
||||
}
|
||||
|
||||
impl Ghost {
|
||||
fn reconstruct(&self, values: &[f64]) -> f64 {
|
||||
pub(super) fn reconstruct(&self, values: &[f64]) -> f64 {
|
||||
let mut pts: Vec<(f64, f64, f64, f64, f64)> = self
|
||||
.nodes
|
||||
.iter()
|
||||
@@ -484,9 +495,65 @@ impl Mask {
|
||||
anchor,
|
||||
fluid_cells,
|
||||
cut: None,
|
||||
step_apertures: None,
|
||||
step_open: None,
|
||||
})
|
||||
}
|
||||
|
||||
// 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> {
|
||||
@@ -555,134 +622,4 @@ impl Mask {
|
||||
pub fn periodic_z(&self) -> bool {
|
||||
self.periodic_z
|
||||
}
|
||||
|
||||
/// Impose the wall on `(u, v, w)` from the same field.
|
||||
pub fn impose(&self, body: &Body, u: &mut [f64], v: &mut [f64], w: &mut [f64], t: f64) -> f64 {
|
||||
let (us, vs, ws) = (u.to_vec(), v.to_vec(), w.to_vec());
|
||||
self.impose_from(body, &us, &vs, &ws, u, v, w, t)
|
||||
}
|
||||
|
||||
/// Solid faces: the surface velocity; ghost faces: the reconstruction
|
||||
/// from the SOURCE field, minus the shared flux compatibility
|
||||
/// correction over the flux-carrying ghosts. Returns the correction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn impose_from(
|
||||
&self,
|
||||
body: &Body,
|
||||
u_src: &[f64],
|
||||
v_src: &[f64],
|
||||
w_src: &[f64],
|
||||
u: &mut [f64],
|
||||
v: &mut [f64],
|
||||
w: &mut [f64],
|
||||
t: f64,
|
||||
) -> f64 {
|
||||
let g = self.grid;
|
||||
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
for i in 1..nx {
|
||||
let idx = g.uface(k, j, i);
|
||||
if self.u_kind[idx] == FaceKind::Solid {
|
||||
u[idx] = body
|
||||
.surface_velocity(
|
||||
i as f64 * dx,
|
||||
(j as f64 + 0.5) * dy,
|
||||
(k as f64 + 0.5) * dz,
|
||||
t,
|
||||
)
|
||||
.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
for j in 1..ny {
|
||||
for i in 0..nx {
|
||||
let idx = g.vface(k, j, i);
|
||||
if self.v_kind[idx] == FaceKind::Solid {
|
||||
v[idx] = body
|
||||
.surface_velocity(
|
||||
(i as f64 + 0.5) * dx,
|
||||
j as f64 * dy,
|
||||
(k as f64 + 0.5) * dz,
|
||||
t,
|
||||
)
|
||||
.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for k in 0..=nz {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let idx = g.wface(k, j, i);
|
||||
if self.w_kind[idx] == FaceKind::Solid {
|
||||
w[idx] = body
|
||||
.surface_velocity(
|
||||
(i as f64 + 0.5) * dx,
|
||||
(j as f64 + 0.5) * dy,
|
||||
k as f64 * dz,
|
||||
t,
|
||||
)
|
||||
.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let u_vals: Vec<f64> = self
|
||||
.u_ghosts
|
||||
.iter()
|
||||
.map(|gh| gh.reconstruct(u_src))
|
||||
.collect();
|
||||
let v_vals: Vec<f64> = self
|
||||
.v_ghosts
|
||||
.iter()
|
||||
.map(|gh| gh.reconstruct(v_src))
|
||||
.collect();
|
||||
let w_vals: Vec<f64> = self
|
||||
.w_ghosts
|
||||
.iter()
|
||||
.map(|gh| gh.reconstruct(w_src))
|
||||
.collect();
|
||||
let (au, av, aw) = (dy * dz, dx * dz, dx * dy);
|
||||
let mut net = 0.0;
|
||||
let mut area = 0.0;
|
||||
for (gh, &val) in self.u_ghosts.iter().zip(&u_vals) {
|
||||
if gh.flux_sign != 0.0 {
|
||||
net += gh.flux_sign * val * au;
|
||||
area += au;
|
||||
}
|
||||
}
|
||||
for (gh, &val) in self.v_ghosts.iter().zip(&v_vals) {
|
||||
if gh.flux_sign != 0.0 {
|
||||
net += gh.flux_sign * val * av;
|
||||
area += av;
|
||||
}
|
||||
}
|
||||
for (gh, &val) in self.w_ghosts.iter().zip(&w_vals) {
|
||||
if gh.flux_sign != 0.0 {
|
||||
net += gh.flux_sign * val * aw;
|
||||
area += aw;
|
||||
}
|
||||
}
|
||||
let correction = if area > 0.0 { net / area } else { 0.0 };
|
||||
for (gh, &val) in self.u_ghosts.iter().zip(&u_vals) {
|
||||
u[gh.idx] = val - gh.flux_sign * correction;
|
||||
}
|
||||
for (gh, &val) in self.v_ghosts.iter().zip(&v_vals) {
|
||||
v[gh.idx] = val - gh.flux_sign * correction;
|
||||
}
|
||||
for (gh, &val) in self.w_ghosts.iter().zip(&w_vals) {
|
||||
w[gh.idx] = val - gh.flux_sign * correction;
|
||||
}
|
||||
// The periodic seam: the w face at k = nz is the face at k = 0.
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let (f0, fn_) = (g.wface(0, j, i), g.wface(nz, j, i));
|
||||
if self.w_kind[f0] != FaceKind::Fluid && self.w_kind[fn_] == self.w_kind[f0] {
|
||||
w[fn_] = w[f0];
|
||||
}
|
||||
}
|
||||
}
|
||||
correction
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user