rtx-cfd: overset P4-0 — the O-grid around the Turek–Hron rigid body (cylinder + flag), gated
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
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
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
62df6bd628
commit
45ff34da8f
@@ -243,3 +243,415 @@ pub fn stadium(
|
|||||||
outer.push(outer[0]);
|
outer.push(outer[0]);
|
||||||
transfinite(&inner, &outer, nn, stretch, Some([0.0, 0.0]))
|
transfinite(&inner, &outer, nn, stretch, Some([0.0, 0.0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Cumulative arclength fractions of a closed polyline (`pts[0]` repeated
|
||||||
|
/// as the implicit last point): `frac[i]` for `i = 0..=n`, `frac[n] = 1`.
|
||||||
|
fn arclength_fractions(pts: &[[f64; 2]]) -> Vec<f64> {
|
||||||
|
let n = pts.len();
|
||||||
|
let mut cum = Vec::with_capacity(n + 1);
|
||||||
|
let mut s = 0.0;
|
||||||
|
cum.push(0.0);
|
||||||
|
for i in 0..n {
|
||||||
|
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||||||
|
s += ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
|
||||||
|
cum.push(s);
|
||||||
|
}
|
||||||
|
cum.iter().map(|c| c / s).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Point at arclength fraction `f` of the closed polyline `pts`.
|
||||||
|
fn point_at_fraction(pts: &[[f64; 2]], cum: &[f64], f: f64) -> [f64; 2] {
|
||||||
|
let n = pts.len();
|
||||||
|
let f = f.clamp(0.0, 1.0);
|
||||||
|
// cum has n+1 entries, cum[i] .. cum[i+1] is segment i.
|
||||||
|
let mut i = match cum.binary_search_by(|c| c.partial_cmp(&f).unwrap()) {
|
||||||
|
Ok(k) => k.min(n - 1),
|
||||||
|
Err(k) => k.saturating_sub(1).min(n - 1),
|
||||||
|
};
|
||||||
|
while i + 1 < cum.len() - 1 && cum[i + 1] < f {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||||||
|
let seg = cum[i + 1] - cum[i];
|
||||||
|
let t = if seg > 0.0 { (f - cum[i]) / seg } else { 0.0 };
|
||||||
|
[a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nearest point on the closed polyline `pts` to `q` (segment projection).
|
||||||
|
fn nearest_on_polyline(pts: &[[f64; 2]], q: [f64; 2]) -> [f64; 2] {
|
||||||
|
let n = pts.len();
|
||||||
|
let (mut best, mut best_d) = (pts[0], f64::INFINITY);
|
||||||
|
for i in 0..n {
|
||||||
|
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||||||
|
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||||
|
let l2 = dx * dx + dy * dy;
|
||||||
|
let t = if l2 > 0.0 {
|
||||||
|
(((q[0] - a[0]) * dx + (q[1] - a[1]) * dy) / l2).clamp(0.0, 1.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let p = [a[0] + t * dx, a[1] + t * dy];
|
||||||
|
let d = (p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2);
|
||||||
|
if d < best_d {
|
||||||
|
best_d = d;
|
||||||
|
best = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Points on the circular arc of centre `c`, radius `r`, from angle `a0` to
|
||||||
|
/// `a1` (radians, signed sweep), `k` segments, EXCLUDING the end point.
|
||||||
|
fn arc_points(c: [f64; 2], r: f64, a0: f64, a1: f64, k: usize) -> Vec<[f64; 2]> {
|
||||||
|
(0..k)
|
||||||
|
.map(|m| {
|
||||||
|
let th = a0 + (a1 - a0) * m as f64 / k as f64;
|
||||||
|
[c[0] + r * th.cos(), c[1] + r * th.sin()]
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Turek–Hron rigid body — the cylinder of radius `r` at `centre` with
|
||||||
|
/// the flag of half-thickness `t` reaching to `x_tip` — as a closed
|
||||||
|
/// COUNTER-CLOCKWISE outline: the flag tip a semicircle of radius `t`, the
|
||||||
|
/// two concave junctions filleted with radius `fillet` (`k_fillet` cells
|
||||||
|
/// each), straights graded from the tip/fillet spacing to `d_straight`
|
||||||
|
/// (ratio `grade`), the cylinder arc at ≈ `d_straight`, the tip arc with
|
||||||
|
/// `k_tip` cells. Starts at the tip's topmost point.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn cylinder_flag_outline(
|
||||||
|
centre: [f64; 2],
|
||||||
|
r: f64,
|
||||||
|
t: f64,
|
||||||
|
x_tip: f64,
|
||||||
|
fillet: f64,
|
||||||
|
k_fillet: usize,
|
||||||
|
k_tip: usize,
|
||||||
|
d_straight: f64,
|
||||||
|
grade: f64,
|
||||||
|
) -> Vec<[f64; 2]> {
|
||||||
|
let (cx, cy) = (centre[0], centre[1]);
|
||||||
|
let (y_top, y_bot) = (cy + t, cy - t);
|
||||||
|
let tip_c = [x_tip - t, cy];
|
||||||
|
// Junction x on the circle, and the fillet centre F (in the fluid,
|
||||||
|
// tangent to the flag line and externally to the cylinder).
|
||||||
|
let x_f = cx + ((r + fillet).powi(2) - (t + fillet).powi(2)).sqrt();
|
||||||
|
let f_top = [x_f, y_top + fillet];
|
||||||
|
let f_bot = [x_f, y_bot - fillet];
|
||||||
|
// Tangent points: on the flag line straight below/above F; on the circle
|
||||||
|
// along C → F.
|
||||||
|
let t1_top = [x_f, y_top];
|
||||||
|
let dir_top = [
|
||||||
|
(f_top[0] - cx) / (r + fillet),
|
||||||
|
(f_top[1] - cy) / (r + fillet),
|
||||||
|
];
|
||||||
|
let t2_top = [cx + r * dir_top[0], cy + r * dir_top[1]];
|
||||||
|
let t1_bot = [x_f, y_bot];
|
||||||
|
let dir_bot = [
|
||||||
|
(f_bot[0] - cx) / (r + fillet),
|
||||||
|
(f_bot[1] - cy) / (r + fillet),
|
||||||
|
];
|
||||||
|
let t2_bot = [cx + r * dir_bot[0], cy + r * dir_bot[1]];
|
||||||
|
let d0 = (PI * t / k_tip as f64).min(fillet * PI / 2.0 / k_fillet as f64);
|
||||||
|
|
||||||
|
let mut pts: Vec<[f64; 2]> = Vec::new();
|
||||||
|
// 1. Top edge, from the tip top (x_tip − t, y_top) to the fillet tangent
|
||||||
|
// point (x_f, y_top), moving −x (CCW: body on the left).
|
||||||
|
let len_top = (x_tip - t) - x_f;
|
||||||
|
let fr = graded_fractions(len_top, d0, d_straight, grade);
|
||||||
|
for &f in &fr[..fr.len() - 1] {
|
||||||
|
pts.push([(x_tip - t) - f * len_top, y_top]);
|
||||||
|
}
|
||||||
|
// 2. Top fillet, from T1 (angle −90° about F) to T2 (angle of C − F),
|
||||||
|
// the short way.
|
||||||
|
let a_t1 = -PI / 2.0;
|
||||||
|
let mut a_t2 = (t2_top[1] - f_top[1]).atan2(t2_top[0] - f_top[0]);
|
||||||
|
while a_t2 - a_t1 > PI {
|
||||||
|
a_t2 -= 2.0 * PI;
|
||||||
|
}
|
||||||
|
while a_t2 - a_t1 < -PI {
|
||||||
|
a_t2 += 2.0 * PI;
|
||||||
|
}
|
||||||
|
pts.extend(arc_points(f_top, fillet, a_t1, a_t2, k_fillet));
|
||||||
|
// 3. Cylinder arc, CCW from angle(T2_top) to angle(T2_bot) + 2π.
|
||||||
|
let th_top = (t2_top[1] - cy).atan2(t2_top[0] - cx);
|
||||||
|
let th_bot = (t2_bot[1] - cy).atan2(t2_bot[0] - cx) + 2.0 * PI;
|
||||||
|
let arc_len = (th_bot - th_top) * r;
|
||||||
|
let k_arc = ((arc_len / d_straight).round() as usize).max(8);
|
||||||
|
pts.extend(arc_points(centre, r, th_top, th_bot, k_arc));
|
||||||
|
// 4. Bottom fillet, from T2_bot to T1_bot (angle +90° about F_bot).
|
||||||
|
let a_b2 = (t2_bot[1] - f_bot[1]).atan2(t2_bot[0] - f_bot[0]);
|
||||||
|
let mut a_b1 = PI / 2.0;
|
||||||
|
while a_b1 - a_b2 > PI {
|
||||||
|
a_b1 -= 2.0 * PI;
|
||||||
|
}
|
||||||
|
while a_b1 - a_b2 < -PI {
|
||||||
|
a_b1 += 2.0 * PI;
|
||||||
|
}
|
||||||
|
pts.extend(arc_points(f_bot, fillet, a_b2, a_b1, k_fillet));
|
||||||
|
// 5. Bottom edge, from (x_f, y_bot) to the tip bottom, moving +x.
|
||||||
|
for &f in &fr[..fr.len() - 1] {
|
||||||
|
pts.push([x_f + f * len_top, y_bot]);
|
||||||
|
}
|
||||||
|
// 6. Tip semicircle, from −90° to +90° about the tip centre (CCW).
|
||||||
|
pts.extend(arc_points(tip_c, t, -PI / 2.0, PI / 2.0, k_tip));
|
||||||
|
let _ = (t1_top, t1_bot);
|
||||||
|
pts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 2-D convex hull (Andrew's monotone chain), counter-clockwise.
|
||||||
|
fn convex_hull(mut pts: Vec<[f64; 2]>) -> Vec<[f64; 2]> {
|
||||||
|
pts.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
pts.dedup();
|
||||||
|
if pts.len() < 3 {
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
let cross = |o: [f64; 2], a: [f64; 2], b: [f64; 2]| {
|
||||||
|
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
||||||
|
};
|
||||||
|
let mut lower: Vec<[f64; 2]> = Vec::new();
|
||||||
|
for &p in &pts {
|
||||||
|
while lower.len() >= 2 && cross(lower[lower.len() - 2], lower[lower.len() - 1], p) <= 0.0 {
|
||||||
|
lower.pop();
|
||||||
|
}
|
||||||
|
lower.push(p);
|
||||||
|
}
|
||||||
|
let mut upper: Vec<[f64; 2]> = Vec::new();
|
||||||
|
for &p in pts.iter().rev() {
|
||||||
|
while upper.len() >= 2 && cross(upper[upper.len() - 2], upper[upper.len() - 1], p) <= 0.0 {
|
||||||
|
upper.pop();
|
||||||
|
}
|
||||||
|
upper.push(p);
|
||||||
|
}
|
||||||
|
lower.pop();
|
||||||
|
upper.pop();
|
||||||
|
lower.extend(upper);
|
||||||
|
lower
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The normal offset by `d` of a convex counter-clockwise polygon, as a
|
||||||
|
/// fine polyline (`per_vertex` points on each rounded corner).
|
||||||
|
fn offset_convex_polygon(hull: &[[f64; 2]], d: f64, per_vertex: usize) -> Vec<[f64; 2]> {
|
||||||
|
let n = hull.len();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for i in 0..n {
|
||||||
|
let prev = hull[(i + n - 1) % n];
|
||||||
|
let cur = hull[i];
|
||||||
|
let next = hull[(i + 1) % n];
|
||||||
|
// Outward normals of the incoming and outgoing edges (CCW polygon:
|
||||||
|
// outward = right-hand normal (dy, −dx)).
|
||||||
|
let n_in = {
|
||||||
|
let (dx, dy) = (cur[0] - prev[0], cur[1] - prev[1]);
|
||||||
|
let l = (dx * dx + dy * dy).sqrt().max(1e-300);
|
||||||
|
[dy / l, -dx / l]
|
||||||
|
};
|
||||||
|
let n_out = {
|
||||||
|
let (dx, dy) = (next[0] - cur[0], next[1] - cur[1]);
|
||||||
|
let l = (dx * dx + dy * dy).sqrt().max(1e-300);
|
||||||
|
[dy / l, -dx / l]
|
||||||
|
};
|
||||||
|
let a0 = n_in[1].atan2(n_in[0]);
|
||||||
|
let mut a1 = n_out[1].atan2(n_out[0]);
|
||||||
|
while a1 < a0 {
|
||||||
|
a1 += 2.0 * PI;
|
||||||
|
}
|
||||||
|
// Rounded corner around `cur` from n_in to n_out (CCW sweep ≤ π).
|
||||||
|
let k = if a1 - a0 > 1e-9 { per_vertex } else { 1 };
|
||||||
|
for m in 0..k {
|
||||||
|
let th = a0 + (a1 - a0) * m as f64 / k as f64;
|
||||||
|
out.push([cur[0] + d * th.cos(), cur[1] + d * th.sin()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Winslow (TTM) smoothing of the interior nodes of a periodic structured
|
||||||
|
/// grid `x[k][i]`, `k = 0..=nn` (row 0 fixed; row nn fixed, or SLIDING on
|
||||||
|
/// the closed curve `outer_curve` when given: after each interior sweep
|
||||||
|
/// every outer node is re-placed at the nearest point of the curve to the
|
||||||
|
/// extrapolated ray from rows nn−2, nn−1 — the distribution on the outer
|
||||||
|
/// ring follows the interior instead of dictating it), `i = 0..ns`
|
||||||
|
/// (column `ns` mirrors column 0). Gauss–Seidel until the largest node
|
||||||
|
/// move is below `tol` or `max_sweeps`. Returns `(sweeps, last move)`.
|
||||||
|
pub fn winslow_smooth(
|
||||||
|
x: &mut [Vec<[f64; 2]>],
|
||||||
|
ns: usize,
|
||||||
|
tol: f64,
|
||||||
|
max_sweeps: usize,
|
||||||
|
outer_curve: Option<&[[f64; 2]]>,
|
||||||
|
) -> (usize, f64) {
|
||||||
|
let nn = x.len() - 1;
|
||||||
|
let mut sweeps = 0;
|
||||||
|
let mut moved = f64::INFINITY;
|
||||||
|
while sweeps < max_sweeps && moved > tol {
|
||||||
|
sweeps += 1;
|
||||||
|
moved = 0.0;
|
||||||
|
if let Some(curve) = outer_curve {
|
||||||
|
for i in 0..ns {
|
||||||
|
let (a, b) = (x[nn - 2][i], x[nn - 1][i]);
|
||||||
|
let cand = [2.0 * b[0] - a[0], 2.0 * b[1] - a[1]];
|
||||||
|
let p = nearest_on_polyline(curve, cand);
|
||||||
|
let d = ((p[0] - x[nn][i][0]).powi(2) + (p[1] - x[nn][i][1]).powi(2)).sqrt();
|
||||||
|
moved = moved.max(d);
|
||||||
|
x[nn][i] = p;
|
||||||
|
}
|
||||||
|
x[nn][ns] = x[nn][0];
|
||||||
|
}
|
||||||
|
for k in 1..nn {
|
||||||
|
for i in 0..ns {
|
||||||
|
let ip = (i + 1) % ns;
|
||||||
|
let im = (i + ns - 1) % ns;
|
||||||
|
let (xe, xw, xn, xs) = (x[k][ip], x[k][im], x[k + 1][i], x[k - 1][i]);
|
||||||
|
let (xne, xnw, xse, xsw) = (x[k + 1][ip], x[k + 1][im], x[k - 1][ip], x[k - 1][im]);
|
||||||
|
let xi = [0.5 * (xe[0] - xw[0]), 0.5 * (xe[1] - xw[1])];
|
||||||
|
let eta = [0.5 * (xn[0] - xs[0]), 0.5 * (xn[1] - xs[1])];
|
||||||
|
let g11 = xi[0] * xi[0] + xi[1] * xi[1];
|
||||||
|
let g22 = eta[0] * eta[0] + eta[1] * eta[1];
|
||||||
|
let g12 = xi[0] * eta[0] + xi[1] * eta[1];
|
||||||
|
let denom = 2.0 * (g11 + g22);
|
||||||
|
if denom <= 1e-300 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut new = [0.0; 2];
|
||||||
|
for c in 0..2 {
|
||||||
|
let cross = 0.25 * (xne[c] - xnw[c] - xse[c] + xsw[c]);
|
||||||
|
new[c] =
|
||||||
|
(g22 * (xe[c] + xw[c]) + g11 * (xn[c] + xs[c]) - 2.0 * g12 * cross) / denom;
|
||||||
|
}
|
||||||
|
let d = ((new[0] - x[k][i][0]).powi(2) + (new[1] - x[k][i][1]).powi(2)).sqrt();
|
||||||
|
moved = moved.max(d);
|
||||||
|
x[k][i] = new;
|
||||||
|
}
|
||||||
|
x[k][ns] = x[k][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(sweeps, moved)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-space the nodes of every column (ray) of `x` along its polyline to the
|
||||||
|
/// fractions `eta` (keeps the smoothed ray SHAPES, restores the across-body
|
||||||
|
/// stretch Winslow equidistributes away).
|
||||||
|
pub fn respace_rays(x: &mut [Vec<[f64; 2]>], ns: usize, eta: &[f64]) {
|
||||||
|
let nn = x.len() - 1;
|
||||||
|
for i in 0..=ns {
|
||||||
|
let ray: Vec<[f64; 2]> = (0..=nn).map(|k| x[k][i]).collect();
|
||||||
|
let mut cum = vec![0.0; nn + 1];
|
||||||
|
for k in 1..=nn {
|
||||||
|
cum[k] = cum[k - 1]
|
||||||
|
+ ((ray[k][0] - ray[k - 1][0]).powi(2) + (ray[k][1] - ray[k - 1][1]).powi(2))
|
||||||
|
.sqrt();
|
||||||
|
}
|
||||||
|
let total = cum[nn];
|
||||||
|
for (k, &e) in eta.iter().enumerate().skip(1).take(nn - 1) {
|
||||||
|
let target = e * total;
|
||||||
|
let mut seg = 0;
|
||||||
|
while seg + 1 < nn && cum[seg + 1] < target {
|
||||||
|
seg += 1;
|
||||||
|
}
|
||||||
|
let l = cum[seg + 1] - cum[seg];
|
||||||
|
let tt = if l > 0.0 {
|
||||||
|
(target - cum[seg]) / l
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
x[k][i] = [
|
||||||
|
ray[seg][0] + tt * (ray[seg + 1][0] - ray[seg][0]),
|
||||||
|
ray[seg][1] + tt * (ray[seg + 1][1] - ray[seg][1]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The O-grid around the Turek–Hron rigid body (`docs/overset_metal_campaign.md`
|
||||||
|
/// §5.11): inner ring = `cylinder_flag_outline` (reversed to CLOCKWISE, the
|
||||||
|
/// right-handed frame), outer ring = the `offset` normal offset of the
|
||||||
|
/// body's convex hull sampled at the inner ring's arclength fractions,
|
||||||
|
/// transfinite start with the geometric `stretch` across, Winslow smoothing
|
||||||
|
/// of the interior, then re-spacing along each ray to the stretch. Returns
|
||||||
|
/// the mesh and `(winslow sweeps, last move)`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn cylinder_flag_patch(
|
||||||
|
centre: [f64; 2],
|
||||||
|
r: f64,
|
||||||
|
t: f64,
|
||||||
|
x_tip: f64,
|
||||||
|
h: f64,
|
||||||
|
fillet: f64,
|
||||||
|
offset: f64,
|
||||||
|
nn: usize,
|
||||||
|
stretch: f64,
|
||||||
|
winslow_sweeps: usize,
|
||||||
|
) -> CfdResult<(PatchMesh, (usize, f64))> {
|
||||||
|
// The fillet radius is a GEOMETRY parameter, fixed across a refinement
|
||||||
|
// ladder (a radius ∝ h is an O(h) boundary error: measured Stokes orders
|
||||||
|
// 1.74 → 1.31 with fillet = h/2 on the ny = 41/62/82 ladder).
|
||||||
|
let mut inner = cylinder_flag_outline(centre, r, t, x_tip, fillet, 3, 16, h, 1.15);
|
||||||
|
inner.reverse(); // clockwise
|
||||||
|
let ns = inner.len();
|
||||||
|
// Outer ring: hull offset, sampled at the inner ring's arclength
|
||||||
|
// fractions, starting from the point nearest the inner start's normal
|
||||||
|
// offset.
|
||||||
|
let mut hull_src: Vec<[f64; 2]> = arc_points(centre, r, 0.0, 2.0 * PI, 256);
|
||||||
|
hull_src.extend(arc_points(
|
||||||
|
[x_tip - t, centre[1]],
|
||||||
|
t,
|
||||||
|
-PI / 2.0,
|
||||||
|
PI / 2.0,
|
||||||
|
64,
|
||||||
|
));
|
||||||
|
hull_src.push([x_tip - t, centre[1] + t]);
|
||||||
|
hull_src.push([x_tip - t, centre[1] - t]);
|
||||||
|
let hull = convex_hull(hull_src);
|
||||||
|
let outer_poly = offset_convex_polygon(&hull, offset, 24);
|
||||||
|
// Initial outer ring: the inner point pushed along its outward normal
|
||||||
|
// (the ring is clockwise, so the outward normal is the LEFT-hand normal
|
||||||
|
// of the direction of travel) and projected onto the hull offset —
|
||||||
|
// exact on the convex parts, bunched across the concave junctions (the
|
||||||
|
// swallowtail), which the sliding Winslow rows then spread out.
|
||||||
|
let outer: Vec<[f64; 2]> = (0..=ns)
|
||||||
|
.map(|i| {
|
||||||
|
let i0 = i % ns;
|
||||||
|
let prev = inner[(i0 + ns - 1) % ns];
|
||||||
|
let cur = inner[i0];
|
||||||
|
let next = inner[(i0 + 1) % ns];
|
||||||
|
let (dx, dy) = (next[0] - prev[0], next[1] - prev[1]);
|
||||||
|
let l = (dx * dx + dy * dy).sqrt().max(1e-300);
|
||||||
|
let normal = [-dy / l, dx / l];
|
||||||
|
nearest_on_polyline(
|
||||||
|
&outer_poly,
|
||||||
|
[cur[0] + offset * normal[0], cur[1] + offset * normal[1]],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut inner_closed = inner.clone();
|
||||||
|
inner_closed.push(inner[0]);
|
||||||
|
// Transfinite start.
|
||||||
|
let eta = stretched_fractions(nn, stretch);
|
||||||
|
let mut x: Vec<Vec<[f64; 2]>> = eta
|
||||||
|
.iter()
|
||||||
|
.map(|&e| {
|
||||||
|
(0..=ns)
|
||||||
|
.map(|i| {
|
||||||
|
[
|
||||||
|
(1.0 - e) * inner_closed[i][0] + e * outer[i][0],
|
||||||
|
(1.0 - e) * inner_closed[i][1] + e * outer[i][1],
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let report = winslow_smooth(&mut x, ns, 1e-10 * h, winslow_sweeps, Some(&outer_poly));
|
||||||
|
respace_rays(&mut x, ns, &eta);
|
||||||
|
let mut xs = Vec::with_capacity((nn + 1) * (ns + 1));
|
||||||
|
let mut ys = Vec::with_capacity(xs.capacity());
|
||||||
|
for row in &x {
|
||||||
|
for p in row {
|
||||||
|
xs.push(p[0]);
|
||||||
|
ys.push(p[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mesh = PatchMesh::from_nodes(ns, nn, xs, ys, Some([0.0, 0.0]))?;
|
||||||
|
Ok((mesh, report))
|
||||||
|
}
|
||||||
|
|||||||
@@ -564,6 +564,7 @@ impl OversetPisoSolver {
|
|||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let p_before_stamp = field.background.p.clone();
|
||||||
let old_map = std::mem::replace(&mut self.overlap, new);
|
let old_map = std::mem::replace(&mut self.overlap, new);
|
||||||
self.background
|
self.background
|
||||||
.set_overlap(self.overlap.background_mask(), self.overlap.fringe_flags());
|
.set_overlap(self.overlap.background_mask(), self.overlap.fringe_flags());
|
||||||
@@ -597,6 +598,17 @@ impl OversetPisoSolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.balance_fringe(&mut field.background);
|
self.balance_fringe(&mut field.background);
|
||||||
|
// Knock-out H2 (`RTX_OVERSET_H2`): cells that turn active → fringe
|
||||||
|
// keep the background's own pressure this step instead of the
|
||||||
|
// patch's stamped value (their p' is still Dirichlet from the
|
||||||
|
// patch).
|
||||||
|
if std::env::var("RTX_OVERSET_H2").is_ok() {
|
||||||
|
for e in &self.overlap.fringe_cells {
|
||||||
|
if old_map.class(e.j, e.i) == CellClass::Active {
|
||||||
|
field.background.p[(e.j, e.i)] = p_before_stamp[(e.j, e.i)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Knock-out H4 (`RTX_OVERSET_H4`): no temporal warm start on a
|
// Knock-out H4 (`RTX_OVERSET_H4`): no temporal warm start on a
|
||||||
// reclassification step.
|
// reclassification step.
|
||||||
if std::env::var("RTX_OVERSET_H4").is_ok() {
|
if std::env::var("RTX_OVERSET_H4").is_ok() {
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
//! A-P4-0 gate 3 (`docs/overset_metal_campaign.md` §5.11): the P0
|
||||||
|
//! manufactured solution on the cylinder–flag O-grid (exact acceptors, the
|
||||||
|
//! S3 harness) — do the Stokes-limit and upwind orders survive the junction
|
||||||
|
//! fillets' skew? Rungs at the benchmark's h = 0.41 / 41, 62, 82.
|
||||||
|
|
||||||
|
use rtx_cfd::mesh::PatchMesh;
|
||||||
|
use rtx_cfd::mesh::patch_gen::cylinder_flag_patch;
|
||||||
|
use rtx_cfd::solvers::incompressible::{
|
||||||
|
CurvilinearParameters, CurvilinearPisoSolver, NormalDiffusion, PatchConvection, PatchField,
|
||||||
|
};
|
||||||
|
use rtx_cfd::{CfdConfig, CfdResult};
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
const RHO: f64 = 1.0;
|
||||||
|
const MU: f64 = 0.05;
|
||||||
|
|
||||||
|
fn u_exact(x: f64, y: f64) -> f64 {
|
||||||
|
(PI * x).sin() * (PI * y).cos()
|
||||||
|
}
|
||||||
|
fn v_exact(x: f64, y: f64) -> f64 {
|
||||||
|
-(PI * x).cos() * (PI * y).sin()
|
||||||
|
}
|
||||||
|
fn p_exact(x: f64, y: f64) -> f64 {
|
||||||
|
(PI * x).sin() * (PI * y).sin()
|
||||||
|
}
|
||||||
|
fn source(x: f64, y: f64, convecting: bool) -> (f64, f64) {
|
||||||
|
let conv = if convecting { RHO * 0.5 * PI } else { 0.0 };
|
||||||
|
(
|
||||||
|
conv * (2.0 * PI * x).sin()
|
||||||
|
+ 2.0 * PI * PI * MU * u_exact(x, y)
|
||||||
|
+ PI * (PI * x).cos() * (PI * y).sin(),
|
||||||
|
conv * (2.0 * PI * y).sin()
|
||||||
|
+ 2.0 * PI * PI * MU * v_exact(x, y)
|
||||||
|
+ PI * (PI * x).sin() * (PI * y).cos(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn patch(ny: usize) -> CfdResult<PatchMesh> {
|
||||||
|
let h = 0.41 / ny as f64;
|
||||||
|
let sweeps: usize = std::env::var("RTX_CF_SWEEPS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(500);
|
||||||
|
// Fixed geometry across the ladder: the fillet of the coarsest rung.
|
||||||
|
let fillet = 0.5 * 0.41 / 41.0;
|
||||||
|
Ok(cylinder_flag_patch(
|
||||||
|
[0.2, 0.2],
|
||||||
|
0.05,
|
||||||
|
0.01,
|
||||||
|
0.6,
|
||||||
|
h,
|
||||||
|
fillet,
|
||||||
|
6.0 * h,
|
||||||
|
12,
|
||||||
|
4.0,
|
||||||
|
sweeps,
|
||||||
|
)?
|
||||||
|
.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn march(ny: usize, convection: PatchConvection) -> CfdResult<(f64, usize, f64)> {
|
||||||
|
let mesh = patch(ny)?;
|
||||||
|
let h = 0.41 / ny as f64;
|
||||||
|
let nu = MU / RHO;
|
||||||
|
let mut hs = f64::INFINITY;
|
||||||
|
for c in 0..mesh.cell_count() {
|
||||||
|
for (f, _) in mesh.cell_faces(c) {
|
||||||
|
if mesh.is_sface(f) {
|
||||||
|
let d = mesh.faces()[f].d;
|
||||||
|
hs = hs.min((d[0] * d[0] + d[1] * d[1]).sqrt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let dt = 0.4 * (hs * hs / (4.0 * nu)).min(h);
|
||||||
|
let config = CfdConfig::new()
|
||||||
|
.with_density(RHO)
|
||||||
|
.with_viscosity(MU)
|
||||||
|
.with_reference_velocity(1.0)
|
||||||
|
.with_reference_length(1.0);
|
||||||
|
let convecting = convection == PatchConvection::Upwind;
|
||||||
|
let mut solver = CurvilinearPisoSolver::new(
|
||||||
|
config,
|
||||||
|
CurvilinearParameters {
|
||||||
|
tolerance: 1e-5,
|
||||||
|
convection,
|
||||||
|
normal_diffusion: NormalDiffusion::LineImplicit,
|
||||||
|
..CurvilinearParameters::default()
|
||||||
|
},
|
||||||
|
mesh,
|
||||||
|
)?;
|
||||||
|
solver.set_boundary_velocity(|x, y, _| (u_exact(x, y), v_exact(x, y)));
|
||||||
|
solver.set_momentum_source(move |x, y, _| source(x, y, convecting));
|
||||||
|
solver.set_acceptor_ring(true);
|
||||||
|
let (ns, nn) = (solver.mesh().ns(), solver.mesh().nn());
|
||||||
|
let acc: Vec<(f64, f64, f64)> = (0..ns)
|
||||||
|
.map(|i| {
|
||||||
|
let xy = solver.mesh().centre(solver.mesh().cell(nn - 1, i));
|
||||||
|
(
|
||||||
|
u_exact(xy[0], xy[1]),
|
||||||
|
v_exact(xy[0], xy[1]),
|
||||||
|
p_exact(xy[0], xy[1]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let zeros = vec![0.0; ns];
|
||||||
|
let mut field = PatchField::new(solver.mesh());
|
||||||
|
solver.initialize(&mut field, |_, _| (0.0, 0.0));
|
||||||
|
solver.stamp_acceptors(&mut field, &acc);
|
||||||
|
solver.set_acceptor_correction(&zeros);
|
||||||
|
let steady_tol = if convecting { 1e-6 } else { 1e-7 };
|
||||||
|
let mut steady = f64::INFINITY;
|
||||||
|
let mut steps = 0;
|
||||||
|
let mut max_div = 0.0_f64;
|
||||||
|
for _ in 0..600_000 {
|
||||||
|
let before = (field.u.clone(), field.v.clone());
|
||||||
|
let r = solver.advance(&mut field, dt).await?;
|
||||||
|
assert!(
|
||||||
|
r.poisson_converged,
|
||||||
|
"pressure solve did not converge: {r:?}"
|
||||||
|
);
|
||||||
|
solver.stamp_acceptors(&mut field, &acc);
|
||||||
|
let flux_scale: f64 = field.flux.iter().map(|f| f.abs()).sum::<f64>().max(1e-300);
|
||||||
|
max_div = max_div.max(r.max_divergence / flux_scale);
|
||||||
|
steps += 1;
|
||||||
|
let change = field
|
||||||
|
.u
|
||||||
|
.iter()
|
||||||
|
.zip(&before.0)
|
||||||
|
.chain(field.v.iter().zip(&before.1))
|
||||||
|
.map(|(a, b)| (a - b).abs())
|
||||||
|
.fold(0.0, f64::max);
|
||||||
|
steady = change / dt;
|
||||||
|
if steady < steady_tol {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(steady < steady_tol, "no steady state: {steady:.3e}");
|
||||||
|
let mesh = solver.mesh();
|
||||||
|
let (mut sq, mut vol) = (0.0, 0.0);
|
||||||
|
for c in 0..mesh.cell_count() {
|
||||||
|
if solver.is_acceptor(c) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let xy = mesh.centre(c);
|
||||||
|
let eu = field.u[c] - u_exact(xy[0], xy[1]);
|
||||||
|
let ev = field.v[c] - v_exact(xy[0], xy[1]);
|
||||||
|
sq += (eu * eu + ev * ev) * mesh.area(c);
|
||||||
|
vol += mesh.area(c);
|
||||||
|
}
|
||||||
|
Ok(((sq / vol).sqrt(), steps, max_div))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cylinder_flag_patch_keeps_the_p0_orders() -> CfdResult<()> {
|
||||||
|
// Measured with the fillet fixed at 5 mm: Stokes 2.17 / 2.13; upwind 2.00
|
||||||
|
// / 1.86 — the patch's cells are so small against the field (cell Péclet
|
||||||
|
// ≈ 0.1 at ν = 0.05) that diffusion's second order dominates and upwind's
|
||||||
|
// O(h) term is still emerging (the order falls toward 1 with refinement),
|
||||||
|
// so the upwind band admits the pre-asymptotic second order.
|
||||||
|
for (convection, gate) in [
|
||||||
|
(PatchConvection::None, 1.8..2.6),
|
||||||
|
(PatchConvection::Upwind, 0.7..2.4),
|
||||||
|
] {
|
||||||
|
let mut errs = Vec::new();
|
||||||
|
let mut hs = Vec::new();
|
||||||
|
for ny in [41usize, 62, 82] {
|
||||||
|
let (l2, steps, max_div) = march(ny, convection).await?;
|
||||||
|
println!(
|
||||||
|
" cylinder-flag {convection:?} ny={ny}: L2 {l2:.6e}, max div {max_div:.2e}, {steps} steps"
|
||||||
|
);
|
||||||
|
errs.push(l2);
|
||||||
|
hs.push(0.41 / ny as f64);
|
||||||
|
}
|
||||||
|
let o: Vec<f64> = errs
|
||||||
|
.windows(2)
|
||||||
|
.zip(hs.windows(2))
|
||||||
|
.map(|(e, h)| (e[0] / e[1]).ln() / (h[0] / h[1]).ln())
|
||||||
|
.collect();
|
||||||
|
println!(" cylinder-flag {convection:?} orders {o:?}");
|
||||||
|
assert!(
|
||||||
|
o.iter().all(|x| gate.contains(x)),
|
||||||
|
"{convection:?} orders {o:?} outside {gate:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
//! 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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user