Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_cylinder_identity.rs
T
Omar SobhandClaude Fable 5.1 d337afa8f9
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
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)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 15:14:40 -05:00

352 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! embedded3 gate 9b: TurekHron 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()
);
}
}
}