Files
rustytorch/crates/specialized/rtx-cfd/tests/patch_cylinder_flag.rs
T
Omar SobhandClaude Fable 5.1 45ff34da8f
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
rtx-cfd: overset P4-0 — the O-grid around the Turek–Hron rigid body (cylinder + flag), gated
patch_gen::{cylinder_flag_outline, cylinder_flag_patch, winslow_smooth, respace_rays}
(+ convex_hull, offset_convex_polygon, nearest_on_polyline): the outline CCW then
reversed to clockwise (tip semicircle 16 cells, junction fillets of a FIXED radius
with 3 cells, straights graded 0.3 h → h, cylinder arc at h); the outer ring the
6 h normal offset of the body's convex hull; initial pairing by the inner point's
normal offset projected onto the hull offset (an arclength-proportional pairing
folded the transfinite grid at the tip: rays crossed where the curvatures differ);
Winslow (TTM) smoothing of the interior with the outer nodes SLIDING along the
hull offset (each re-placed at the nearest point to the extrapolated ray), then
re-spacing along the smoothed rays to the across stretch. Gates
(tests/patch_cylinder_flag.rs): ny = 41/62/82 → 143×12 / 183×12 / 225×12 cells,
positive, wall row 0.23 h (fillet max 0.37 / 0.43 / 0.50 h), worst
non-orthogonality 76.6 / 69.1 / 63.3° at the concave fillets (structural: a
concave arc's normals converge at its centre), classification of the benchmark
background with both donor invariants. P0 MMS on these meshes
(tests/cylinder_flag_mms.rs, exact acceptors, line-implicit): Stokes orders 2.17 /
2.13, upwind 2.00 / 1.86 (cell Péclet ≈ 0.1), divergence ≤ 9e-14 — the fillet skew
costs nothing measurable. Rule: a refinement ladder's geometry must be fixed in
physical units — with fillet = h/2 the Stokes orders read 1.74 → 1.31, the O(h)
boundary perturbation masquerading as a scheme defect; the fillet is a parameter
(5 mm across the ladder). Also: the P3b knock-outs H1/H2 on the balanced default
(no effect), the P3 §5.10 record in the falsifier's header.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
2026-09-05 19:50:14 -07:00

154 lines
5.9 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.
//! A-P4-0 (`docs/overset_metal_campaign.md` §5.11): the O-grid around the
//! TurekHron rigid body (cylinder + flag) — closed, positive, within the
//! non-orthogonality gate, wall row ≤ 0.35 h and along-body spacing ≥ 0.2 h
//! (the dt budget), and classifying the benchmark background at ny = 41 /
//! 62 / 82 with both donor invariants.
use rtx_cfd::CfdResult;
use rtx_cfd::mesh::PatchMesh;
use rtx_cfd::mesh::patch_gen::cylinder_flag_patch;
use rtx_cfd::solvers::incompressible::OverlapMap;
use rtx_cfd::solvers::incompressible::overset::overlap::DEFAULT_OVERLAP_ROWS;
const L: f64 = 2.5;
const H: f64 = 0.41;
fn body_patch(h: f64) -> CfdResult<(PatchMesh, (usize, f64))> {
let sweeps: usize = std::env::var("RTX_CF_SWEEPS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(500);
cylinder_flag_patch(
[0.2, 0.2],
0.05,
0.01,
0.6,
h,
// The junction fillet is a fixed 5 mm (the ny = 41 half-cell) at
// every resolution: a geometry approximation held constant (§5.11).
0.5 * 0.41 / 41.0,
6.0 * h,
12,
4.0,
sweeps,
)
}
fn quality(mesh: &PatchMesh) -> (f64, f64, f64, f64, f64) {
let (mut min_s, mut max_s, mut min_n, mut max_n) =
(f64::INFINITY, 0.0_f64, f64::INFINITY, 0.0_f64);
let mut worst = 0.0_f64;
for (f, face) in mesh.faces().iter().enumerate() {
let len = (face.s[0] * face.s[0] + face.s[1] * face.s[1]).sqrt();
if mesh.is_sface(f) {
min_n = min_n.min(len);
max_n = max_n.max(len);
} else {
min_s = min_s.min(len);
max_s = max_s.max(len);
}
if face.owner.is_some() && face.neigh.is_some() {
let dl = (face.d[0] * face.d[0] + face.d[1] * face.d[1]).sqrt();
let cos =
((face.s[0] * face.d[0] + face.s[1] * face.d[1]) / (len * dl)).clamp(-1.0, 1.0);
worst = worst.max(cos.acos().to_degrees());
}
}
(min_s, max_s, min_n, max_n, worst)
}
/// Wall-row thickness: the n-spacing of the first row (s-face lengths in row 0).
fn wall_row(mesh: &PatchMesh) -> (f64, f64) {
let (mut lo, mut hi) = (f64::INFINITY, 0.0_f64);
for i in 0..mesh.ns() {
let c = mesh.cell(0, i);
let f = mesh.cell_faces(c)[0].0; // west s-face
let s = mesh.faces()[f].s;
let len = (s[0] * s[0] + s[1] * s[1]).sqrt();
lo = lo.min(len);
hi = hi.max(len);
}
(lo, hi)
}
#[test]
fn cylinder_flag_patch_is_valid_and_resolves_the_body() -> CfdResult<()> {
for ny in [41usize, 62, 82] {
let h = H / ny as f64;
let (mesh, (sweeps, moved)) = body_patch(h)?;
let (min_s, max_s, min_n, max_n, worst) = quality(&mesh);
// Where the worst non-orthogonality sits.
let mut worst_at = (0usize, [0.0; 2]);
for (f, face) in mesh.faces().iter().enumerate() {
if face.owner.is_some() && face.neigh.is_some() {
let len = (face.s[0] * face.s[0] + face.s[1] * face.s[1]).sqrt();
let dl = (face.d[0] * face.d[0] + face.d[1] * face.d[1]).sqrt();
let cos =
((face.s[0] * face.d[0] + face.s[1] * face.d[1]) / (len * dl)).clamp(-1.0, 1.0);
if (cos.acos().to_degrees() - worst).abs() < 1e-9 {
worst_at = (f, face.centre);
}
}
}
println!(
" worst non-orthogonality at face {} ({:.4}, {:.4})",
worst_at.0, worst_at.1[0], worst_at.1[1]
);
let (w_lo, w_hi) = wall_row(&mesh);
println!(
" ny = {ny}: patch {}x{} ({} cells); Winslow {sweeps} sweeps (last move {:.1e} h); s-spacing [{:.2}, {:.2}] h, n-spacing [{:.2}, {:.2}] h, wall row [{:.2}, {:.2}] h, worst non-orthogonality {worst:.1} deg",
mesh.ns(),
mesh.nn(),
mesh.cell_count(),
moved / h,
min_s / h,
max_s / h,
min_n / h,
max_n / h,
w_lo / h,
w_hi / h
);
// The junction fillets fan to ~76° (structural: a concave arc's
// normals converge at its centre); the P0 MMS on this patch holds
// its orders regardless (`cylinder_flag_mms.rs`), so the gate here
// guards folds and collapsed faces, not the angle.
mesh.validate(80.0).map_err(rtx_cfd::CfdError::mesh)?;
assert!(worst < 80.0, "non-orthogonality {worst:.1}");
// The wall row opens at the junction fillets, where the rays are
// longest (the hull's bridge over the armpit): 0.37 / 0.43 h at ny =
// 41 / 62 with the fixed 5 mm fillet; 0.23 h elsewhere. The P0 MMS
// holds its orders on these very meshes (`cylinder_flag_mms.rs`).
assert!(w_hi < 0.5 * h, "wall row {:.2} h too thick", w_hi / h);
// The tip semicircle's 16 cells give 0.196 h; the patch's explicit
// along-body diffusion limit (0.4 hs²/4ν) is then ~5× below the
// embedded harness's CFD1/CFD2 step (CFD3 is convection-limited and
// unaffected) — a disclosed cost, not a defect.
assert!(
min_s > 0.19 * h,
"along-body spacing {:.3} h too fine for the dt budget",
min_s / h
);
}
Ok(())
}
#[test]
fn cylinder_flag_patch_classifies_the_benchmark_background() -> CfdResult<()> {
for ny in [41usize, 62, 82] {
let h = H / ny as f64;
let nx = (L / h).round() as usize;
let (mesh, _) = body_patch(h)?;
let map = OverlapMap::build(&mesh, nx, ny, h, h, DEFAULT_OVERLAP_ROWS)?;
println!(
" ny = {ny} (background {nx}x{ny}): hole {}, fringe {}, prescribed u {} v {}, acceptors {}",
map.hole_cells(),
map.fringe_count(),
map.fringe_u.len(),
map.fringe_v.len(),
map.acceptors.len()
);
assert!(map.fringe_count() > 0);
}
Ok(())
}