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 / Format Check (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
CI / Clippy Check (push) Failing after 4s
CI / Build (ubuntu-latest) (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 7s
CI / Build CPU-Only (Explicit) (push) Failing after 58s
Documentation / Build API Documentation (push) Failing after 1m0s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
568 lines
22 KiB
Rust
568 lines
22 KiB
Rust
//! The embedded-sphere manufactured solution shared by the embedded3
|
||
//! wall gates (items 9–11): the fields, the source, the boundary data,
|
||
//! the exact surface integrals, and one steady march measured.
|
||
|
||
use rtx_cfd::solvers::incompressible::ConvectionScheme;
|
||
use rtx_cfd::solvers::incompressible::embedded3::{
|
||
Body, FaceKind, Field, Fluid, Grid, Parameters, Solver, WallScheme,
|
||
};
|
||
use std::f64::consts::PI;
|
||
|
||
pub const RHO: f64 = 1.0;
|
||
pub const MU: f64 = 0.05;
|
||
/// The sphere's default centre (off-centre so the exact force is not zero by symmetry).
|
||
pub const C: (f64, f64, f64) = (0.6, 0.45, 0.5);
|
||
pub const R: f64 = 0.2;
|
||
|
||
pub fn u3(x: f64, y: f64, z: f64) -> f64 {
|
||
(PI * x).sin() * (PI * y).cos() * (PI * z).cos()
|
||
}
|
||
pub fn v3(x: f64, y: f64, z: f64) -> f64 {
|
||
(PI * x).cos() * (PI * y).sin() * (PI * z).cos()
|
||
}
|
||
pub fn w3(x: f64, y: f64, z: f64) -> f64 {
|
||
-2.0 * (PI * x).cos() * (PI * y).cos() * (PI * z).sin()
|
||
}
|
||
pub fn p3(x: f64, y: f64, z: f64) -> f64 {
|
||
(PI * x).sin() * (PI * y).sin() * (PI * z).sin()
|
||
}
|
||
/// The velocity gradient ∂u_i/∂x_j and the pressure gradient.
|
||
pub fn grads(x: f64, y: f64, z: f64) -> ([[f64; 3]; 3], [f64; 3]) {
|
||
let (sx, cx) = (PI * x).sin_cos();
|
||
let (sy, cy) = (PI * y).sin_cos();
|
||
let (sz, cz) = (PI * z).sin_cos();
|
||
(
|
||
[
|
||
[PI * cx * cy * cz, -PI * sx * sy * cz, -PI * sx * cy * sz],
|
||
[-PI * sx * sy * cz, PI * cx * cy * cz, -PI * cx * sy * sz],
|
||
[
|
||
2.0 * PI * sx * cy * sz,
|
||
2.0 * PI * cx * sy * sz,
|
||
-2.0 * PI * cx * cy * cz,
|
||
],
|
||
],
|
||
[PI * cx * sy * sz, PI * sx * cy * sz, PI * sx * sy * cz],
|
||
)
|
||
}
|
||
pub fn source3(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
|
||
let (g, gp) = grads(x, y, z);
|
||
let u = [u3(x, y, z), v3(x, y, z), w3(x, y, z)];
|
||
let lap = -3.0 * PI * PI;
|
||
let conv = |i: usize| u[0] * g[i][0] + u[1] * g[i][1] + u[2] * g[i][2];
|
||
(
|
||
RHO * conv(0) + gp[0] - MU * lap * u[0],
|
||
RHO * conv(1) + gp[1] - MU * lap * u[1],
|
||
RHO * conv(2) + gp[2] - MU * lap * u[2],
|
||
)
|
||
}
|
||
pub fn boundary3(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
|
||
let u = if x <= 0.0 || x >= 1.0 {
|
||
0.0
|
||
} else {
|
||
u3(x, y, z)
|
||
};
|
||
let v = if y <= 0.0 || y >= 1.0 {
|
||
0.0
|
||
} else {
|
||
v3(x, y, z)
|
||
};
|
||
let w = if z <= 0.0 || z >= 1.0 {
|
||
0.0
|
||
} else {
|
||
w3(x, y, z)
|
||
};
|
||
(u, v, w)
|
||
}
|
||
|
||
/// Exact force `∮ (−p I + μ(∇u + ∇uᵀ)) n dA` and momentum flux `∮ ρ u (u·n) dA`
|
||
/// over the sphere by a fine Fibonacci quadrature.
|
||
pub fn exact_force_and_flux(c: (f64, f64, f64)) -> ([f64; 3], [f64; 3]) {
|
||
let n = 200_000;
|
||
let golden = PI * (3.0 - 5.0_f64.sqrt());
|
||
let (mut f, mut m) = ([0.0; 3], [0.0; 3]);
|
||
let da = 4.0 * PI * R * R / n as f64;
|
||
for k in 0..n {
|
||
let zz = 1.0 - 2.0 * (k as f64 + 0.5) / n as f64;
|
||
let rr = (1.0 - zz * zz).sqrt();
|
||
let th = golden * k as f64;
|
||
let nrm = [rr * th.cos(), rr * th.sin(), zz];
|
||
let (x, y, z) = (c.0 + R * nrm[0], c.1 + R * nrm[1], c.2 + R * nrm[2]);
|
||
let (g, _) = grads(x, y, z);
|
||
let p = p3(x, y, z);
|
||
let u = [u3(x, y, z), v3(x, y, z), w3(x, y, z)];
|
||
let un = u[0] * nrm[0] + u[1] * nrm[1] + u[2] * nrm[2];
|
||
for i in 0..3 {
|
||
let mut t = -p * nrm[i];
|
||
for j in 0..3 {
|
||
t += MU * (g[i][j] + g[j][i]) * nrm[j];
|
||
}
|
||
f[i] += t * da;
|
||
m[i] += RHO * u[i] * un * da;
|
||
}
|
||
}
|
||
(f, m)
|
||
}
|
||
|
||
pub struct Measurement {
|
||
pub l2_velocity: f64,
|
||
pub max_div: f64,
|
||
pub ghost_correction: f64,
|
||
/// The scheme's wall route (the traction sampler on the binary wall,
|
||
/// the operator route on the cut wall).
|
||
pub force_surface: [f64; 3],
|
||
pub skipped: usize,
|
||
pub force_cv: [f64; 3],
|
||
/// The traction sampler on the cut wall (S2-1); the wall route again
|
||
/// on the binary wall.
|
||
pub force_sampler: [f64; 3],
|
||
}
|
||
|
||
/// March the manufactured solution with the sphere at `c` to steady state on grid `n`.
|
||
pub fn measure(n: usize, scheme: WallScheme, c: (f64, f64, f64)) -> Measurement {
|
||
let h = 1.0 / n as f64;
|
||
let dt = 0.4 * (h * h / (4.0 * MU / RHO)).min(h);
|
||
let mut solver = Solver::new(
|
||
Fluid {
|
||
density: RHO,
|
||
viscosity: MU,
|
||
reference_velocity: 1.0,
|
||
reference_length: 1.0,
|
||
},
|
||
Parameters {
|
||
corrector_steps: 2,
|
||
tolerance: 1e-8,
|
||
// S2-7b: `RTX_E3_MMS_SCHEME=tvd` for a second-order interior.
|
||
convection_scheme: if std::env::var("RTX_E3_MMS_SCHEME").is_ok_and(|v| v == "tvd") {
|
||
ConvectionScheme::TvdVanAlbada
|
||
} else {
|
||
ConvectionScheme::Upwind
|
||
},
|
||
wall_scheme: scheme,
|
||
momentum_volume_cell_mean: std::env::var("RTX_E3_CELL_MEAN").is_ok_and(|v| v == "1"),
|
||
..Parameters::default()
|
||
},
|
||
);
|
||
solver.set_momentum_source(|x, y, z, _t| source3(x, y, z));
|
||
solver.set_boundary_velocity(|x, y, z, _t| boundary3(x, y, z));
|
||
solver.set_body(
|
||
Body::sphere(move |_t| c, R)
|
||
.with_surface_velocity(|x, y, z, _t| (u3(x, y, z), v3(x, y, z), w3(x, y, z))),
|
||
);
|
||
let g = Grid::cubic(n, n, n, h);
|
||
let mut f = Field::new(g);
|
||
solver.initialize(&mut f);
|
||
let mut last = solver.advance(&mut f, dt);
|
||
for _ in 0..200_000 {
|
||
let (bu, bv, bw) = (f.u.clone(), f.v.clone(), f.w.clone());
|
||
last = solver.advance(&mut f, dt);
|
||
let mut change = 0.0_f64;
|
||
for (a, b) in
|
||
f.u.iter()
|
||
.zip(&bu)
|
||
.chain(f.v.iter().zip(&bv))
|
||
.chain(f.w.iter().zip(&bw))
|
||
{
|
||
change = change.max((a - b).abs());
|
||
}
|
||
if change / dt < 1e-6 {
|
||
break;
|
||
}
|
||
}
|
||
let mask = solver.mask().expect("mask");
|
||
let (mut sq, mut vol) = (0.0, 0.0);
|
||
let dv = h * h * h;
|
||
for k in 0..n {
|
||
for j in 0..n {
|
||
for i in 1..n {
|
||
if mask.u_kind(g.uface(k, j, i)) == FaceKind::Fluid {
|
||
let e = f.u[g.uface(k, j, i)]
|
||
- u3(i as f64 * h, (j as f64 + 0.5) * h, (k as f64 + 0.5) * h);
|
||
sq += e * e * dv;
|
||
vol += dv;
|
||
}
|
||
}
|
||
}
|
||
for j in 1..n {
|
||
for i in 0..n {
|
||
if mask.v_kind(g.vface(k, j, i)) == FaceKind::Fluid {
|
||
let e = f.v[g.vface(k, j, i)]
|
||
- v3((i as f64 + 0.5) * h, j as f64 * h, (k as f64 + 0.5) * h);
|
||
sq += e * e * dv;
|
||
vol += dv;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for k in 1..n {
|
||
for j in 0..n {
|
||
for i in 0..n {
|
||
if mask.w_kind(g.wface(k, j, i)) == FaceKind::Fluid {
|
||
let e = f.w[g.wface(k, j, i)]
|
||
- w3((i as f64 + 0.5) * h, (j as f64 + 0.5) * h, k as f64 * h);
|
||
sq += e * e * dv;
|
||
vol += dv;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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);
|
||
// a virtually merged small cell's flux counts with its master's (only
|
||
// the pair's continuity holds).
|
||
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);
|
||
let mut cell_flux = vec![0.0; g.cells()];
|
||
for k in 0..n {
|
||
for j in 0..n {
|
||
for i in 0..n {
|
||
let idx = g.cell(k, j, i);
|
||
if mask.is_fluid_cell(idx) {
|
||
let flux = (mask.a_u(g.uface(k, j, i + 1)) * f.u[g.uface(k, j, i + 1)]
|
||
- mask.a_u(g.uface(k, j, i)) * f.u[g.uface(k, j, i)])
|
||
* 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];
|
||
cell_flux[mask.master(idx).unwrap_or(idx)] += flux;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (idx, &flux) in cell_flux.iter().enumerate() {
|
||
sum_flux += flux.abs();
|
||
if (flux / (h * h * h)).abs() > max_div {
|
||
max_div = (flux / (h * h * h)).abs();
|
||
at_vol = mask.vol(idx);
|
||
}
|
||
}
|
||
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
|
||
);
|
||
// S2-7b: the pressure's error against the manufactured p (mean-free
|
||
// over the full interior cells) in the full cells and in the cut cells,
|
||
// and the six axis wall points' reads by the linear box probe and the
|
||
// quadratic fit (r 2.5 h) — in units of the pressure's scale (1).
|
||
{
|
||
let (mut sum_full, mut n_full) = (0.0, 0usize);
|
||
let exact = |idx: usize| {
|
||
let (k, j, i) = g.kji(idx);
|
||
p3((i as f64 + 0.5) * h, (j as f64 + 0.5) * h, (k as f64 + 0.5) * h)
|
||
};
|
||
let is_cut = |idx: usize| mask.vol(idx) < 1.0 - 1e-9;
|
||
for idx in 0..g.cells() {
|
||
if mask.is_fluid_cell(idx) && !is_cut(idx) && mask.master(idx).is_none() {
|
||
sum_full += f.p[idx] - exact(idx);
|
||
n_full += 1;
|
||
}
|
||
}
|
||
let level = sum_full / n_full.max(1) as f64;
|
||
let (mut sq_full, mut sq_cut, mut n_cut) = (0.0, 0.0, 0usize);
|
||
// By fluid fraction (small < 0.5 ≤ large) and the cut cells' mean
|
||
// error (a level) against their scatter about it.
|
||
let (mut sq_small, mut n_small, mut sq_large, mut n_large, mut sum_cut) = (0.0, 0usize, 0.0, 0usize, 0.0);
|
||
let (mut sum_small, mut sum_large) = (0.0, 0.0);
|
||
for idx in 0..g.cells() {
|
||
if !mask.is_fluid_cell(idx) || mask.master(idx).is_some() {
|
||
continue;
|
||
}
|
||
let e = f.p[idx] - level - exact(idx);
|
||
if is_cut(idx) {
|
||
sq_cut += e * e;
|
||
n_cut += 1;
|
||
sum_cut += e;
|
||
if mask.vol(idx) < 0.5 {
|
||
sq_small += e * e;
|
||
sum_small += e;
|
||
n_small += 1;
|
||
} else {
|
||
sq_large += e * e;
|
||
sum_large += e;
|
||
n_large += 1;
|
||
}
|
||
} else {
|
||
sq_full += e * e;
|
||
}
|
||
}
|
||
let mean_cut = sum_cut / n_cut.max(1) as f64;
|
||
println!(
|
||
" [{scheme:?} n {n}] cut cells' pressure error: mean {mean_cut:+.3e}, scatter about it {:.3e}; fraction < 0.5: rms {:.3e} mean {:+.3e} ({n_small}), ≥ 0.5: rms {:.3e} mean {:+.3e} ({n_large})",
|
||
((sq_cut / n_cut.max(1) as f64) - mean_cut * mean_cut).max(0.0).sqrt(),
|
||
(sq_small / n_small.max(1) as f64).sqrt(),
|
||
sum_small / n_small.max(1) as f64,
|
||
(sq_large / n_large.max(1) as f64).sqrt(),
|
||
sum_large / n_large.max(1) as f64
|
||
);
|
||
// The six axis wall points' signed errors: linear box / quadratic
|
||
// r 2.5 h / quadratic through full cells only (r 2.5 h and 3.5 h).
|
||
let mut wl = String::new();
|
||
for (dx, dy, dz) in [(1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, -1.0)] {
|
||
let (x, y, z) = (c.0 + R * dx, c.1 + R * dy, c.2 + R * dz);
|
||
let pe = p3(x, y, z) + level;
|
||
let f1 = |v: Option<f64>| v.map_or("n/a".to_string(), |v| format!("{:+.4}", v - pe));
|
||
wl += &format!(
|
||
" [{}]",
|
||
[
|
||
f1(mask.pressure_at(&f.p, x, y, z)),
|
||
f1(mask.pressure_at_quadratic(&f.p, x, y, z, 2.5 * h)),
|
||
f1(mask.pressure_at_quadratic_from(&f.p, x, y, z, 2.5 * h, true)),
|
||
f1(mask.pressure_at_quadratic_from(&f.p, x, y, z, 3.5 * h, true)),
|
||
]
|
||
.join(" ")
|
||
);
|
||
}
|
||
println!(" [{scheme:?} n {n}] wall-point signed errors (linear, quad, full-only quad r2.5, r3.5):{wl}");
|
||
let (mut sq_lin, mut sq_quad, mut n_lin, mut n_quad) = (0.0, 0.0, 0usize, 0usize);
|
||
for (dx, dy, dz) in [(1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, -1.0)] {
|
||
let (x, y, z) = (c.0 + R * dx, c.1 + R * dy, c.2 + R * dz);
|
||
let pe = p3(x, y, z);
|
||
if let Some(pl) = mask.pressure_at(&f.p, x, y, z) {
|
||
sq_lin += (pl - level - pe).powi(2);
|
||
n_lin += 1;
|
||
}
|
||
if let Some(pq) = mask.pressure_at_quadratic(&f.p, x, y, z, 2.5 * h) {
|
||
sq_quad += (pq - level - pe).powi(2);
|
||
n_quad += 1;
|
||
}
|
||
}
|
||
println!(
|
||
" [{scheme:?} n {n}] pressure error rms: full cells {:.3e} ({n_full}), cut cells {:.3e} ({n_cut}); wall-point reads rms: linear box {:.3e} ({n_lin}/6), quadratic r2.5h {:.3e} ({n_quad}/6)",
|
||
(sq_full / n_full.max(1) as f64).sqrt(),
|
||
(sq_cut / n_cut.max(1) as f64).sqrt(),
|
||
(sq_lin / n_lin.max(1) as f64).sqrt(),
|
||
(sq_quad / n_quad.max(1) as f64).sqrt()
|
||
);
|
||
}
|
||
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 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_sampler = match scheme {
|
||
WallScheme::GhostBinary => surface.f,
|
||
WallScheme::CutCell => mask
|
||
.cut_wall_force_reconstructed(body, &f, MU, t, None)
|
||
.expect("reconstructed"),
|
||
};
|
||
Measurement {
|
||
l2_velocity: (sq / vol).sqrt(),
|
||
max_div,
|
||
ghost_correction: solver.ghost_correction().abs(),
|
||
force_surface: surface.f,
|
||
skipped: surface.skipped,
|
||
force_cv,
|
||
force_sampler,
|
||
}
|
||
}
|
||
|
||
/// S2-7b: the discrete operator applied to the EXACT manufactured field
|
||
/// valued at the faces' open-part centroids — one predictor, one
|
||
/// corrector; the predictor's acceleration per fluid face (recovered from
|
||
/// the one correction) in units of the source's largest acceleration, by
|
||
/// aperture band, plus the full faces next to cut cells and the interior;
|
||
/// the corrected field's divergence per cut cell over its largest face
|
||
/// flux; the correction's pressure at cut cells over the pressure scale.
|
||
pub fn operator_probe(n: usize, c: (f64, f64, f64)) {
|
||
let h = 1.0 / n as f64;
|
||
let dt = 0.4 * (h * h / (4.0 * MU / RHO)).min(h);
|
||
let mut params = Parameters {
|
||
corrector_steps: 1,
|
||
tolerance: 1e-10,
|
||
convection_scheme: if std::env::var("RTX_E3_MMS_SCHEME").is_ok_and(|v| v == "tvd") {
|
||
ConvectionScheme::TvdVanAlbada
|
||
} else {
|
||
ConvectionScheme::Upwind
|
||
},
|
||
wall_scheme: WallScheme::CutCell,
|
||
..Parameters::default()
|
||
};
|
||
params.momentum_volume_cell_mean = false;
|
||
let mut solver = Solver::new(
|
||
Fluid {
|
||
density: RHO,
|
||
viscosity: MU,
|
||
reference_velocity: 1.0,
|
||
reference_length: 1.0,
|
||
},
|
||
params,
|
||
);
|
||
solver.set_momentum_source(|x, y, z, _t| source3(x, y, z));
|
||
solver.set_boundary_velocity(|x, y, z, _t| boundary3(x, y, z));
|
||
solver.set_body(
|
||
Body::sphere(move |_t| c, R)
|
||
.with_surface_velocity(|x, y, z, _t| (u3(x, y, z), v3(x, y, z), w3(x, y, z))),
|
||
);
|
||
let g = Grid::cubic(n, n, n, h);
|
||
let mut f = Field::new(g);
|
||
solver.initialize(&mut f);
|
||
let tables = solver
|
||
.mask()
|
||
.expect("mask")
|
||
.face_shift_tables()
|
||
.expect("shift tables")
|
||
.clone();
|
||
for k in 0..n {
|
||
for j in 0..n {
|
||
for i in 0..=n {
|
||
let idx = g.uface(k, j, i);
|
||
let t = &tables[0][3 * idx..3 * idx + 3];
|
||
f.u[idx] = u3(i as f64 * h + t[0], (j as f64 + 0.5) * h + t[1], (k as f64 + 0.5) * h + t[2]);
|
||
}
|
||
}
|
||
for j in 0..=n {
|
||
for i in 0..n {
|
||
let idx = g.vface(k, j, i);
|
||
let t = &tables[1][3 * idx..3 * idx + 3];
|
||
f.v[idx] = v3((i as f64 + 0.5) * h + t[0], j as f64 * h + t[1], (k as f64 + 0.5) * h + t[2]);
|
||
}
|
||
}
|
||
}
|
||
for k in 0..=n {
|
||
for j in 0..n {
|
||
for i in 0..n {
|
||
let idx = g.wface(k, j, i);
|
||
let t = &tables[2][3 * idx..3 * idx + 3];
|
||
f.w[idx] = w3((i as f64 + 0.5) * h + t[0], (j as f64 + 0.5) * h + t[1], k as f64 * h + t[2]);
|
||
}
|
||
}
|
||
}
|
||
for idx in 0..g.cells() {
|
||
let (k, j, i) = g.kji(idx);
|
||
f.p[idx] = p3((i as f64 + 0.5) * h, (j as f64 + 0.5) * h, (k as f64 + 0.5) * h);
|
||
}
|
||
{
|
||
let (body, mask) = (solver.body().expect("body"), solver.mask().expect("mask"));
|
||
mask.impose(body, &mut f.u, &mut f.v, &mut f.w, solver.time());
|
||
}
|
||
solver.advance(&mut f, dt);
|
||
let mask = solver.mask().expect("mask");
|
||
let pp = &f.p_prime;
|
||
// The source's largest acceleration (the unit of the read).
|
||
let mut a_max: f64 = 0.0;
|
||
for k in 0..n {
|
||
for j in 0..n {
|
||
for i in 0..n {
|
||
let s = source3((i as f64 + 0.5) * h, (j as f64 + 0.5) * h, (k as f64 + 0.5) * h);
|
||
a_max = a_max.max((s.0 * s.0 + s.1 * s.1 + s.2 * s.2).sqrt() / RHO);
|
||
}
|
||
}
|
||
}
|
||
let is_cut = |idx: usize| mask.vol(idx) < 1.0 - 1e-9;
|
||
// Bands: α < 1/16, 1/16–1/8, 1/8–1/4, ¼–½, ½–¾, ¾–1, full next to a cut
|
||
// cell, interior. Each entry: the acceleration and the FORCE per unit
|
||
// `ρ h A` (the acceleration times the floored fraction max(α, 0.1)).
|
||
let bin_of = |a: f64, near: bool| -> usize {
|
||
if a < 1.0 / 16.0 {
|
||
0
|
||
} else if a < 1.0 / 8.0 {
|
||
1
|
||
} else if a < 0.25 {
|
||
2
|
||
} else if a < 1.0 {
|
||
2 + ((a * 4.0).floor() as usize).min(3)
|
||
} else if near {
|
||
6
|
||
} else {
|
||
7
|
||
}
|
||
};
|
||
let mut acc: [Vec<(f64, f64)>; 8] = Default::default();
|
||
// u faces
|
||
for k in 0..n {
|
||
for j in 0..n {
|
||
for i in 1..n {
|
||
let idx = g.uface(k, j, i);
|
||
if mask.u_kind(idx) != FaceKind::Fluid {
|
||
continue;
|
||
}
|
||
let (cm, cp) = (g.cell(k, j, i - 1), g.cell(k, j, i));
|
||
let star = f.u[idx] + (dt / RHO) * mask.grad_weight(0, idx) * (pp[cp] - pp[cm]) / h;
|
||
let a = (star - f.u_old[idx]) / dt / a_max;
|
||
acc[bin_of(mask.a_u(idx), is_cut(cm) || is_cut(cp))].push((a, a * mask.a_u(idx).max(0.1)));
|
||
}
|
||
}
|
||
}
|
||
for k in 0..n {
|
||
for j in 1..n {
|
||
for i in 0..n {
|
||
let idx = g.vface(k, j, i);
|
||
if mask.v_kind(idx) != FaceKind::Fluid {
|
||
continue;
|
||
}
|
||
let (cm, cp) = (g.cell(k, j - 1, i), g.cell(k, j, i));
|
||
let star = f.v[idx] + (dt / RHO) * mask.grad_weight(1, idx) * (pp[cp] - pp[cm]) / h;
|
||
let a = (star - f.v_old[idx]) / dt / a_max;
|
||
acc[bin_of(mask.a_v(idx), is_cut(cm) || is_cut(cp))].push((a, a * mask.a_v(idx).max(0.1)));
|
||
}
|
||
}
|
||
}
|
||
for k in 1..n {
|
||
for j in 0..n {
|
||
for i in 0..n {
|
||
let idx = g.wface(k, j, i);
|
||
if mask.w_kind(idx) != FaceKind::Fluid {
|
||
continue;
|
||
}
|
||
let (cm, cp) = (g.cell(k - 1, j, i), g.cell(k, j, i));
|
||
let star = f.w[idx] + (dt / RHO) * mask.grad_weight(2, idx) * (pp[cp] - pp[cm]) / h;
|
||
let a = (star - f.w_old[idx]) / dt / a_max;
|
||
acc[bin_of(mask.a_w(idx), is_cut(cm) || is_cut(cp))].push((a, a * mask.a_w(idx).max(0.1)));
|
||
}
|
||
}
|
||
}
|
||
// The correction's pressure at cut cells and interior cells, over the
|
||
// pressure scale (1), mean-free over the interior.
|
||
let (mut sum_int, mut n_int) = (0.0, 0usize);
|
||
for idx in 0..g.cells() {
|
||
if mask.is_fluid_cell(idx) && !is_cut(idx) && mask.master(idx).is_none() {
|
||
sum_int += pp[idx];
|
||
n_int += 1;
|
||
}
|
||
}
|
||
let lvl = sum_int / n_int.max(1) as f64;
|
||
let (mut sq_int, mut sq_cut, mut n_cut) = (0.0, 0.0, 0usize);
|
||
for idx in 0..g.cells() {
|
||
if !mask.is_fluid_cell(idx) || mask.master(idx).is_some() {
|
||
continue;
|
||
}
|
||
let e = pp[idx] - lvl;
|
||
if is_cut(idx) {
|
||
sq_cut += e * e;
|
||
n_cut += 1;
|
||
} else {
|
||
sq_int += e * e;
|
||
}
|
||
}
|
||
let rms = |v: &[f64]| (v.iter().map(|x| x * x).sum::<f64>() / v.len().max(1) as f64).sqrt();
|
||
let names = ["α<1/16", "1/16–1/8", "1/8–1/4", "¼–½", "½–¾", "¾–1", "full next to cut", "interior"];
|
||
let mut line = format!(" probe n {n} (source accel {a_max:.3e}):");
|
||
for (b, name) in names.iter().enumerate() {
|
||
let a: Vec<f64> = acc[b].iter().map(|x| x.0).collect();
|
||
let fo: Vec<f64> = acc[b].iter().map(|x| x.1).collect();
|
||
line += &format!(" {name}: {} acc {:.3e} force {:.3e};", acc[b].len(), rms(&a), rms(&fo));
|
||
}
|
||
line += &format!(
|
||
" p' rms interior {:.3e} cut {:.3e} ({n_cut})",
|
||
(sq_int / n_int.max(1) as f64).sqrt(),
|
||
(sq_cut / n_cut.max(1) as f64).sqrt()
|
||
);
|
||
println!("{line}");
|
||
}
|