rtx-cfd embedded3 item 9: wall.rs (binary ghost mask: three face families, trilinear stencil with periodic-z wrap, z-weighted least-squares ghost fit, flux compatibility correction; slip walls allowed as touched sides) + loads.rs (surface-stress route with probes, control-volume route with full-span z faces skipped); the step wired (body/mask, predicates, anchor, ghost re-imposition). Gates HELD: CFD1 ny 41 nz 1 CV 15.6156 / surface 15.7126 both to 1e-6 of the 2D record; nz 4 periodic CV 1.1e-6 / surface 4.2e-4; sphere MMS order 0.89, div 1e-8, ghost correction 1.0e-4 → 2.0e-5, both load routes' errors falling (0.153 → 0.115 surface, 0.214 → 0.139 CV)
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 / Build CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 4s
CI / Format Check (push) Failing after 12s
CI / Build (ubuntu-latest) (push) Failing after 1m57s
CI / Clippy Check (push) Failing after 2m20s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m50s
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 / Build CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 4s
CI / Format Check (push) Failing after 12s
CI / Build (ubuntu-latest) (push) Failing after 1m57s
CI / Clippy Check (push) Failing after 2m20s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m50s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d4ffac9ac7
commit
d337afa8f9
@@ -0,0 +1,351 @@
|
||||
//! embedded3 gate 9b: Turek–Hron CFD1 (the cylinder with the rigid flag,
|
||||
//! Re 20) on the 3D solver at ny = 41 — the 2D geometry extruded, at nz = 1
|
||||
//! (dz = 1, z slip) and nz = 4 periodic: the settled control-volume drag
|
||||
//! 15.6156 and surface drag 15.7126 of the 2D embedded record to
|
||||
//! `rel < 5e-4` (printed-digit identity across the regimes).
|
||||
|
||||
use rtx_cfd::solvers::incompressible::embedded3::{
|
||||
Body, Boundaries, Field, Fluid, Grid, Parameters, Side, Solver,
|
||||
};
|
||||
use rtx_cfd::solvers::incompressible::{EmbeddedBody, MgSmoother};
|
||||
|
||||
const L: f64 = 2.5;
|
||||
const H: f64 = 0.41;
|
||||
const RHO: f64 = 1000.0;
|
||||
const NU: f64 = 1e-3;
|
||||
const U_MEAN: f64 = 0.2;
|
||||
const SOR_DRAG_CV: f64 = 15.6156;
|
||||
const SOR_DRAG_SURFACE: f64 = 15.7126;
|
||||
|
||||
fn inflow(y: f64) -> f64 {
|
||||
1.5 * U_MEAN * y * (H - y) / (0.5 * H).powi(2)
|
||||
}
|
||||
|
||||
fn body2() -> EmbeddedBody {
|
||||
EmbeddedBody::union(
|
||||
EmbeddedBody::circle(0.2, 0.2, 0.05),
|
||||
EmbeddedBody::rectangle(0.20, 0.19, 0.6, 0.21),
|
||||
)
|
||||
}
|
||||
|
||||
fn run(ny: usize, nz: usize, dz: f64, periodic: bool) -> (f64, f64, usize, usize) {
|
||||
let h = H / ny as f64;
|
||||
let nx = (L / h).round() as usize;
|
||||
let mu = RHO * NU;
|
||||
let u_peak = 1.5 * 1.5 * U_MEAN;
|
||||
let dt = 0.25 / (2.0 * u_peak / h + 4.0 * NU / (h * h));
|
||||
let z = if periodic {
|
||||
Side::Periodic
|
||||
} else {
|
||||
Side::SlipWall
|
||||
};
|
||||
let mut solver = Solver::new(
|
||||
Fluid {
|
||||
density: RHO,
|
||||
viscosity: mu,
|
||||
reference_velocity: U_MEAN,
|
||||
reference_length: 0.1,
|
||||
},
|
||||
Parameters {
|
||||
corrector_steps: 2,
|
||||
tolerance: 1e-7,
|
||||
boundaries: Boundaries {
|
||||
x1: Side::PressureOutlet,
|
||||
z0: z,
|
||||
z1: z,
|
||||
..Boundaries::default()
|
||||
},
|
||||
poisson_smoother: MgSmoother::Lexicographic,
|
||||
..Parameters::default()
|
||||
},
|
||||
);
|
||||
solver.set_boundary_velocity(|x, y, _z, _t| {
|
||||
if x <= 0.0 {
|
||||
(inflow(y), 0.0, 0.0)
|
||||
} else {
|
||||
(0.0, 0.0, 0.0)
|
||||
}
|
||||
});
|
||||
let lz = nz as f64 * dz;
|
||||
solver.set_body(Body::extruded(body2(), lz));
|
||||
let g = Grid {
|
||||
nx,
|
||||
ny,
|
||||
nz,
|
||||
dx: h,
|
||||
dy: h,
|
||||
dz,
|
||||
};
|
||||
let mut f = Field::new(g);
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
let u0 = inflow((j as f64 + 0.5) * h);
|
||||
for i in 0..=nx {
|
||||
f.u[g.uface(k, j, i)] = u0;
|
||||
}
|
||||
}
|
||||
}
|
||||
solver.initialize(&mut f);
|
||||
let cv = (
|
||||
(0.10 / h).round() as usize,
|
||||
(0.75 / h).round() as usize,
|
||||
(0.05 / h).round() as usize,
|
||||
(0.36 / h).round() as usize,
|
||||
0,
|
||||
nz,
|
||||
);
|
||||
let flow_through = L / U_MEAN;
|
||||
let min_steps = (flow_through / dt).ceil() as usize;
|
||||
let mut history: Vec<f64> = Vec::new();
|
||||
let mut steps = 0;
|
||||
loop {
|
||||
solver.advance(&mut f, dt);
|
||||
steps += 1;
|
||||
if steps % 50 == 0 {
|
||||
let fx = solver
|
||||
.mask()
|
||||
.unwrap()
|
||||
.control_volume_force(&f, dt, RHO, mu, None, cv)[0]
|
||||
/ lz;
|
||||
history.push(fx);
|
||||
let umax = f.u.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
|
||||
assert!(umax.is_finite(), "non-finite at step {steps}");
|
||||
if steps >= min_steps && history.len() > 4 {
|
||||
let now = history[history.len() - 1];
|
||||
let then = history[history.len() - 5];
|
||||
if ((now - then) / now).abs() < 1e-4 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(steps < 400_000, "did not settle");
|
||||
}
|
||||
let mask = solver.mask().unwrap();
|
||||
let surface = mask.surface_force(solver.body().unwrap(), &f, mu, solver.time(), 0.5 * h);
|
||||
let drag_cv = mask.control_volume_force(&f, dt, RHO, mu, None, cv)[0] / lz;
|
||||
(drag_cv, surface.f[0] / lz, surface.skipped, steps)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cfd1_at_ny_41_reproduces_the_two_d_record() {
|
||||
let ny = 41;
|
||||
let h = H / ny as f64;
|
||||
for (nz, dz, periodic) in [(1usize, 1.0, false), (4, h, true)] {
|
||||
let (cv, surface, skipped, steps) = run(ny, nz, dz, periodic);
|
||||
let rel_cv = ((cv - SOR_DRAG_CV) / SOR_DRAG_CV).abs();
|
||||
let rel_s = ((surface - SOR_DRAG_SURFACE) / SOR_DRAG_SURFACE).abs();
|
||||
println!(
|
||||
" ny 41 nz {nz} periodic {periodic}: {steps} steps; CV drag {cv:.4} (record 15.6156, rel {rel_cv:.2e}); surface drag {surface:.4} (record 15.7126, rel {rel_s:.2e}, skipped {skipped})"
|
||||
);
|
||||
assert!(rel_cv < 5e-4, "CV drag {cv:.4} vs the record 15.6156");
|
||||
assert!(
|
||||
rel_s < 5e-4,
|
||||
"surface drag {surface:.4} vs the record 15.7126"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagnostic: which probes fail on the skipped surface samples, and the
|
||||
/// surface force per z level, at nz 4 periodic after 200 steps.
|
||||
#[test]
|
||||
#[ignore = "diagnostic: skipped surface samples and per-level force on CFD1 at nz 4"]
|
||||
fn skipped_samples_diagnostic() {
|
||||
let ny = 41;
|
||||
let h = H / ny as f64;
|
||||
let nx = (L / h).round() as usize;
|
||||
let mu = RHO * NU;
|
||||
let u_peak = 1.5 * 1.5 * U_MEAN;
|
||||
let dt = 0.25 / (2.0 * u_peak / h + 4.0 * NU / (h * h));
|
||||
let (nz, dz) = (4usize, h);
|
||||
let lz = nz as f64 * dz;
|
||||
let mut solver = Solver::new(
|
||||
Fluid {
|
||||
density: RHO,
|
||||
viscosity: mu,
|
||||
reference_velocity: U_MEAN,
|
||||
reference_length: 0.1,
|
||||
},
|
||||
Parameters {
|
||||
corrector_steps: 2,
|
||||
tolerance: 1e-7,
|
||||
boundaries: Boundaries {
|
||||
x1: Side::PressureOutlet,
|
||||
z0: Side::Periodic,
|
||||
z1: Side::Periodic,
|
||||
..Boundaries::default()
|
||||
},
|
||||
poisson_smoother: MgSmoother::Lexicographic,
|
||||
..Parameters::default()
|
||||
},
|
||||
);
|
||||
solver.set_boundary_velocity(|x, y, _z, _t| {
|
||||
if x <= 0.0 {
|
||||
(inflow(y), 0.0, 0.0)
|
||||
} else {
|
||||
(0.0, 0.0, 0.0)
|
||||
}
|
||||
});
|
||||
solver.set_body(Body::extruded(body2(), lz));
|
||||
let g = Grid {
|
||||
nx,
|
||||
ny,
|
||||
nz,
|
||||
dx: h,
|
||||
dy: h,
|
||||
dz,
|
||||
};
|
||||
let mut f = Field::new(g);
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
let u0 = inflow((j as f64 + 0.5) * h);
|
||||
for i in 0..=nx {
|
||||
f.u[g.uface(k, j, i)] = u0;
|
||||
}
|
||||
}
|
||||
}
|
||||
solver.initialize(&mut f);
|
||||
for _ in 0..200 {
|
||||
solver.advance(&mut f, dt);
|
||||
}
|
||||
let mask = solver.mask().unwrap();
|
||||
let body = solver.body().unwrap();
|
||||
let samples = body.surface_samples(0.5 * h);
|
||||
let mut by_z: std::collections::BTreeMap<i64, (usize, usize, f64)> =
|
||||
std::collections::BTreeMap::new();
|
||||
let mut shown = 0;
|
||||
for s in &samples {
|
||||
let n = [s.nx, s.ny, s.nz];
|
||||
let key = (s.z * 1e4).round() as i64;
|
||||
let e = by_z.entry(key).or_insert((0, 0, 0.0));
|
||||
e.0 += 1;
|
||||
match mask.traction_at(body, &f, mu, solver.time(), [s.x, s.y, s.z], n) {
|
||||
Some(tr) => e.2 += tr[0] * s.area,
|
||||
None => {
|
||||
e.1 += 1;
|
||||
if shown < 6 {
|
||||
shown += 1;
|
||||
let at = |d: f64| [s.x + d * n[0], s.y + d * n[1], s.z + d * n[2]];
|
||||
let (x1, x2) = (at(h), at(2.0 * h));
|
||||
println!(
|
||||
" skipped ({:.4}, {:.4}, {:.4}) n ({:.2}, {:.2}): p1 {} p2 {} u1 {} u2 {}",
|
||||
s.x,
|
||||
s.y,
|
||||
s.z,
|
||||
s.nx,
|
||||
s.ny,
|
||||
mask.pressure_at(&f.p, x1[0], x1[1], x1[2]).is_some(),
|
||||
mask.pressure_at(&f.p, x2[0], x2[1], x2[2]).is_some(),
|
||||
mask.velocity_at(body, &f, x1[0], x1[1], x1[2], 0.0)
|
||||
.is_some(),
|
||||
mask.velocity_at(body, &f, x2[0], x2[1], x2[2], 0.0)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (z, (n, sk, fx)) in &by_z {
|
||||
println!(
|
||||
" z {:.4}: {n} samples, {sk} skipped, drag contribution per unit depth {:.4}",
|
||||
*z as f64 / 1e4,
|
||||
fx / (lz / by_z.len() as f64)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagnostic: is the periodic nz 4 solution z-invariant, and does its
|
||||
/// plane 0 equal the nz 1 solution, after 200 steps from the same start?
|
||||
#[test]
|
||||
#[ignore = "diagnostic: plane symmetry of CFD1 at nz 4 periodic"]
|
||||
fn plane_symmetry_diagnostic() {
|
||||
let ny = 41;
|
||||
let h = H / ny as f64;
|
||||
let nx = (L / h).round() as usize;
|
||||
let mu = RHO * NU;
|
||||
let u_peak = 1.5 * 1.5 * U_MEAN;
|
||||
let dt = 0.25 / (2.0 * u_peak / h + 4.0 * NU / (h * h));
|
||||
let mk = |nz: usize, dz: f64, z: Side| {
|
||||
let mut s = Solver::new(
|
||||
Fluid {
|
||||
density: RHO,
|
||||
viscosity: mu,
|
||||
reference_velocity: U_MEAN,
|
||||
reference_length: 0.1,
|
||||
},
|
||||
Parameters {
|
||||
corrector_steps: 2,
|
||||
tolerance: 1e-7,
|
||||
boundaries: Boundaries {
|
||||
x1: Side::PressureOutlet,
|
||||
z0: z,
|
||||
z1: z,
|
||||
..Boundaries::default()
|
||||
},
|
||||
poisson_smoother: MgSmoother::Lexicographic,
|
||||
..Parameters::default()
|
||||
},
|
||||
);
|
||||
s.set_boundary_velocity(|x, y, _z, _t| {
|
||||
if x <= 0.0 {
|
||||
(inflow(y), 0.0, 0.0)
|
||||
} else {
|
||||
(0.0, 0.0, 0.0)
|
||||
}
|
||||
});
|
||||
s.set_body(Body::extruded(body2(), nz as f64 * dz));
|
||||
let g = Grid {
|
||||
nx,
|
||||
ny,
|
||||
nz,
|
||||
dx: h,
|
||||
dy: h,
|
||||
dz,
|
||||
};
|
||||
let mut f = Field::new(g);
|
||||
for k in 0..nz {
|
||||
for j in 0..ny {
|
||||
let u0 = inflow((j as f64 + 0.5) * h);
|
||||
for i in 0..=nx {
|
||||
f.u[g.uface(k, j, i)] = u0;
|
||||
}
|
||||
}
|
||||
}
|
||||
s.initialize(&mut f);
|
||||
(s, f, g)
|
||||
};
|
||||
let (mut s1, mut f1, g1) = mk(1, 1.0, Side::SlipWall);
|
||||
let (mut s4, mut f4, g4) = mk(4, h, Side::Periodic);
|
||||
println!(
|
||||
" ghost faces: nz 1 {} / nz 4 {} (per plane {})",
|
||||
s1.mask().unwrap().ghost_faces(),
|
||||
s4.mask().unwrap().ghost_faces(),
|
||||
s4.mask().unwrap().ghost_faces() / 4
|
||||
);
|
||||
for step in 1..=200 {
|
||||
s1.advance(&mut f1, dt);
|
||||
s4.advance(&mut f4, dt);
|
||||
if [1, 2, 10, 50, 200].contains(&step) {
|
||||
let plane = |f: &Field, g: &Grid, k: usize| {
|
||||
f.u[k * g.ny * (g.nx + 1)..(k + 1) * g.ny * (g.nx + 1)].to_vec()
|
||||
};
|
||||
let p0 = plane(&f4, &g4, 0);
|
||||
let mut zinv = 0.0_f64;
|
||||
for k in 1..4 {
|
||||
for (a, b) in plane(&f4, &g4, k).iter().zip(&p0) {
|
||||
zinv = zinv.max((a - b).abs());
|
||||
}
|
||||
}
|
||||
let p1 = plane(&f1, &g1, 0);
|
||||
let vs1 = p0
|
||||
.iter()
|
||||
.zip(&p1)
|
||||
.fold(0.0_f64, |m, (a, b)| m.max((a - b).abs()));
|
||||
let wmax = f4.w.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
|
||||
println!(
|
||||
" step {step}: nz 4 planes within {zinv:.3e}; plane 0 vs nz 1 {vs1:.3e}; max |w| {wmax:.3e}; ghost corr nz1 {:.3e} / nz4 {:.3e}",
|
||||
s1.ghost_correction(),
|
||||
s4.ghost_correction()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
//! embedded3 gate 9a: the manufactured solution with an embedded sphere
|
||||
//! (centre (0.6, 0.45, 0.5), r 0.2, off-centre so the exact force is not
|
||||
//! zero by symmetry) carrying the exact field as its surface velocity, on
|
||||
//! the binary ghost wall. The velocity error falls at the scheme's order,
|
||||
//! every fluid cell is divergence-free, the compatibility correction
|
||||
//! shrinks, and both load routes 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).
|
||||
|
||||
use rtx_cfd::solvers::incompressible::ConvectionScheme;
|
||||
use rtx_cfd::solvers::incompressible::embedded3::{Body, Field, Fluid, Grid, Parameters, Solver};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const RHO: f64 = 1.0;
|
||||
const MU: f64 = 0.05;
|
||||
const C: (f64, f64, f64) = (0.6, 0.45, 0.5);
|
||||
const R: f64 = 0.2;
|
||||
|
||||
fn u3(x: f64, y: f64, z: f64) -> f64 {
|
||||
(PI * x).sin() * (PI * y).cos() * (PI * z).cos()
|
||||
}
|
||||
fn v3(x: f64, y: f64, z: f64) -> f64 {
|
||||
(PI * x).cos() * (PI * y).sin() * (PI * z).cos()
|
||||
}
|
||||
fn w3(x: f64, y: f64, z: f64) -> f64 {
|
||||
-2.0 * (PI * x).cos() * (PI * y).cos() * (PI * z).sin()
|
||||
}
|
||||
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.
|
||||
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],
|
||||
)
|
||||
}
|
||||
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],
|
||||
)
|
||||
}
|
||||
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.
|
||||
fn exact_force_and_flux() -> ([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)
|
||||
}
|
||||
|
||||
struct Measurement {
|
||||
l2_velocity: f64,
|
||||
max_div: f64,
|
||||
ghost_correction: f64,
|
||||
force_surface: [f64; 3],
|
||||
skipped: usize,
|
||||
force_cv: [f64; 3],
|
||||
}
|
||||
|
||||
fn measure(n: usize) -> 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,
|
||||
convection_scheme: ConvectionScheme::Upwind,
|
||||
..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(|_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);
|
||||
for _ in 0..200_000 {
|
||||
let (bu, bv, bw) = (f.u.clone(), f.v.clone(), f.w.clone());
|
||||
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");
|
||||
use rtx_cfd::solvers::incompressible::embedded3::FaceKind;
|
||||
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 mut max_div = 0.0_f64;
|
||||
for k in 0..n {
|
||||
for j in 0..n {
|
||||
for i in 0..n {
|
||||
if mask.is_fluid_cell(g.cell(k, j, i)) {
|
||||
let div = (f.u[g.uface(k, j, i + 1)] - f.u[g.uface(k, j, i)]) / h
|
||||
+ (f.v[g.vface(k, j + 1, i)] - f.v[g.vface(k, j, i)]) / h
|
||||
+ (f.w[g.wface(k + 1, j, i)] - f.w[g.wface(k, j, i)]) / h;
|
||||
max_div = max_div.max(div.abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let body = solver.body().expect("body");
|
||||
let surface = mask.surface_force(body, &f, MU, solver.time(), 0.5 * h);
|
||||
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));
|
||||
Measurement {
|
||||
l2_velocity: (sq / vol).sqrt(),
|
||||
max_div,
|
||||
ghost_correction: solver.ghost_correction().abs(),
|
||||
force_surface: surface.f,
|
||||
skipped: surface.skipped,
|
||||
force_cv,
|
||||
}
|
||||
}
|
||||
|
||||
fn norm(a: [f64; 3]) -> f64 {
|
||||
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
|
||||
}
|
||||
|
||||
fn ladder(resolutions: &[usize]) {
|
||||
let (fe, m) = exact_force_and_flux();
|
||||
let f_scale = norm(fe);
|
||||
let fcv = [fe[0] - m[0], fe[1] - m[1], fe[2] - m[2]];
|
||||
println!(
|
||||
" 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 errors: Vec<f64> = ms.iter().map(|x| x.l2_velocity).collect();
|
||||
let mut se = Vec::new();
|
||||
let mut ce = Vec::new();
|
||||
for (k, (mm, &n)) in ms.iter().zip(resolutions).enumerate() {
|
||||
let rate = if k == 0 {
|
||||
" -".to_string()
|
||||
} else {
|
||||
format!("{:5.2}", (errors[k - 1] / errors[k]).log2())
|
||||
};
|
||||
let s = norm([
|
||||
mm.force_surface[0] - fe[0],
|
||||
mm.force_surface[1] - fe[1],
|
||||
mm.force_surface[2] - fe[2],
|
||||
]) / f_scale;
|
||||
let c = norm([
|
||||
mm.force_cv[0] - fcv[0],
|
||||
mm.force_cv[1] - fcv[1],
|
||||
mm.force_cv[2] - fcv[2],
|
||||
]) / f_scale;
|
||||
println!(
|
||||
" n = {n:3} L2 u {:.4e} (order {rate}) max div {:.2e} ghost corr {:.2e} F_surface {:.4?} rel {s:.3e} (skipped {}) F_cv {:.4?} rel {c:.3e}",
|
||||
mm.l2_velocity,
|
||||
mm.max_div,
|
||||
mm.ghost_correction,
|
||||
mm.force_surface,
|
||||
mm.skipped,
|
||||
mm.force_cv
|
||||
);
|
||||
se.push(s);
|
||||
ce.push(c);
|
||||
}
|
||||
assert!(
|
||||
errors.windows(2).all(|w| w[1] < w[0]),
|
||||
"errors not monotone {errors:?}"
|
||||
);
|
||||
for w in errors.windows(2) {
|
||||
let rate = (w[0] / w[1]).log2();
|
||||
assert!(
|
||||
rate > 0.75 && rate < 2.3,
|
||||
"order {rate:.3} outside [0.75, 2.3]"
|
||||
);
|
||||
}
|
||||
for mm in &ms {
|
||||
assert!(mm.max_div < 1e-5, "max div {:.3e}", mm.max_div);
|
||||
}
|
||||
assert!(
|
||||
se.windows(2).all(|w| w[1] < w[0]),
|
||||
"surface-route error not falling {se:?}"
|
||||
);
|
||||
assert!(
|
||||
ce.windows(2).all(|w| w[1] < w[0]),
|
||||
"control-volume-route error not falling {ce:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_sphere_recovers_the_manufactured_solution() {
|
||||
ladder(&[12, 24]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "the three-rung ladder to n = 48 (minutes on the host)"]
|
||||
fn embedded_sphere_three_rungs() {
|
||||
ladder(&[12, 24, 48]);
|
||||
}
|
||||
Reference in New Issue
Block a user