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
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
154 lines
5.9 KiB
Rust
154 lines
5.9 KiB
Rust
//! A-P4-0 (`docs/overset_metal_campaign.md` §5.11): the O-grid around the
|
||
//! Turek–Hron 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(())
|
||
}
|