embedded3 item 10: the apertured cut-cell wall (AM-wall) — cutwall.rs classification, apertured projection with the compatible wall flux, cut predictor (V_u = αhA, averaged mass fluxes, implicit wall shear, inertia floor), cut load route; sphere MMS CutCell ≤ GhostBinary at n 12/24 (ratio 0.96), loads 9.3/10.7 % at n 24
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-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 4s
CI / Format Check (push) Failing after 12s
Documentation / Build User Guide (push) Successful in 5s
CI / Build (ubuntu-latest) (push) Failing after 1m50s
CI / Clippy Check (push) Failing after 2m5s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m40s
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-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 4s
CI / Format Check (push) Failing after 12s
Documentation / Build User Guide (push) Successful in 5s
CI / Build (ubuntu-latest) (push) Failing after 1m50s
CI / Clippy Check (push) Failing after 2m5s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m40s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d337afa8f9
commit
0e4c97ed24
@@ -0,0 +1,400 @@
|
|||||||
|
//! The apertured cut-cell wall (`WallScheme::CutCell`, item 10 — the
|
||||||
|
//! "AM-wall"): the cut geometry classifies the grid (a cell is fluid where
|
||||||
|
//! its fluid volume is positive; an interior face is an unknown where its
|
||||||
|
//! aperture is positive, prescribed the surface velocity otherwise — no
|
||||||
|
//! ghost faces), and the wall enters the operators through the apertures:
|
||||||
|
//! continuity `Σ_f A_f u_f·n_f + U_b·W_c = 0` per cell, the projection
|
||||||
|
//! coefficient `dt·A_f/δ`, the momentum control volume of an unknown face
|
||||||
|
//! `V_u = A_f h` with its own faces' apertures averaged from the two
|
||||||
|
//! adjacent cells (mass fluxes averaged, so the momentum volume conserves
|
||||||
|
//! mass exactly when the cells do), the wall closing it (`W = −Σ A n`),
|
||||||
|
//! an implicit wall shear `μ A_w (u_f − U_b)/d_f`, and an inertia floor of
|
||||||
|
//! 0.1 in the time derivative only. Every prescribed value is the limit of
|
||||||
|
//! the computed one as the aperture closes (the shear coefficient grows as
|
||||||
|
//! `1/A_f`), which is what makes the wall smooth in the interface position.
|
||||||
|
|
||||||
|
use super::Grid;
|
||||||
|
use super::body::Body;
|
||||||
|
use super::cut::CutGeometry;
|
||||||
|
use super::field::Field;
|
||||||
|
use super::step::{Boundaries, Side};
|
||||||
|
use super::wall::{FaceKind, Mask};
|
||||||
|
|
||||||
|
/// The inertia floor: the momentum volume's fraction in the time
|
||||||
|
/// derivative is at least this.
|
||||||
|
pub(super) const INERTIA_FLOOR: f64 = 0.1;
|
||||||
|
/// The wall-distance floor of a face, in units of the smallest spacing.
|
||||||
|
pub(super) const DISTANCE_FLOOR: f64 = 0.05;
|
||||||
|
|
||||||
|
/// Lattice addressing of faces and cells with the periodic wrap in z as
|
||||||
|
/// data: a face of component `c` at `p = [i, j, k]` (its own coordinate is
|
||||||
|
/// the face index, the others the cell's), a cell at `[i, j, k]`.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(super) struct Lattice {
|
||||||
|
pub(super) g: Grid,
|
||||||
|
pub(super) periodic_z: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lattice {
|
||||||
|
fn wrap_z(&self, k: i64, planes: i64) -> Option<usize> {
|
||||||
|
if self.periodic_z {
|
||||||
|
Some(k.rem_euclid(self.g.nz as i64) as usize)
|
||||||
|
} else if (0..planes).contains(&k) {
|
||||||
|
Some(k as usize)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index of the face of component `c` at `p`, `None` outside.
|
||||||
|
pub(super) fn face(&self, c: usize, p: [i64; 3]) -> Option<usize> {
|
||||||
|
let (nx, ny, nz) = (self.g.nx as i64, self.g.ny as i64, self.g.nz as i64);
|
||||||
|
let (i, j) = (p[0], p[1]);
|
||||||
|
let (ni, nj, nk) = match c {
|
||||||
|
0 => (nx + 1, ny, nz),
|
||||||
|
1 => (nx, ny + 1, nz),
|
||||||
|
_ => (nx, ny, nz + 1),
|
||||||
|
};
|
||||||
|
if !(0..ni).contains(&i) || !(0..nj).contains(&j) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let k = self.wrap_z(p[2], nk)?;
|
||||||
|
let (i, j) = (i as usize, j as usize);
|
||||||
|
Some(match c {
|
||||||
|
0 => self.g.uface(k, j, i),
|
||||||
|
1 => self.g.vface(k, j, i),
|
||||||
|
_ => self.g.wface(k, j, i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index of the cell at `p`, `None` outside.
|
||||||
|
pub(super) fn cell(&self, p: [i64; 3]) -> Option<usize> {
|
||||||
|
let (nx, ny, nz) = (self.g.nx as i64, self.g.ny as i64, self.g.nz as i64);
|
||||||
|
if !(0..nx).contains(&p[0]) || !(0..ny).contains(&p[1]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let k = self.wrap_z(p[2], nz)?;
|
||||||
|
Some(self.g.cell(k, p[1] as usize, p[0] as usize))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The centre of the face of component `c` at `p`.
|
||||||
|
pub(super) fn face_position(&self, c: usize, p: [i64; 3]) -> [f64; 3] {
|
||||||
|
let h = [self.g.dx, self.g.dy, self.g.dz];
|
||||||
|
let mut x = [0.0; 3];
|
||||||
|
for d in 0..3 {
|
||||||
|
let off = if d == c { 0.0 } else { 0.5 };
|
||||||
|
x[d] = (p[d] as f64 + off) * h[d];
|
||||||
|
}
|
||||||
|
x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The geometry of an unknown face's momentum control volume.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(super) struct CvGeometry {
|
||||||
|
/// The face's own aperture.
|
||||||
|
pub(super) alpha: f64,
|
||||||
|
/// The control volume's face apertures `[direction][minus, plus]`.
|
||||||
|
pub(super) ap: [[f64; 2]; 3],
|
||||||
|
/// The wall's vector area closing the control volume (into the body).
|
||||||
|
pub(super) wall: [f64; 3],
|
||||||
|
/// The wall distance of the face (floored).
|
||||||
|
pub(super) distance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mask {
|
||||||
|
/// Classify the grid against `body` at `t` by its cut geometry.
|
||||||
|
pub fn build_cut(body: &Body, g: Grid, t: f64, b: Boundaries) -> Result<Self, String> {
|
||||||
|
let cut = CutGeometry::build(body, g, t);
|
||||||
|
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
|
||||||
|
let periodic = b.z0 == Side::Periodic;
|
||||||
|
let allowed = |side: Side| matches!(side, Side::Velocity | Side::Periodic | Side::SlipWall);
|
||||||
|
let mut cell_fluid = vec![true; g.cells()];
|
||||||
|
let mut fluid_cells = 0;
|
||||||
|
let mut anchor = None;
|
||||||
|
for k in 0..nz {
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
let idx = g.cell(k, j, i);
|
||||||
|
let fluid = cut.vol[idx] > 0.0;
|
||||||
|
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 kind = |a: f64| {
|
||||||
|
if a > 0.0 {
|
||||||
|
FaceKind::Fluid
|
||||||
|
} else {
|
||||||
|
FaceKind::Solid
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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()];
|
||||||
|
for k in 0..nz {
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 1..nx {
|
||||||
|
let f = g.uface(k, j, i);
|
||||||
|
u_kind[f] = kind(cut.a_u[f]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j in 1..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
let f = g.vface(k, j, i);
|
||||||
|
v_kind[f] = kind(cut.a_v[f]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let w_range = if periodic { 0..nz + 1 } else { 1..nz };
|
||||||
|
for k in w_range {
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
let f = g.wface(k, j, i);
|
||||||
|
w_kind[f] = kind(cut.a_w[f]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
grid: g,
|
||||||
|
periodic_z: periodic,
|
||||||
|
cell_fluid,
|
||||||
|
u_kind,
|
||||||
|
v_kind,
|
||||||
|
w_kind,
|
||||||
|
u_ghosts: Vec::new(),
|
||||||
|
v_ghosts: Vec::new(),
|
||||||
|
w_ghosts: Vec::new(),
|
||||||
|
anchor,
|
||||||
|
fluid_cells,
|
||||||
|
cut: Some(cut),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn lattice(&self) -> Lattice {
|
||||||
|
Lattice {
|
||||||
|
g: self.grid,
|
||||||
|
periodic_z: self.periodic_z,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aperture of the face of component `c` at lattice `p` (1 without a
|
||||||
|
/// cut geometry), `None` outside the grid.
|
||||||
|
pub(super) fn aperture(&self, c: usize, p: [i64; 3]) -> Option<f64> {
|
||||||
|
let f = self.lattice().face(c, p)?;
|
||||||
|
Some(match c {
|
||||||
|
0 => self.a_u(f),
|
||||||
|
1 => self.a_v(f),
|
||||||
|
_ => self.a_w(f),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The momentum control volume of the unknown face of component `c` at
|
||||||
|
/// `p`: its face apertures are the averages of the two adjacent cells'
|
||||||
|
/// (the own-direction faces at the cell centres average the face's and
|
||||||
|
/// its own-direction neighbours' apertures), its wall closes them, its
|
||||||
|
/// wall distance is the face centre's signed distance moved to the
|
||||||
|
/// fluid part's centre, `φ + ½h(1 − α)`, floored.
|
||||||
|
pub(super) fn cv_geometry(&self, c: usize, p: [i64; 3]) -> CvGeometry {
|
||||||
|
let g = self.grid;
|
||||||
|
let h = [g.dx, g.dy, g.dz];
|
||||||
|
let area = [g.dy * g.dz, g.dx * g.dz, g.dx * g.dy];
|
||||||
|
let mut e = |d: usize| {
|
||||||
|
let mut v = [0i64; 3];
|
||||||
|
v[d] = 1;
|
||||||
|
v
|
||||||
|
};
|
||||||
|
let add =
|
||||||
|
|a: [i64; 3], b: [i64; 3], s: i64| [a[0] + s * b[0], a[1] + s * b[1], a[2] + s * b[2]];
|
||||||
|
let ec = e(c);
|
||||||
|
let cell_minus = add(p, ec, -1);
|
||||||
|
let cell_plus = p;
|
||||||
|
let alpha = self.aperture(c, p).unwrap_or(1.0);
|
||||||
|
let mut ap = [[1.0; 2]; 3];
|
||||||
|
for d in 0..3 {
|
||||||
|
let ed = e(d);
|
||||||
|
if d == c {
|
||||||
|
let am = self.aperture(c, add(p, ec, -1)).unwrap_or(alpha);
|
||||||
|
let apl = self.aperture(c, add(p, ec, 1)).unwrap_or(alpha);
|
||||||
|
ap[d] = [0.5 * (am + alpha), 0.5 * (alpha + apl)];
|
||||||
|
} else {
|
||||||
|
let minus = 0.5
|
||||||
|
* (self.aperture(d, cell_minus).unwrap_or(1.0)
|
||||||
|
+ self.aperture(d, cell_plus).unwrap_or(1.0));
|
||||||
|
let plus = 0.5
|
||||||
|
* (self.aperture(d, add(cell_minus, ed, 1)).unwrap_or(1.0)
|
||||||
|
+ self.aperture(d, add(cell_plus, ed, 1)).unwrap_or(1.0));
|
||||||
|
ap[d] = [minus, plus];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut wall = [0.0; 3];
|
||||||
|
for d in 0..3 {
|
||||||
|
wall[d] = -(ap[d][1] - ap[d][0]) * area[d];
|
||||||
|
}
|
||||||
|
let h_min = g.dx.min(g.dy).min(g.dz);
|
||||||
|
let phi_face = self.cut.as_ref().map_or(h_min, |cut| {
|
||||||
|
let f = self
|
||||||
|
.lattice()
|
||||||
|
.face(c, p)
|
||||||
|
.expect("unknown face inside the grid");
|
||||||
|
match c {
|
||||||
|
0 => cut.d_u[f],
|
||||||
|
1 => cut.d_v[f],
|
||||||
|
_ => cut.d_w[f],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let distance = (phi_face + 0.5 * h[c] * (1.0 - alpha)).max(DISTANCE_FLOOR * h_min);
|
||||||
|
CvGeometry {
|
||||||
|
alpha,
|
||||||
|
ap,
|
||||||
|
wall,
|
||||||
|
distance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The surface velocity component `c` at the foot of the normal from
|
||||||
|
/// the face centre.
|
||||||
|
pub fn surface_velocity_at(&self, body: &Body, x: [f64; 3], c: usize, t: f64) -> f64 {
|
||||||
|
let g = self.grid;
|
||||||
|
let eps = 1e-6 * g.dx.min(g.dy).min(g.dz);
|
||||||
|
let s = body.phi(x[0], x[1], x[2], t);
|
||||||
|
let (n1, n2, n3) = body.normal(x[0], x[1], x[2], t, eps);
|
||||||
|
let v = body.surface_velocity(x[0] - s * n1, x[1] - s * n2, x[2] - s * n3, t);
|
||||||
|
[v.0, v.1, v.2][c]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The volume fluxes of the surface velocity through every cell's wall
|
||||||
|
/// into the body, `U_b·W_c` (zero for a body at rest; the porous
|
||||||
|
/// manufactured surface's flux otherwise), made compatible: the net
|
||||||
|
/// flux (the quadrature's defect on a closed surface — a rigid
|
||||||
|
/// translation's is zero by closure) is redistributed over the wall
|
||||||
|
/// cells by wall area, as the binary wall's ghost fluxes are. Returns
|
||||||
|
/// the table and the correction (flux per unit wall area).
|
||||||
|
pub fn wall_flux_table(&self, body: &Body, t: f64) -> (Vec<f64>, f64) {
|
||||||
|
let mut table = vec![0.0; self.grid.cells()];
|
||||||
|
let Some(cut) = self.cut.as_ref() else {
|
||||||
|
return (table, 0.0);
|
||||||
|
};
|
||||||
|
let (mut net, mut area) = (0.0, 0.0);
|
||||||
|
for idx in 0..table.len() {
|
||||||
|
if !self.cell_fluid[idx] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let w = cut.wall[idx];
|
||||||
|
let a = (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt();
|
||||||
|
if a == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
table[idx] = self.wall_flux(body, idx, t);
|
||||||
|
net += table[idx];
|
||||||
|
area += a;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
let Some(cut) = self.cut.as_ref() else {
|
||||||
|
return 0.0;
|
||||||
|
};
|
||||||
|
let w = cut.wall[idx];
|
||||||
|
if w == [0.0; 3] {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let g = self.grid;
|
||||||
|
let (k, j, i) = g.kji(idx);
|
||||||
|
let x = [
|
||||||
|
(i as f64 + 0.5) * g.dx,
|
||||||
|
(j as f64 + 0.5) * g.dy,
|
||||||
|
(k as f64 + 0.5) * g.dz,
|
||||||
|
];
|
||||||
|
let mut flux = 0.0;
|
||||||
|
for (c, wc) in w.iter().enumerate() {
|
||||||
|
flux += self.surface_velocity_at(body, x, c, t) * wc;
|
||||||
|
}
|
||||||
|
flux
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cut-cell load route: the force on the body from the operators
|
||||||
|
/// themselves — `Σ_c p_c W_c` over the cells plus the implicit wall
|
||||||
|
/// 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 cut = self.cut.as_ref()?;
|
||||||
|
let g = self.grid;
|
||||||
|
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let lat = self.lattice();
|
||||||
|
let values: [&[f64]; 3] = [&f.u, &f.v, &f.w];
|
||||||
|
let w_range = if self.periodic_z { 0..nz } else { 1..nz };
|
||||||
|
for c in 0..3 {
|
||||||
|
let (ir, jr, kr) = match c {
|
||||||
|
0 => (1..nx, 0..ny, 0..nz),
|
||||||
|
1 => (0..nx, 1..ny, 0..nz),
|
||||||
|
_ => (0..nx, 0..ny, w_range.clone()),
|
||||||
|
};
|
||||||
|
for k in kr {
|
||||||
|
for j in jr.clone() {
|
||||||
|
for i in ir.clone() {
|
||||||
|
let p = [i as i64, j as i64, k as i64];
|
||||||
|
let idx = lat.face(c, p).expect("face");
|
||||||
|
let kind = match c {
|
||||||
|
0 => self.u_kind[idx],
|
||||||
|
1 => self.v_kind[idx],
|
||||||
|
_ => self.w_kind[idx],
|
||||||
|
};
|
||||||
|
if kind != FaceKind::Fluid {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cv = self.cv_geometry(c, p);
|
||||||
|
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 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ub = self.surface_velocity_at(body, lat.face_position(c, p), c, t);
|
||||||
|
force[c] += mu * a_w * (values[c][idx] - ub) / cv.distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(force)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -379,6 +379,7 @@ impl Mask {
|
|||||||
if !self.is_fluid_cell(idx) {
|
if !self.is_fluid_cell(idx) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let dv = dv * self.vol(idx);
|
||||||
let (fu0, fu1) = (g.uface(k, j, i), g.uface(k, j, i + 1));
|
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 (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 (fw0, fw1) = (g.wface(k, j, i), g.wface(k + 1, j, i));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
pub mod body;
|
pub mod body;
|
||||||
pub mod cut;
|
pub mod cut;
|
||||||
|
pub mod cutwall;
|
||||||
pub mod field;
|
pub mod field;
|
||||||
pub mod grid;
|
pub mod grid;
|
||||||
pub mod loads;
|
pub mod loads;
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
//! The predictor on the apertured cut-cell wall (`cutwall.rs`): one
|
||||||
|
//! routine for the three components, addressed on the face lattice. The
|
||||||
|
//! momentum control volume of an unknown face is `V_u = α h A` closed by
|
||||||
|
//! 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),
|
||||||
|
//! and the implicit wall shear `μ A_w (u − U_b)/d_f`; the time derivative
|
||||||
|
//! carries the inertia floor.
|
||||||
|
|
||||||
|
use super::{Side, Solver};
|
||||||
|
use crate::solvers::incompressible::embedded3::cutwall::INERTIA_FLOOR;
|
||||||
|
use crate::solvers::incompressible::embedded3::field::Field;
|
||||||
|
use crate::solvers::incompressible::simple::ConvectionScheme;
|
||||||
|
|
||||||
|
impl Solver {
|
||||||
|
/// The three components' predictors on the unknown faces; the
|
||||||
|
/// prescribed faces keep their imposed values.
|
||||||
|
pub(super) fn cut_predictor(&self, field: &mut Field, dt: f64, t_old: f64) {
|
||||||
|
let mask = self.mask.as_ref().expect("cut mask");
|
||||||
|
let g = field.grid;
|
||||||
|
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
|
||||||
|
let periodic = self.params.boundaries.periodic_z();
|
||||||
|
let lat = mask.lattice();
|
||||||
|
let w_range = if periodic { 0..nz } else { 1..nz };
|
||||||
|
for c in 0..3 {
|
||||||
|
let (ir, jr, kr) = match c {
|
||||||
|
0 => (1..nx, 0..ny, 0..nz),
|
||||||
|
1 => (0..nx, 1..ny, 0..nz),
|
||||||
|
_ => (0..nx, 0..ny, w_range.clone()),
|
||||||
|
};
|
||||||
|
let mut updates = Vec::new();
|
||||||
|
for k in kr {
|
||||||
|
for j in jr.clone() {
|
||||||
|
for i in ir.clone() {
|
||||||
|
let fluid = match c {
|
||||||
|
0 => self.u_is_fluid(k, j, i),
|
||||||
|
1 => self.v_is_fluid(k, j, i),
|
||||||
|
_ => self.w_is_fluid(k, j, i),
|
||||||
|
};
|
||||||
|
if !fluid {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p = [i as i64, j as i64, k as i64];
|
||||||
|
let idx = lat.face(c, p).expect("face");
|
||||||
|
updates.push((idx, self.cut_face_update(field, c, p, dt, t_old)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let out: &mut Vec<f64> = match c {
|
||||||
|
0 => &mut field.u,
|
||||||
|
1 => &mut field.v,
|
||||||
|
_ => &mut field.w,
|
||||||
|
};
|
||||||
|
for (idx, val) in updates {
|
||||||
|
out[idx] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if periodic {
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
field.w[g.wface(nz, j, i)] = field.w[g.wface(0, j, i)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The predicted value of the unknown face of component `c` at lattice
|
||||||
|
/// `p` from the old field.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn cut_face_update(&self, field: &Field, c: usize, p: [i64; 3], dt: f64, t_old: f64) -> f64 {
|
||||||
|
let mask = self.mask.as_ref().expect("cut mask");
|
||||||
|
let body = self.body.as_ref().expect("body");
|
||||||
|
let g = field.grid;
|
||||||
|
let h = [g.dx, g.dy, g.dz];
|
||||||
|
let n = [g.nx as f64, g.ny as f64, g.nz as f64];
|
||||||
|
let area = [g.dy * g.dz, g.dx * g.dz, g.dx * g.dy];
|
||||||
|
let rho = self.fluid.density;
|
||||||
|
let mu = self.fluid.viscosity;
|
||||||
|
let b = self.params.boundaries;
|
||||||
|
let sides = [[b.x0, b.x1], [b.y0, b.y1], [b.z0, b.z1]];
|
||||||
|
let scheme = self.params.convection_scheme;
|
||||||
|
let lat = mask.lattice();
|
||||||
|
let old: [&[f64]; 3] = [&field.u_old, &field.v_old, &field.w_old];
|
||||||
|
let e = |d: usize| {
|
||||||
|
let mut v = [0i64; 3];
|
||||||
|
v[d] = 1;
|
||||||
|
v
|
||||||
|
};
|
||||||
|
let add =
|
||||||
|
|a: [i64; 3], b: [i64; 3], s: i64| [a[0] + s * b[0], a[1] + s * b[1], a[2] + s * b[2]];
|
||||||
|
// The old value of a face of component `cc` at `q`, `None` outside.
|
||||||
|
let val = |cc: usize, q: [i64; 3]| lat.face(cc, q).map(|f| old[cc][f]);
|
||||||
|
let ap = |cc: usize, q: [i64; 3]| mask.aperture(cc, q);
|
||||||
|
let cv = mask.cv_geometry(c, p);
|
||||||
|
let x = lat.face_position(c, p);
|
||||||
|
let u0 = val(c, p).expect("the face");
|
||||||
|
let ec = e(c);
|
||||||
|
let cell_minus = add(p, ec, -1);
|
||||||
|
let cell_plus = p;
|
||||||
|
let ub = mask.surface_velocity_at(body, x, c, t_old);
|
||||||
|
|
||||||
|
let mut mass_out = 0.0;
|
||||||
|
let mut conv = 0.0;
|
||||||
|
let mut diff = 0.0;
|
||||||
|
for d in 0..3 {
|
||||||
|
let ed = e(d);
|
||||||
|
let a_d = area[d];
|
||||||
|
// Neighbouring faces of this component along d (None beyond a wall).
|
||||||
|
let up1 = val(c, add(p, ed, 1));
|
||||||
|
let up2 = val(c, add(p, ed, 2));
|
||||||
|
let dn1 = val(c, add(p, ed, -1));
|
||||||
|
let dn2 = val(c, add(p, ed, -2));
|
||||||
|
// The control volume's mass fluxes through its plus / minus faces
|
||||||
|
// along d: the averages of the two adjacent cells' face fluxes
|
||||||
|
// (own direction: the face's and its neighbours' fluxes).
|
||||||
|
let (m_plus, m_minus) = if d == c {
|
||||||
|
let f_up = ap(c, add(p, ec, 1)).unwrap_or(cv.alpha) * up1.unwrap_or(u0);
|
||||||
|
let f_dn = ap(c, add(p, ec, -1)).unwrap_or(cv.alpha) * dn1.unwrap_or(u0);
|
||||||
|
let f0 = cv.alpha * u0;
|
||||||
|
(0.5 * (f0 + f_up) * a_d, 0.5 * (f_dn + f0) * a_d)
|
||||||
|
} else {
|
||||||
|
let flux = |q: [i64; 3]| ap(d, q).unwrap_or(1.0) * val(d, q).unwrap_or(0.0);
|
||||||
|
(
|
||||||
|
0.5 * (flux(add(cell_minus, ed, 1)) + flux(add(cell_plus, ed, 1))) * a_d,
|
||||||
|
0.5 * (flux(cell_minus) + flux(cell_plus)) * a_d,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
mass_out += m_plus - m_minus;
|
||||||
|
// Beyond a domain side along d: the boundary value on a Velocity
|
||||||
|
// side, the face's own value (mirror) otherwise.
|
||||||
|
let beyond = |plus: bool| {
|
||||||
|
let side = sides[d][usize::from(plus)];
|
||||||
|
if side == Side::Velocity {
|
||||||
|
let mut xb = x;
|
||||||
|
xb[d] = if plus { n[d] * h[d] } else { 0.0 };
|
||||||
|
[
|
||||||
|
self.boundary(xb[0], xb[1], xb[2], t_old).0,
|
||||||
|
self.boundary(xb[0], xb[1], xb[2], t_old).1,
|
||||||
|
self.boundary(xb[0], xb[1], xb[2], t_old).2,
|
||||||
|
][c]
|
||||||
|
} else {
|
||||||
|
u0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Convection through the plus face.
|
||||||
|
let (u_plus, delta_plus) = match up1 {
|
||||||
|
Some(un) => {
|
||||||
|
let delta = if scheme == ConvectionScheme::Upwind {
|
||||||
|
0.0
|
||||||
|
} else if m_plus >= 0.0 {
|
||||||
|
scheme.face_correction(dn1, u0, un)
|
||||||
|
} else {
|
||||||
|
scheme.face_correction(up2, un, u0)
|
||||||
|
};
|
||||||
|
(Self::upwind(m_plus, u0, un), delta)
|
||||||
|
}
|
||||||
|
None => (Self::upwind(m_plus, u0, beyond(true)), 0.0),
|
||||||
|
};
|
||||||
|
let (u_minus, delta_minus) = match dn1 {
|
||||||
|
Some(ud) => {
|
||||||
|
let delta = if scheme == ConvectionScheme::Upwind {
|
||||||
|
0.0
|
||||||
|
} else if m_minus >= 0.0 {
|
||||||
|
scheme.face_correction(dn2, ud, u0)
|
||||||
|
} else {
|
||||||
|
scheme.face_correction(up1, u0, ud)
|
||||||
|
};
|
||||||
|
(Self::upwind(m_minus, ud, u0), delta)
|
||||||
|
}
|
||||||
|
None => (Self::upwind(m_minus, beyond(false), u0), 0.0),
|
||||||
|
};
|
||||||
|
conv += m_plus * (u_plus + delta_plus) - m_minus * (u_minus + delta_minus);
|
||||||
|
// Diffusion through the plus / minus faces.
|
||||||
|
let (g_minus, g_plus) = (cv.ap[d][0], cv.ap[d][1]);
|
||||||
|
diff += match up1 {
|
||||||
|
Some(un) => mu * g_plus * a_d * (un - u0) / h[d],
|
||||||
|
None => {
|
||||||
|
if sides[d][1] == Side::Velocity {
|
||||||
|
mu * g_plus * a_d * (beyond(true) - u0) / (0.5 * h[d])
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
diff -= match dn1 {
|
||||||
|
Some(ud) => mu * g_minus * a_d * (u0 - ud) / h[d],
|
||||||
|
None => {
|
||||||
|
if sides[d][0] == Side::Velocity {
|
||||||
|
mu * g_minus * a_d * (u0 - beyond(false)) / (0.5 * h[d])
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// The wall's momentum flux closes the mass balance exactly.
|
||||||
|
conv -= mass_out * ub;
|
||||||
|
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];
|
||||||
|
let v_u = cv.alpha * h[c] * area[c];
|
||||||
|
let source = self.momentum_source.as_ref().map_or(0.0, |f| {
|
||||||
|
let s = f(x[0], x[1], x[2], t_old);
|
||||||
|
[s.0, s.1, s.2][c] * v_u
|
||||||
|
});
|
||||||
|
let a_w =
|
||||||
|
(cv.wall[0] * cv.wall[0] + cv.wall[1] * cv.wall[1] + cv.wall[2] * cv.wall[2]).sqrt();
|
||||||
|
let shear = mu * a_w / cv.distance;
|
||||||
|
let v_eff = cv.alpha.max(INERTIA_FLOOR) * h[c] * area[c];
|
||||||
|
let inertia = rho * v_eff / dt;
|
||||||
|
(inertia * u0 - conv + diff + pressure + source + shear * ub) / (inertia + shear)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
//! `dz = 1`) every number is the 2D solver's. The fluid predicates are the
|
//! `dz = 1`) every number is the 2D solver's. The fluid predicates are the
|
||||||
//! wall's hooks (item 9).
|
//! wall's hooks (item 9).
|
||||||
|
|
||||||
|
mod cut_predictor;
|
||||||
#[cfg(feature = "cuda")]
|
#[cfg(feature = "cuda")]
|
||||||
pub mod device;
|
pub mod device;
|
||||||
mod predictor;
|
mod predictor;
|
||||||
@@ -110,6 +111,7 @@ pub struct Solver {
|
|||||||
body: Option<Body>,
|
body: Option<Body>,
|
||||||
mask: Option<Mask>,
|
mask: Option<Mask>,
|
||||||
last_ghost_correction: f64,
|
last_ghost_correction: f64,
|
||||||
|
wall_fluxes: Vec<f64>,
|
||||||
pcg_cache: PcgCache,
|
pcg_cache: PcgCache,
|
||||||
time: f64,
|
time: f64,
|
||||||
initialized: bool,
|
initialized: bool,
|
||||||
@@ -137,6 +139,7 @@ impl Solver {
|
|||||||
body: None,
|
body: None,
|
||||||
mask: None,
|
mask: None,
|
||||||
last_ghost_correction: 0.0,
|
last_ghost_correction: 0.0,
|
||||||
|
wall_fluxes: Vec::new(),
|
||||||
pcg_cache: PcgCache::default(),
|
pcg_cache: PcgCache::default(),
|
||||||
time: 0.0,
|
time: 0.0,
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -174,7 +177,8 @@ impl Solver {
|
|||||||
self.mask.as_ref()
|
self.mask.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The last step's ghost compatibility correction.
|
/// The last step's compatibility correction: the binary wall's shared
|
||||||
|
/// ghost flux correction, or the cut wall's wall-flux correction.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn ghost_correction(&self) -> f64 {
|
pub fn ghost_correction(&self) -> f64 {
|
||||||
self.last_ghost_correction
|
self.last_ghost_correction
|
||||||
@@ -226,6 +230,36 @@ impl Solver {
|
|||||||
.is_none_or(|m| m.is_fluid_cell(m.grid().cell(k, j, i)))
|
.is_none_or(|m| m.is_fluid_cell(m.grid().cell(k, j, i)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The apertures (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)))
|
||||||
|
}
|
||||||
|
#[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)))
|
||||||
|
}
|
||||||
|
#[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)))
|
||||||
|
}
|
||||||
|
/// 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).
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn wall_flux(&self, idx: usize) -> f64 {
|
||||||
|
self.wall_fluxes[idx]
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn has_cut(&self) -> bool {
|
||||||
|
self.mask.as_ref().is_some_and(|m| m.cut().is_some())
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn upwind(face_velocity: f64, upstream: f64, downstream: f64) -> f64 {
|
pub(super) fn upwind(face_velocity: f64, upstream: f64, downstream: f64) -> f64 {
|
||||||
if face_velocity >= 0.0 {
|
if face_velocity >= 0.0 {
|
||||||
upstream
|
upstream
|
||||||
@@ -283,7 +317,10 @@ impl Solver {
|
|||||||
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
|
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
|
||||||
let b = self.params.boundaries;
|
let b = self.params.boundaries;
|
||||||
let periodic = b.periodic_z();
|
let periodic = b.periodic_z();
|
||||||
for k in 0..nz {
|
if self.has_cut() {
|
||||||
|
self.cut_predictor(field, dt, t_old);
|
||||||
|
}
|
||||||
|
for k in (0..nz).filter(|_| !self.has_cut()) {
|
||||||
for j in 0..ny {
|
for j in 0..ny {
|
||||||
for i in 1..nx {
|
for i in 1..nx {
|
||||||
if !self.u_is_fluid(k, j, i) {
|
if !self.u_is_fluid(k, j, i) {
|
||||||
@@ -295,7 +332,7 @@ impl Solver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k in 0..nz {
|
for k in (0..nz).filter(|_| !self.has_cut()) {
|
||||||
for j in 1..ny {
|
for j in 1..ny {
|
||||||
for i in 0..nx {
|
for i in 0..nx {
|
||||||
if !self.v_is_fluid(k, j, i) {
|
if !self.v_is_fluid(k, j, i) {
|
||||||
@@ -308,7 +345,7 @@ impl Solver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let k_range = if periodic { 0..nz } else { 1..nz };
|
let k_range = if periodic { 0..nz } else { 1..nz };
|
||||||
for k in k_range {
|
for k in k_range.filter(|_| !self.has_cut()) {
|
||||||
for j in 0..ny {
|
for j in 0..ny {
|
||||||
for i in 0..nx {
|
for i in 0..nx {
|
||||||
if !self.w_is_fluid(k, j, i) {
|
if !self.w_is_fluid(k, j, i) {
|
||||||
@@ -365,15 +402,15 @@ impl Solver {
|
|||||||
let t = self.time;
|
let t = self.time;
|
||||||
if let Some(body) = &self.body {
|
if let Some(body) = &self.body {
|
||||||
if self.mask.is_none() {
|
if self.mask.is_none() {
|
||||||
assert_eq!(
|
let mask = match self.params.wall_scheme {
|
||||||
self.params.wall_scheme,
|
WallScheme::GhostBinary => {
|
||||||
WallScheme::GhostBinary,
|
|
||||||
"item 10 brings CutCell"
|
|
||||||
);
|
|
||||||
self.mask = Some(
|
|
||||||
Mask::build(body, field.grid, t, self.params.boundaries)
|
Mask::build(body, field.grid, t, self.params.boundaries)
|
||||||
.expect("embedded mask"),
|
}
|
||||||
);
|
WallScheme::CutCell => {
|
||||||
|
Mask::build_cut(body, field.grid, t, self.params.boundaries)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.mask = Some(mask.expect("embedded mask"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.apply_boundary_normals(field, t);
|
self.apply_boundary_normals(field, t);
|
||||||
@@ -398,6 +435,14 @@ impl Solver {
|
|||||||
self.momentum_predictor(field, dt, t_old);
|
self.momentum_predictor(field, dt, t_old);
|
||||||
self.apply_boundary_normals(field, t_new);
|
self.apply_boundary_normals(field, t_new);
|
||||||
field.copy_to_starred();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut total = 0;
|
let mut total = 0;
|
||||||
let mut final_residual = f64::INFINITY;
|
let mut final_residual = f64::INFINITY;
|
||||||
let mut poisson_iterations = 0;
|
let mut poisson_iterations = 0;
|
||||||
@@ -414,8 +459,8 @@ impl Solver {
|
|||||||
}
|
}
|
||||||
// Ghost faces follow the corrected field (the next step's stencil data).
|
// Ghost faces follow the corrected field (the next step's stencil data).
|
||||||
if let (Some(body), Some(mask)) = (&self.body, &self.mask) {
|
if let (Some(body), Some(mask)) = (&self.body, &self.mask) {
|
||||||
self.last_ghost_correction =
|
let imposed = mask.impose(body, &mut field.u, &mut field.v, &mut field.w, t_new);
|
||||||
mask.impose(body, &mut field.u, &mut field.v, &mut field.w, t_new);
|
self.last_ghost_correction = cut_correction.unwrap_or(imposed);
|
||||||
}
|
}
|
||||||
self.time = t_new;
|
self.time = t_new;
|
||||||
StepResult {
|
StepResult {
|
||||||
|
|||||||
+30
-16
@@ -39,42 +39,42 @@ impl Solver {
|
|||||||
extra += ae_outlet;
|
extra += ae_outlet;
|
||||||
}
|
}
|
||||||
} else if self.u_is_fluid(k, j, i + 1) {
|
} else if self.u_is_fluid(k, j, i + 1) {
|
||||||
problem.ae[idx] = ae_interior;
|
problem.ae[idx] = ae_interior * self.au(k, j, i + 1);
|
||||||
}
|
}
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
if b.x0 == outlet {
|
if b.x0 == outlet {
|
||||||
extra += ae_outlet;
|
extra += ae_outlet;
|
||||||
}
|
}
|
||||||
} else if self.u_is_fluid(k, j, i) {
|
} else if self.u_is_fluid(k, j, i) {
|
||||||
problem.aw[idx] = ae_interior;
|
problem.aw[idx] = ae_interior * self.au(k, j, i);
|
||||||
}
|
}
|
||||||
if j + 1 == ny {
|
if j + 1 == ny {
|
||||||
if b.y1 == outlet {
|
if b.y1 == outlet {
|
||||||
extra += an_outlet;
|
extra += an_outlet;
|
||||||
}
|
}
|
||||||
} else if self.v_is_fluid(k, j + 1, i) {
|
} else if self.v_is_fluid(k, j + 1, i) {
|
||||||
problem.an[idx] = an_interior;
|
problem.an[idx] = an_interior * self.av(k, j + 1, i);
|
||||||
}
|
}
|
||||||
if j == 0 {
|
if j == 0 {
|
||||||
if b.y0 == outlet {
|
if b.y0 == outlet {
|
||||||
extra += an_outlet;
|
extra += an_outlet;
|
||||||
}
|
}
|
||||||
} else if self.v_is_fluid(k, j, i) {
|
} else if self.v_is_fluid(k, j, i) {
|
||||||
problem.as_[idx] = an_interior;
|
problem.as_[idx] = an_interior * self.av(k, j, i);
|
||||||
}
|
}
|
||||||
if k + 1 == nz && !periodic {
|
if k + 1 == nz && !periodic {
|
||||||
if b.z1 == outlet {
|
if b.z1 == outlet {
|
||||||
extra += at_outlet;
|
extra += at_outlet;
|
||||||
}
|
}
|
||||||
} else if self.w_is_fluid((k + 1) % nz, j, i) {
|
} else if self.w_is_fluid((k + 1) % nz, j, i) {
|
||||||
problem.at[idx] = at_interior;
|
problem.at[idx] = at_interior * self.aw((k + 1) % nz, j, i);
|
||||||
}
|
}
|
||||||
if k == 0 && !periodic {
|
if k == 0 && !periodic {
|
||||||
if b.z0 == outlet {
|
if b.z0 == outlet {
|
||||||
extra += at_outlet;
|
extra += at_outlet;
|
||||||
}
|
}
|
||||||
} else if self.w_is_fluid(k, j, i) {
|
} else if self.w_is_fluid(k, j, i) {
|
||||||
problem.ab[idx] = at_interior;
|
problem.ab[idx] = at_interior * self.aw(k, j, i);
|
||||||
}
|
}
|
||||||
problem.extra_diag[idx] = extra;
|
problem.extra_diag[idx] = extra;
|
||||||
}
|
}
|
||||||
@@ -246,6 +246,7 @@ impl Solver {
|
|||||||
let g = field.grid;
|
let g = field.grid;
|
||||||
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
||||||
let rho = self.fluid.density;
|
let rho = self.fluid.density;
|
||||||
|
let cut = self.has_cut();
|
||||||
let mut source_scale = 0.0;
|
let mut source_scale = 0.0;
|
||||||
for k in 0..nz {
|
for k in 0..nz {
|
||||||
for j in 0..ny {
|
for j in 0..ny {
|
||||||
@@ -255,15 +256,19 @@ impl Solver {
|
|||||||
field.sp[idx] = 0.0;
|
field.sp[idx] = 0.0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let divergence_flux = rho
|
let mut divergence_flux = rho
|
||||||
* ((field.u_star[g.uface(k, j, i + 1)] - field.u_star[g.uface(k, j, i)])
|
* ((self.au(k, j, i + 1) * field.u_star[g.uface(k, j, i + 1)]
|
||||||
|
- self.au(k, j, i) * field.u_star[g.uface(k, j, i)])
|
||||||
* (dy * dz)
|
* (dy * dz)
|
||||||
+ (field.v_star[g.vface(k, j + 1, i)]
|
+ (self.av(k, j + 1, i) * field.v_star[g.vface(k, j + 1, i)]
|
||||||
- field.v_star[g.vface(k, j, i)])
|
- self.av(k, j, i) * field.v_star[g.vface(k, j, i)])
|
||||||
* (dx * dz)
|
* (dx * dz)
|
||||||
+ (field.w_star[g.wface(k + 1, j, i)]
|
+ (self.aw(k + 1, j, i) * field.w_star[g.wface(k + 1, j, i)]
|
||||||
- field.w_star[g.wface(k, j, i)])
|
- self.aw(k, j, i) * field.w_star[g.wface(k, j, i)])
|
||||||
* (dx * dy));
|
* (dx * dy));
|
||||||
|
if cut {
|
||||||
|
divergence_flux += rho * self.wall_flux(idx);
|
||||||
|
}
|
||||||
field.sp[idx] = -divergence_flux;
|
field.sp[idx] = -divergence_flux;
|
||||||
source_scale += divergence_flux.abs();
|
source_scale += divergence_flux.abs();
|
||||||
}
|
}
|
||||||
@@ -315,6 +320,7 @@ impl Solver {
|
|||||||
let g = field.grid;
|
let g = field.grid;
|
||||||
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
let (nx, ny, nz, dx, dy, dz) = (g.nx, g.ny, g.nz, g.dx, g.dy, g.dz);
|
||||||
let rho = self.fluid.density;
|
let rho = self.fluid.density;
|
||||||
|
let cut = self.has_cut();
|
||||||
let b = self.params.boundaries;
|
let b = self.params.boundaries;
|
||||||
let outlet = Side::PressureOutlet;
|
let outlet = Side::PressureOutlet;
|
||||||
let periodic = b.periodic_z();
|
let periodic = b.periodic_z();
|
||||||
@@ -402,12 +408,20 @@ impl Solver {
|
|||||||
if !self.cell_is_fluid(k, j, i) {
|
if !self.cell_is_fluid(k, j, i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let divergence_flux = rho
|
let idx = g.cell(k, j, i);
|
||||||
* ((field.u[g.uface(k, j, i + 1)] - field.u[g.uface(k, j, i)]) * (dy * dz)
|
let mut divergence_flux = rho
|
||||||
+ (field.v[g.vface(k, j + 1, i)] - field.v[g.vface(k, j, i)])
|
* ((self.au(k, j, i + 1) * field.u[g.uface(k, j, i + 1)]
|
||||||
|
- self.au(k, j, i) * field.u[g.uface(k, j, i)])
|
||||||
|
* (dy * dz)
|
||||||
|
+ (self.av(k, j + 1, i) * field.v[g.vface(k, j + 1, i)]
|
||||||
|
- self.av(k, j, i) * field.v[g.vface(k, j, i)])
|
||||||
* (dx * dz)
|
* (dx * dz)
|
||||||
+ (field.w[g.wface(k + 1, j, i)] - field.w[g.wface(k, j, i)])
|
+ (self.aw(k + 1, j, i) * field.w[g.wface(k + 1, j, i)]
|
||||||
|
- self.aw(k, j, i) * field.w[g.wface(k, j, i)])
|
||||||
* (dx * dy));
|
* (dx * dy));
|
||||||
|
if cut {
|
||||||
|
divergence_flux += rho * self.wall_flux(idx);
|
||||||
|
}
|
||||||
mass_imbalance += divergence_flux.abs();
|
mass_imbalance += divergence_flux.abs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
use super::Grid;
|
use super::Grid;
|
||||||
use super::body::Body;
|
use super::body::Body;
|
||||||
|
use super::cut::CutGeometry;
|
||||||
use super::step::{Boundaries, Side};
|
use super::step::{Boundaries, Side};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -45,7 +46,7 @@ pub(crate) struct StencilNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Ghost {
|
pub(super) struct Ghost {
|
||||||
idx: usize,
|
idx: usize,
|
||||||
x: f64,
|
x: f64,
|
||||||
y: f64,
|
y: f64,
|
||||||
@@ -62,17 +63,20 @@ struct Ghost {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Mask {
|
pub struct Mask {
|
||||||
grid: Grid,
|
pub(super) grid: Grid,
|
||||||
periodic_z: bool,
|
pub(super) periodic_z: bool,
|
||||||
cell_fluid: Vec<bool>,
|
pub(super) cell_fluid: Vec<bool>,
|
||||||
u_kind: Vec<FaceKind>,
|
pub(super) u_kind: Vec<FaceKind>,
|
||||||
v_kind: Vec<FaceKind>,
|
pub(super) v_kind: Vec<FaceKind>,
|
||||||
w_kind: Vec<FaceKind>,
|
pub(super) w_kind: Vec<FaceKind>,
|
||||||
u_ghosts: Vec<Ghost>,
|
pub(super) u_ghosts: Vec<Ghost>,
|
||||||
v_ghosts: Vec<Ghost>,
|
pub(super) v_ghosts: Vec<Ghost>,
|
||||||
w_ghosts: Vec<Ghost>,
|
pub(super) w_ghosts: Vec<Ghost>,
|
||||||
anchor: usize,
|
pub(super) anchor: usize,
|
||||||
fluid_cells: 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 z lattice position of a query: the lower plane index, the upper
|
/// The z lattice position of a query: the lower plane index, the upper
|
||||||
@@ -479,9 +483,38 @@ impl Mask {
|
|||||||
w_ghosts,
|
w_ghosts,
|
||||||
anchor,
|
anchor,
|
||||||
fluid_cells,
|
fluid_cells,
|
||||||
|
cut: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 vol(&self, idx: usize) -> f64 {
|
||||||
|
self.cut.as_ref().map_or(1.0, |c| c.vol[idx])
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_fluid_cell(&self, idx: usize) -> bool {
|
pub fn is_fluid_cell(&self, idx: usize) -> bool {
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
//! embedded3 gate 9a: the manufactured solution with an embedded sphere
|
//! embedded3 gates 9a and 10: the manufactured solution with an embedded
|
||||||
//! (centre (0.6, 0.45, 0.5), r 0.2, off-centre so the exact force is not
|
//! sphere (centre (0.6, 0.45, 0.5), r 0.2, off-centre so the exact force is
|
||||||
//! zero by symmetry) carrying the exact field as its surface velocity, on
|
//! not zero by symmetry) carrying the exact field as its surface velocity,
|
||||||
//! the binary ghost wall. The velocity error falls at the scheme's order,
|
//! on the binary ghost wall (item 9) and the apertured cut-cell wall (item
|
||||||
//! every fluid cell is divergence-free, the compatibility correction
|
//! 10). The velocity error falls at the scheme's order, every fluid cell
|
||||||
//! shrinks, and both load routes converge to the exact surface integral of
|
//! is divergence-free (apertured, with the porous surface's flux, on the
|
||||||
//! the manufactured stress (the control-volume route measures F − M with M
|
//! cut wall), the compatibility correction shrinks, and both load routes
|
||||||
//! the momentum flux through the porous manufactured surface).
|
//! converge to the exact surface integral of the manufactured stress (the
|
||||||
|
//! control-volume route measures F − M with M the momentum flux through
|
||||||
|
//! the porous manufactured surface). Item 10's gate: the cut wall's errors
|
||||||
|
//! are at most the binary wall's at every n, its loads within 10 % at the
|
||||||
|
//! finest rung.
|
||||||
|
|
||||||
use rtx_cfd::solvers::incompressible::ConvectionScheme;
|
use rtx_cfd::solvers::incompressible::ConvectionScheme;
|
||||||
use rtx_cfd::solvers::incompressible::embedded3::{Body, Field, Fluid, Grid, Parameters, Solver};
|
use rtx_cfd::solvers::incompressible::embedded3::{
|
||||||
|
Body, FaceKind, Field, Fluid, Grid, Parameters, Solver, WallScheme,
|
||||||
|
};
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
const RHO: f64 = 1.0;
|
const RHO: f64 = 1.0;
|
||||||
@@ -114,7 +120,7 @@ struct Measurement {
|
|||||||
force_cv: [f64; 3],
|
force_cv: [f64; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure(n: usize) -> Measurement {
|
fn measure(n: usize, scheme: WallScheme) -> Measurement {
|
||||||
let h = 1.0 / n as f64;
|
let h = 1.0 / n as f64;
|
||||||
let dt = 0.4 * (h * h / (4.0 * MU / RHO)).min(h);
|
let dt = 0.4 * (h * h / (4.0 * MU / RHO)).min(h);
|
||||||
let mut solver = Solver::new(
|
let mut solver = Solver::new(
|
||||||
@@ -128,6 +134,7 @@ fn measure(n: usize) -> Measurement {
|
|||||||
corrector_steps: 2,
|
corrector_steps: 2,
|
||||||
tolerance: 1e-8,
|
tolerance: 1e-8,
|
||||||
convection_scheme: ConvectionScheme::Upwind,
|
convection_scheme: ConvectionScheme::Upwind,
|
||||||
|
wall_scheme: scheme,
|
||||||
..Parameters::default()
|
..Parameters::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -140,9 +147,10 @@ fn measure(n: usize) -> Measurement {
|
|||||||
let g = Grid::cubic(n, n, n, h);
|
let g = Grid::cubic(n, n, n, h);
|
||||||
let mut f = Field::new(g);
|
let mut f = Field::new(g);
|
||||||
solver.initialize(&mut f);
|
solver.initialize(&mut f);
|
||||||
|
let mut last = solver.advance(&mut f, dt);
|
||||||
for _ in 0..200_000 {
|
for _ in 0..200_000 {
|
||||||
let (bu, bv, bw) = (f.u.clone(), f.v.clone(), f.w.clone());
|
let (bu, bv, bw) = (f.u.clone(), f.v.clone(), f.w.clone());
|
||||||
solver.advance(&mut f, dt);
|
last = solver.advance(&mut f, dt);
|
||||||
let mut change = 0.0_f64;
|
let mut change = 0.0_f64;
|
||||||
for (a, b) in
|
for (a, b) in
|
||||||
f.u.iter()
|
f.u.iter()
|
||||||
@@ -157,7 +165,6 @@ fn measure(n: usize) -> Measurement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mask = solver.mask().expect("mask");
|
let mask = solver.mask().expect("mask");
|
||||||
use rtx_cfd::solvers::incompressible::embedded3::FaceKind;
|
|
||||||
let (mut sq, mut vol) = (0.0, 0.0);
|
let (mut sq, mut vol) = (0.0, 0.0);
|
||||||
let dv = h * h * h;
|
let dv = h * h * h;
|
||||||
for k in 0..n {
|
for k in 0..n {
|
||||||
@@ -194,21 +201,53 @@ fn measure(n: usize) -> Measurement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let body = solver.body().expect("body");
|
||||||
|
let t = solver.time();
|
||||||
|
// The apertured divergence per unit volume, the porous surface's flux
|
||||||
|
// through the wall included (the plain divergence on the binary wall).
|
||||||
let mut max_div = 0.0_f64;
|
let mut max_div = 0.0_f64;
|
||||||
|
let mut at_vol = 1.0;
|
||||||
|
let mut sum_flux = 0.0;
|
||||||
|
let (wall_fluxes, _) = mask.wall_flux_table(body, t);
|
||||||
for k in 0..n {
|
for k in 0..n {
|
||||||
for j in 0..n {
|
for j in 0..n {
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
if mask.is_fluid_cell(g.cell(k, j, i)) {
|
let idx = g.cell(k, j, i);
|
||||||
let div = (f.u[g.uface(k, j, i + 1)] - f.u[g.uface(k, j, i)]) / h
|
if mask.is_fluid_cell(idx) {
|
||||||
+ (f.v[g.vface(k, j + 1, i)] - f.v[g.vface(k, j, i)]) / h
|
let flux = (mask.a_u(g.uface(k, j, i + 1)) * f.u[g.uface(k, j, i + 1)]
|
||||||
+ (f.w[g.wface(k + 1, j, i)] - f.w[g.wface(k, j, i)]) / h;
|
- mask.a_u(g.uface(k, j, i)) * f.u[g.uface(k, j, i)])
|
||||||
max_div = max_div.max(div.abs());
|
* h
|
||||||
|
* h
|
||||||
|
+ (mask.a_v(g.vface(k, j + 1, i)) * f.v[g.vface(k, j + 1, i)]
|
||||||
|
- mask.a_v(g.vface(k, j, i)) * f.v[g.vface(k, j, i)])
|
||||||
|
* h
|
||||||
|
* h
|
||||||
|
+ (mask.a_w(g.wface(k + 1, j, i)) * f.w[g.wface(k + 1, j, i)]
|
||||||
|
- mask.a_w(g.wface(k, j, i)) * f.w[g.wface(k, j, i)])
|
||||||
|
* h
|
||||||
|
* h
|
||||||
|
+ wall_fluxes[idx];
|
||||||
|
sum_flux += flux.abs();
|
||||||
|
if (flux / (h * h * h)).abs() > max_div {
|
||||||
|
max_div = (flux / (h * h * h)).abs();
|
||||||
|
at_vol = mask.vol(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let body = solver.body().expect("body");
|
}
|
||||||
let surface = mask.surface_force(body, &f, MU, solver.time(), 0.5 * h);
|
println!(
|
||||||
|
" [{scheme:?} n {n}] max div {max_div:.2e} in a cell of fluid fraction {at_vol:.3e}; Σ|flux| {sum_flux:.2e}; last step residual {:.2e}",
|
||||||
|
last.final_residual
|
||||||
|
);
|
||||||
|
let surface = match scheme {
|
||||||
|
WallScheme::GhostBinary => mask.surface_force(body, &f, MU, t, 0.5 * h),
|
||||||
|
WallScheme::CutCell => rtx_cfd::solvers::incompressible::embedded3::SurfaceForce {
|
||||||
|
f: mask.cut_wall_force(body, &f, MU, t).expect("cut wall"),
|
||||||
|
samples: 0,
|
||||||
|
skipped: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
let (i0, i1) = (n / 8, n - n / 8);
|
let (i0, i1) = (n / 8, n - n / 8);
|
||||||
let src = |x: f64, y: f64, z: f64| source3(x, y, z);
|
let src = |x: f64, y: f64, z: f64| source3(x, y, z);
|
||||||
let force_cv = mask.control_volume_force(&f, dt, RHO, MU, Some(&src), (i0, i1, i0, i1, i0, i1));
|
let force_cv = mask.control_volume_force(&f, dt, RHO, MU, Some(&src), (i0, i1, i0, i1, i0, i1));
|
||||||
@@ -226,14 +265,21 @@ fn norm(a: [f64; 3]) -> f64 {
|
|||||||
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
|
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ladder(resolutions: &[usize]) {
|
/// The velocity errors and the two routes' relative force errors per rung.
|
||||||
|
struct Ladder {
|
||||||
|
errors: Vec<f64>,
|
||||||
|
surface: Vec<f64>,
|
||||||
|
cv: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ladder(resolutions: &[usize], scheme: WallScheme) -> Ladder {
|
||||||
let (fe, m) = exact_force_and_flux();
|
let (fe, m) = exact_force_and_flux();
|
||||||
let f_scale = norm(fe);
|
let f_scale = norm(fe);
|
||||||
let fcv = [fe[0] - m[0], fe[1] - m[1], fe[2] - m[2]];
|
let fcv = [fe[0] - m[0], fe[1] - m[1], fe[2] - m[2]];
|
||||||
println!(
|
println!(
|
||||||
" exact force {fe:.5?}; momentum flux {m:.5?}; the control-volume route measures {fcv:.5?}"
|
" {scheme:?}: exact force {fe:.5?}; momentum flux {m:.5?}; the control-volume route measures {fcv:.5?}"
|
||||||
);
|
);
|
||||||
let ms: Vec<Measurement> = resolutions.iter().map(|&n| measure(n)).collect();
|
let ms: Vec<Measurement> = resolutions.iter().map(|&n| measure(n, scheme)).collect();
|
||||||
let errors: Vec<f64> = ms.iter().map(|x| x.l2_velocity).collect();
|
let errors: Vec<f64> = ms.iter().map(|x| x.l2_velocity).collect();
|
||||||
let mut se = Vec::new();
|
let mut se = Vec::new();
|
||||||
let mut ce = Vec::new();
|
let mut ce = Vec::new();
|
||||||
@@ -287,15 +333,57 @@ fn ladder(resolutions: &[usize]) {
|
|||||||
ce.windows(2).all(|w| w[1] < w[0]),
|
ce.windows(2).all(|w| w[1] < w[0]),
|
||||||
"control-volume-route error not falling {ce:?}"
|
"control-volume-route error not falling {ce:?}"
|
||||||
);
|
);
|
||||||
|
Ladder {
|
||||||
|
errors,
|
||||||
|
surface: se,
|
||||||
|
cv: ce,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Item 10's comparison: the cut wall's velocity error at most the binary
|
||||||
|
/// wall's at every rung; both routes within `load_bound` at the finest.
|
||||||
|
fn compare(resolutions: &[usize], load_bound: f64) {
|
||||||
|
let ghost = ladder(resolutions, WallScheme::GhostBinary);
|
||||||
|
let cut = ladder(resolutions, WallScheme::CutCell);
|
||||||
|
for (k, &n) in resolutions.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
" n = {n:3} L2 u ghost {:.4e} cut {:.4e} (ratio {:.3})",
|
||||||
|
ghost.errors[k],
|
||||||
|
cut.errors[k],
|
||||||
|
cut.errors[k] / ghost.errors[k]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cut.errors[k] <= ghost.errors[k],
|
||||||
|
"cut-cell error above the binary wall's at n = {n}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let last = resolutions.len() - 1;
|
||||||
|
assert!(
|
||||||
|
cut.surface[last] < load_bound && cut.cv[last] < load_bound,
|
||||||
|
"cut-cell loads at the finest rung: surface {:.3e}, control volume {:.3e} (bound {load_bound})",
|
||||||
|
cut.surface[last],
|
||||||
|
cut.cv[last]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn embedded_sphere_recovers_the_manufactured_solution() {
|
fn embedded_sphere_recovers_the_manufactured_solution() {
|
||||||
ladder(&[12, 24]);
|
ladder(&[12, 24], WallScheme::GhostBinary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cut_cell_wall_recovers_the_manufactured_solution() {
|
||||||
|
compare(&[12, 24], 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "the three-rung ladder to n = 48 (minutes on the host)"]
|
#[ignore = "the three-rung ladder to n = 48 (minutes on the host)"]
|
||||||
fn embedded_sphere_three_rungs() {
|
fn embedded_sphere_three_rungs() {
|
||||||
ladder(&[12, 24, 48]);
|
ladder(&[12, 24, 48], WallScheme::GhostBinary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "item 10's finest rung: the cut wall's loads within 10 % at n = 48"]
|
||||||
|
fn cut_cell_three_rungs() {
|
||||||
|
compare(&[12, 24, 48], 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user