rtx-cfd: P5-0 — cylinder_flag_patch_deformed / cylinder_flag_outline_deformed: the O-grid around the cylinder and the FEA's deformed flag edges (FlagEdges bottom / tip / top), edges resampled at the rigid outline's graded arclength spacing, root fillets from the rigid construction, the tip semicircle from the deformed tip's centre and axis; the O-grid body factored out (o_grid_from_outline, hull_source) and the rigid generator kept bit-identical (its pins pass); gates: zero displacement reproduces the rigid outline to 6e-17 (mesh 2.5e-8 through the hull/Winslow pipeline, recorded), ±80 mm cantilever shapes valid at 80° with wall row 0.43 h, along-body 0.196 h, wall nodes on the edges to 6e-17, ny = 41 overlap builds
CI / CI Success (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-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 / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0116sg1Qz1gMv9hdcKP1XUam
This commit is contained in:
Omar Sobh
2026-09-07 07:10:01 -07:00
co-authored by Claude Fable 5.1
parent cee9adef26
commit c2451fbacf
2 changed files with 483 additions and 14 deletions
+269 -14
View File
@@ -587,22 +587,277 @@ pub fn cylinder_flag_patch(
// 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);
let inner = cylinder_flag_outline(centre, r, t, x_tip, fillet, 3, 16, h, 1.15);
let hull_src = hull_source(centre, r, [x_tip - t, centre[1]], t, [1.0, 0.0]);
o_grid_from_outline(inner, hull_src, h, offset, nn, stretch, winslow_sweeps)
}
/// The FEA flag's wetted edges, deformed: `bottom` from the root to the
/// tip, `tip` from the bottom corner to the top corner, `top` from the
/// tip back to the root (the `Interface` walk's order).
#[derive(Debug, Clone)]
pub struct FlagEdges {
/// Root → tip.
pub bottom: Vec<[f64; 2]>,
/// Bottom corner → top corner.
pub tip: Vec<[f64; 2]>,
/// Tip → root.
pub top: Vec<[f64; 2]>,
}
/// The O-grid around the cylinder and a DEFORMED flag (P5-0, §5.12): the
/// inner ring from [`cylinder_flag_outline_deformed`], everything else as
/// [`cylinder_flag_patch`]; with undeformed edges the two agree to
/// rounding.
#[allow(clippy::too_many_arguments)]
pub fn cylinder_flag_patch_deformed(
centre: [f64; 2],
r: f64,
t: f64,
edges: &FlagEdges,
h: f64,
fillet: f64,
offset: f64,
nn: usize,
stretch: f64,
winslow_sweeps: usize,
) -> CfdResult<(PatchMesh, (usize, f64))> {
let (inner, tip) = cylinder_flag_outline_deformed(centre, r, t, edges, fillet, 3, 16, h, 1.15);
let hull_src = hull_source(centre, r, tip.centre, tip.radius, tip.axis);
o_grid_from_outline(inner, hull_src, h, offset, nn, stretch, winslow_sweeps)
}
/// The deformed flag's rounded tip: the semicircle's centre, radius and
/// outward axis.
#[derive(Debug, Clone, Copy)]
pub struct TipArc {
/// Centre of the semicircle.
pub centre: [f64; 2],
/// Radius (half the tip edge's length, ≈ the half-thickness).
pub radius: f64,
/// Unit vector from the centre through the arc's apex (the flag's
/// tangent at the tip).
pub axis: [f64; 2],
}
/// Cumulative arclength of an OPEN polyline (`cum[i]` at `pts[i]`).
fn open_cum(pts: &[[f64; 2]]) -> Vec<f64> {
let mut cum = Vec::with_capacity(pts.len());
let mut s = 0.0;
cum.push(0.0);
for w in pts.windows(2) {
s += ((w[1][0] - w[0][0]).powi(2) + (w[1][1] - w[0][1]).powi(2)).sqrt();
cum.push(s);
}
cum
}
/// Point at arclength `s` along an open polyline.
fn open_point_at(pts: &[[f64; 2]], cum: &[f64], s: f64) -> [f64; 2] {
let n = pts.len();
let s = s.clamp(0.0, cum[n - 1]);
let mut i = 0;
while i + 2 < n && cum[i + 1] < s {
i += 1;
}
let seg = cum[i + 1] - cum[i];
let t = if seg > 0.0 { (s - cum[i]) / seg } else { 0.0 };
[
pts[i][0] + t * (pts[i + 1][0] - pts[i][0]),
pts[i][1] + t * (pts[i + 1][1] - pts[i][1]),
]
}
/// The sub-polyline of an open polyline between arclengths `s0 < s1`,
/// with interpolated end points.
fn open_slice(pts: &[[f64; 2]], cum: &[f64], s0: f64, s1: f64) -> Vec<[f64; 2]> {
let mut out = vec![open_point_at(pts, cum, s0)];
for (i, c) in cum.iter().enumerate() {
if *c > s0 && *c < s1 {
out.push(pts[i]);
}
}
out.push(open_point_at(pts, cum, s1));
out
}
/// Arclength at which an open polyline first crosses `x = x0` (searched
/// from the end `from_end` — the root end of a flag edge), linear on the
/// crossing segment.
fn arclength_at_x(pts: &[[f64; 2]], cum: &[f64], x0: f64, from_end: bool) -> f64 {
let n = pts.len();
let order: Vec<usize> = if from_end {
(0..n - 1).rev().collect()
} else {
(0..n - 1).collect()
};
for i in order {
let (a, b) = (pts[i], pts[i + 1]);
if (a[0] - x0) * (b[0] - x0) <= 0.0 && a[0] != b[0] {
let f = (x0 - a[0]) / (b[0] - a[0]);
return cum[i] + f * (cum[i + 1] - cum[i]);
}
}
if from_end {
cum[n - 1]
} else {
0.0
}
}
/// Points at graded arclength fractions along an open polyline from its
/// start, EXCLUDING the end point (the rigid outline's convention).
fn graded_along(pts: &[[f64; 2]], d0: f64, d_straight: f64, grade: f64) -> Vec<[f64; 2]> {
let cum = open_cum(pts);
let len = cum[cum.len() - 1];
let fr = graded_fractions(len, d0, d_straight, grade);
fr[..fr.len() - 1]
.iter()
.map(|&f| open_point_at(pts, &cum, f * len))
.collect()
}
/// The deformed TurekHron body outline (counter-clockwise, starting at
/// the tip arc's top end, the order of [`cylinder_flag_outline`]): the top
/// edge along the deformed top polyline from the tip arc to the root
/// fillet's tangent point (resampled at the graded arclength spacing), the
/// root fillets and the cylinder arc from the RIGID construction (the
/// clamp keeps the root straight to a micron), the bottom edge, and the
/// tip semicircle whose centre and axis come from the deformed tip. Returns
/// the outline and the tip arc.
#[allow(clippy::too_many_arguments)]
pub fn cylinder_flag_outline_deformed(
centre: [f64; 2],
r: f64,
t: f64,
edges: &FlagEdges,
fillet: f64,
k_fillet: usize,
k_tip: usize,
d_straight: f64,
grade: f64,
) -> (Vec<[f64; 2]>, TipArc) {
let (cx, cy) = (centre[0], centre[1]);
// Root junction, as the rigid outline.
let x_f = cx + ((r + fillet).powi(2) - (t + fillet).powi(2)).sqrt();
let f_top = [x_f, cy + t + fillet];
let f_bot = [x_f, cy - t - fillet];
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 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);
// The tip: corners, the tangent axis from the last segments, the
// semicircle of radius half the tip edge, centred `radius` back.
let (bottom, top) = (&edges.bottom, &edges.top);
let (nb, nt) = (bottom.len(), top.len());
let (b, tc) = (bottom[nb - 1], top[0]);
let (bp, tp) = (bottom[nb - 2], top[1]);
let ax = [
(b[0] - bp[0]) + (tc[0] - tp[0]),
(b[1] - bp[1]) + (tc[1] - tp[1]),
];
let al = (ax[0] * ax[0] + ax[1] * ax[1]).sqrt().max(1e-300);
let axis = [ax[0] / al, ax[1] / al];
let normal = [-axis[1], axis[0]]; // left of the axis = the top side
let radius = 0.5 * ((tc[0] - b[0]).powi(2) + (tc[1] - b[1]).powi(2)).sqrt();
let mid = [0.5 * (b[0] + tc[0]), 0.5 * (b[1] + tc[1])];
let tip_c = [mid[0] - radius * axis[0], mid[1] - radius * axis[1]];
let start_top = [tip_c[0] + radius * normal[0], tip_c[1] + radius * normal[1]];
let start_bot = [tip_c[0] - radius * normal[0], tip_c[1] - radius * normal[1]];
let mut pts: Vec<[f64; 2]> = Vec::new();
// 1. Top edge: from the arc's top end toward the root, along the top
// polyline (given tip → root) from arclength `radius` to x = x_f.
let cum_t = open_cum(top);
let s_root_t = arclength_at_x(top, &cum_t, x_f, true);
let mut top_run = open_slice(top, &cum_t, radius, s_root_t);
top_run[0] = start_top;
pts.extend(graded_along(&top_run, d0, d_straight, grade));
// 2. Top fillet, 3. cylinder arc, 4. bottom fillet — the rigid construction.
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));
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));
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 = x_f along the bottom polyline (root → tip)
// to `radius` short of the corner, ending at the arc's bottom end.
let cum_b = open_cum(bottom);
let s_root_b = arclength_at_x(bottom, &cum_b, x_f, false);
let mut bot_run = open_slice(bottom, &cum_b, s_root_b, cum_b[nb - 1] - radius);
let last = bot_run.len() - 1;
bot_run[last] = start_bot;
pts.extend(graded_along(&bot_run, d0, d_straight, grade));
// 6. Tip semicircle from the bottom end through the apex to the top end.
let a0 = (start_bot[1] - tip_c[1]).atan2(start_bot[0] - tip_c[0]);
pts.extend(arc_points(tip_c, radius, a0, a0 + PI, k_tip));
(
pts,
TipArc {
centre: tip_c,
radius,
axis,
},
)
}
/// The hull source points: the cylinder sampled finely, the tip
/// semicircle (centre, radius, outward axis) sampled finely, and its two
/// end points.
fn hull_source(
centre: [f64; 2],
r: f64,
tip_c: [f64; 2],
rt: f64,
axis: [f64; 2],
) -> Vec<[f64; 2]> {
let mut hull_src: Vec<[f64; 2]> = arc_points(centre, r, 0.0, 2.0 * PI, 256);
let a = axis[1].atan2(axis[0]);
hull_src.extend(arc_points(tip_c, rt, a - PI / 2.0, a + PI / 2.0, 64));
let normal = [-axis[1], axis[0]];
hull_src.push([tip_c[0] + rt * normal[0], tip_c[1] + rt * normal[1]]);
hull_src.push([tip_c[0] - rt * normal[0], tip_c[1] - rt * normal[1]]);
hull_src
}
/// The O-grid body shared by the rigid and the deformed generators: the
/// inner ring (counter-clockwise in, reversed to clockwise), the outer
/// ring on the hull offset, transfinite start, Winslow, re-spacing.
fn o_grid_from_outline(
mut inner: Vec<[f64; 2]>,
hull_src: Vec<[f64; 2]>,
h: f64,
offset: f64,
nn: usize,
stretch: f64,
winslow_sweeps: usize,
) -> CfdResult<(PatchMesh, (usize, f64))> {
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