Files
rustytorch/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs
T
Omar Sobh 26904e37d8
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 (ubuntu-latest) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 6s
Documentation / Build User Guide (push) Successful in 7s
CI / Build CPU-Only (Explicit) (push) Failing after 9s
CI / Format Check (push) Failing after 28s
CI / Clippy Check (push) Failing after 1m19s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m26s
PERF-2 P1.b: the polyline index prunes from the start with an upper bound from each chunk's first vertex (never the answer; the strict-minimum scan rule is unchanged), so a query near the polygon's far end no longer scans every chunk; pin unchanged, 0 mismatches
2026-09-15 23:35:40 -05:00

1313 lines
48 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.
//! Patch generators: transfinite (linear-blend) interpolation between two
//! curves with geometric stretching across, and the test shapes the
//! curvilinear solver is verified on (`docs/overset_metal_campaign.md`
//! §2.2 P0 and §5.3).
use super::patch_mesh::PatchMesh;
use crate::error::CfdResult;
use std::f64::consts::PI;
/// Fractions `η_k ∈ [0, 1]`, `k = 0..=nn`, with spacing growing
/// geometrically by the total factor `stretch` (last / first); `1` is
/// uniform.
pub fn stretched_fractions(nn: usize, stretch: f64) -> Vec<f64> {
if nn == 0 {
return vec![0.0];
}
if (stretch - 1.0).abs() < 1e-12 || nn == 1 {
return (0..=nn).map(|k| k as f64 / nn as f64).collect();
}
let g = stretch.powf(1.0 / (nn as f64 - 1.0));
let total = (1.0 - g.powi(nn as i32)) / (1.0 - g);
let mut eta = Vec::with_capacity(nn + 1);
let mut acc = 0.0;
eta.push(0.0);
for k in 0..nn {
acc += g.powi(k as i32) / total;
eta.push(acc);
}
eta[nn] = 1.0;
eta
}
/// Linear-blend transfinite interpolation between `inner` (row 0) and
/// `outer` (row nn), both with `ns + 1` points (the last repeating the
/// first plus `shift` when periodic). Straight rays between corresponding
/// points; `stretch` is the across-patch spacing ratio.
pub fn transfinite(
inner: &[[f64; 2]],
outer: &[[f64; 2]],
nn: usize,
stretch: f64,
periodic: Option<[f64; 2]>,
) -> CfdResult<PatchMesh> {
assert_eq!(inner.len(), outer.len(), "curves need equal point counts");
let ns = inner.len() - 1;
let eta = stretched_fractions(nn, stretch);
let mut x = Vec::with_capacity((nn + 1) * (ns + 1));
let mut y = Vec::with_capacity((nn + 1) * (ns + 1));
for &e in &eta {
for i in 0..=ns {
x.push((1.0 - e) * inner[i][0] + e * outer[i][0]);
y.push((1.0 - e) * inner[i][1] + e * outer[i][1]);
}
}
PatchMesh::from_nodes(ns, nn, x, y, periodic)
}
/// A uniform Cartesian patch: `s = x`, `n = y`. Non-periodic unless
/// `periodic_x`, in which case the seam carries the shift `(lx, 0)`.
pub fn cartesian(nx: usize, ny: usize, lx: f64, ly: f64, periodic_x: bool) -> CfdResult<PatchMesh> {
let (dx, dy) = (lx / nx as f64, ly / ny as f64);
let mut x = Vec::with_capacity((ny + 1) * (nx + 1));
let mut y = Vec::with_capacity(x.capacity());
for k in 0..=ny {
for i in 0..=nx {
x.push(i as f64 * dx);
y.push(k as f64 * dy);
}
}
PatchMesh::from_nodes(nx, ny, x, y, periodic_x.then_some([lx, 0.0]))
}
/// An O-grid annulus around the circle of radius `r0` centred at `centre`
/// out to a wobbly outer ring of mean radius `r1`: the outer points are
/// rotated by `skew * sin(θ)` and their radius modulated by
/// `1 + 0.1 · skew · sin(2θ)`, so the rays are non-orthogonal to the
/// rings and the cells are skewed smoothly. `skew = 0` gives the polar
/// grid. Periodic in s. `s` runs CLOCKWISE (the body on the right, `n`
/// outward): that is the right-handed `(s, n)` frame `PatchMesh` needs.
pub fn annulus_skewed(
centre: [f64; 2],
r0: f64,
r1: f64,
ns: usize,
nn: usize,
skew: f64,
stretch: f64,
) -> CfdResult<PatchMesh> {
let mut inner = Vec::with_capacity(ns + 1);
let mut outer = Vec::with_capacity(ns + 1);
for i in 0..=ns {
let th = -2.0 * PI * (i % ns) as f64 / ns as f64;
inner.push([centre[0] + r0 * th.cos(), centre[1] + r0 * th.sin()]);
let th_o = th + skew * th.sin();
let r_o = r1 * (1.0 + 0.1 * skew * (2.0 * th).sin());
outer.push([centre[0] + r_o * th_o.cos(), centre[1] + r_o * th_o.sin()]);
}
transfinite(&inner, &outer, nn, stretch, Some([0.0, 0.0]))
}
/// A channel `[0, lx] × [0, ly]` sheared affinely: `x' = x + alpha · y`.
/// All cells are congruent parallelograms; the n-faces stay horizontal.
pub fn channel_sheared(
lx: f64,
ly: f64,
nx: usize,
ny: usize,
alpha: f64,
periodic_x: bool,
) -> CfdResult<PatchMesh> {
let (dx, dy) = (lx / nx as f64, ly / ny as f64);
let mut x = Vec::with_capacity((ny + 1) * (nx + 1));
let mut y = Vec::with_capacity(x.capacity());
for k in 0..=ny {
let yy = k as f64 * dy;
for i in 0..=nx {
x.push(i as f64 * dx + alpha * yy);
y.push(yy);
}
}
PatchMesh::from_nodes(nx, ny, x, y, periodic_x.then_some([lx, 0.0]))
}
/// A channel whose skew varies smoothly along x: `x' = x + alpha · y ·
/// sin(2π x / lx)` (zero at both ends, so it can be periodic), with the
/// across-channel spacing stretched by `stretch` toward the top wall.
/// Folds when `2π alpha ly / lx > 1`; keep `alpha` around 0.1.
pub fn channel_varying_skew(
lx: f64,
ly: f64,
nx: usize,
ny: usize,
alpha: f64,
stretch: f64,
periodic_x: bool,
) -> CfdResult<PatchMesh> {
let dx = lx / nx as f64;
let eta = stretched_fractions(ny, stretch);
let mut x = Vec::with_capacity((ny + 1) * (nx + 1));
let mut y = Vec::with_capacity(x.capacity());
for &e in &eta {
let yy = e * ly;
for i in 0..=nx {
let xx = i as f64 * dx;
x.push(xx + alpha * yy * (2.0 * PI * xx / lx).sin());
y.push(yy);
}
}
PatchMesh::from_nodes(nx, ny, x, y, periodic_x.then_some([lx, 0.0]))
}
/// Spacings along a straight of `length` that grow geometrically from `d0`
/// at both ends (ratio `ratio`) to at most `d1` in the middle, symmetric;
/// returns the cumulative fractions `0 ..= 1`. The count adapts to the
/// length.
pub fn graded_fractions(length: f64, d0: f64, d1: f64, ratio: f64) -> Vec<f64> {
// One end's graded run, until the spacing reaches d1 or half the length.
let mut run = Vec::new();
let mut d = d0;
let mut acc = 0.0;
while d < d1 && acc + d < 0.5 * length {
run.push(d);
acc += d;
d *= ratio;
}
let middle = length - 2.0 * acc;
let n_mid = ((middle / d1).round() as usize).max(1);
let d_mid = middle / n_mid as f64;
let mut spacings = run.clone();
spacings.extend(std::iter::repeat_n(d_mid, n_mid));
spacings.extend(run.iter().rev());
let total: f64 = spacings.iter().sum();
let mut fr = Vec::with_capacity(spacings.len() + 1);
let mut s = 0.0;
fr.push(0.0);
for w in &spacings {
s += w;
fr.push(s / total);
}
let last = fr.len() - 1;
fr[last] = 1.0;
fr
}
/// An O-grid around a STADIUM (a rectangle of half-length `hx` and
/// half-thickness `r` with semicircular ends of radius `r`, the plate of
/// the fresh-cell falsifier rounded at its ends) centred at `centre`: the
/// inner ring is the stadium, the outer ring its normal offset by
/// `offset`, so every ray is a normal (orthogonal cells). Along the body
/// the straights are graded from the arc spacing `d0 = π r / k_arc` at the
/// tangent points to `d_straight` in the middle (ratio `grade`); each end
/// arc carries `k_arc` cells. Across, `nn` cells with the geometric
/// `stretch` (wall spacing = `offset · (g 1)/(g^nn 1)`). `s` runs
/// CLOCKWISE (the right-handed frame `PatchMesh` needs), starting at the
/// top-left tangent point. Periodic.
#[allow(clippy::too_many_arguments)]
pub fn stadium(
centre: [f64; 2],
hx: f64,
r: f64,
offset: f64,
k_arc: usize,
d_straight: f64,
grade: f64,
nn: usize,
stretch: f64,
) -> CfdResult<PatchMesh> {
let a = hx - r; // half-length of the straights
let d0 = PI * r / k_arc as f64;
let straight = graded_fractions(2.0 * a, d0, d_straight, grade);
let mut inner = Vec::new();
let mut outer = Vec::new();
let mut push = |p: [f64; 2], n: [f64; 2]| {
inner.push([centre[0] + p[0], centre[1] + p[1]]);
outer.push([
centre[0] + p[0] + offset * n[0],
centre[1] + p[1] + offset * n[1],
]);
};
// Top straight, left → right (clockwise around the body).
for &f in &straight[..straight.len() - 1] {
push([-a + f * 2.0 * a, r], [0.0, 1.0]);
}
// Right arc, from +90° down to 90° (exclusive of both ends' duplicates
// handled by the straights: include angles strictly between).
for k in 0..k_arc {
let th = PI / 2.0 - PI * k as f64 / k_arc as f64;
let (s, c) = th.sin_cos();
push([a + r * c, r * s], [c, s]);
}
// Bottom straight, right → left.
for &f in &straight[..straight.len() - 1] {
push([a - f * 2.0 * a, -r], [0.0, -1.0]);
}
// Left arc, from 90° down to 270°.
for k in 0..k_arc {
let th = -PI / 2.0 - PI * k as f64 / k_arc as f64;
let (s, c) = th.sin_cos();
push([-a + r * c, r * s], [c, s]);
}
// Close the ring: the last point repeats the first.
inner.push(inner[0]);
outer.push(outer[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).
/// Chunked bounding boxes over a closed polyline for EXACT nearest-point
/// queries (PERF-2 P1, `docs/perf2_campaign.md`): [`Self::nearest`] returns
/// the same point as [`nearest_on_polyline`] — the same segments are tested
/// in the same order with the same strict-minimum rule — but skips every
/// chunk of segments whose box is farther from the query than the best
/// distance so far (with a 1e-12 relative margin, so a box's rounded
/// lower bound can never hide a segment that could still win).
pub struct PolylineIndex<'a> {
pts: &'a [[f64; 2]],
chunk: usize,
/// Per chunk: `[xmin, xmax, ymin, ymax]` of its segments' end points.
boxes: Vec<[f64; 4]>,
}
impl<'a> PolylineIndex<'a> {
/// Index `pts` (a closed polyline: segment `i` joins `pts[i]` and
/// `pts[(i + 1) % n]`) in chunks of 32 segments.
pub fn new(pts: &'a [[f64; 2]]) -> Self {
let n = pts.len();
let chunk = 32;
let boxes = (0..n.div_ceil(chunk))
.map(|c| {
let mut b = [
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
];
let start = c * chunk;
let end = (start + chunk).min(n);
// The chunk's segments' end points: `start..=end` (wrapping).
for i in start..=end {
let p = pts[i % n];
b[0] = b[0].min(p[0]);
b[1] = b[1].max(p[0]);
b[2] = b[2].min(p[1]);
b[3] = b[3].max(p[1]);
}
b
})
.collect();
Self { pts, chunk, boxes }
}
/// The nearest point of the polyline to `q`, bit for bit the answer of
/// [`nearest_on_polyline`].
pub fn nearest(&self, q: [f64; 2]) -> [f64; 2] {
let pts = self.pts;
let n = pts.len();
let (mut best, mut best_d) = (pts[0], f64::INFINITY);
// An UPPER bound on the minimum from each chunk's first vertex (a
// point of the polyline, so no segment can beat it by more than the
// rounding margin): chunks farther than it are pruned from the
// start, and the bound never becomes the answer — the scan below
// keeps the brute-force rule (first segment at the strict minimum).
let mut prune_d = f64::INFINITY;
for c in 0..self.boxes.len() {
let a = pts[c * self.chunk];
let d = (a[0] - q[0]).powi(2) + (a[1] - q[1]).powi(2);
prune_d = prune_d.min(d);
}
for (c, b) in self.boxes.iter().enumerate() {
let dx = (b[0] - q[0]).max(0.0).max(q[0] - b[1]);
let dy = (b[2] - q[1]).max(0.0).max(q[1] - b[3]);
if dx * dx + dy * dy > best_d.min(prune_d) * (1.0 + 1e-12) {
continue;
}
let start = c * self.chunk;
let end = (start + self.chunk).min(n);
for i in start..end {
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
}
}
/// The nearest point of the closed polyline `pts` to `q` by a scan of every
/// segment (the reference for [`PolylineIndex`]).
pub 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 TurekHron 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 nn2, nn1 — the distribution on the outer
/// ring follows the interior instead of dictating it), `i = 0..ns`
/// (column `ns` mirrors column 0). GaussSeidel 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 {
let index = PolylineIndex::new(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 = index.nearest(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 TurekHron 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 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,
None,
)
}
/// 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,
x_tip_ref: f64,
h: f64,
fillet: f64,
offset: f64,
nn: usize,
stretch: f64,
winslow_sweeps: usize,
) -> CfdResult<(PatchMesh, (usize, f64))> {
cylinder_flag_patch_deformed_from(
None,
centre,
r,
t,
edges,
x_tip_ref,
h,
fillet,
offset,
nn,
stretch,
winslow_sweeps,
)
}
/// [`cylinder_flag_patch_deformed`] warm-started from `prev` (the same
/// topology: its interior rows are the Winslow start instead of the
/// transfinite one, so a few sweeps suffice when the outline moved a
/// little — P5's per-pass regeneration). `None` is the cold build.
#[allow(clippy::too_many_arguments)]
pub fn cylinder_flag_patch_deformed_from(
prev: Option<&PatchMesh>,
centre: [f64; 2],
r: f64,
t: f64,
edges: &FlagEdges,
x_tip_ref: f64,
h: f64,
fillet: f64,
offset: f64,
nn: usize,
stretch: f64,
winslow_sweeps: usize,
) -> CfdResult<(PatchMesh, (usize, f64))> {
cylinder_flag_patch_deformed_from_tip(
prev,
centre,
r,
t,
edges,
x_tip_ref,
h,
fillet,
offset,
nn,
stretch,
winslow_sweeps,
t,
)
}
/// [`cylinder_flag_patch_deformed`] with the flat tip of
/// [`cylinder_flag_outline_deformed_tip`] (`tip_corner = t` is the recorded
/// patch).
#[allow(clippy::too_many_arguments)]
pub fn cylinder_flag_patch_deformed_tip(
centre: [f64; 2],
r: f64,
t: f64,
edges: &FlagEdges,
x_tip_ref: f64,
h: f64,
fillet: f64,
offset: f64,
nn: usize,
stretch: f64,
winslow_sweeps: usize,
tip_corner: f64,
) -> CfdResult<(PatchMesh, (usize, f64))> {
cylinder_flag_patch_deformed_from_tip(
None,
centre,
r,
t,
edges,
x_tip_ref,
h,
fillet,
offset,
nn,
stretch,
winslow_sweeps,
tip_corner,
)
}
/// [`cylinder_flag_patch_deformed_from`] with the flat tip.
thread_local! {
/// PERF-2 P0 (`docs/perf2_campaign.md`): wall time [ns] of the
/// regeneration's stages on this thread — outline, hull + offset, ring
/// projection, Winslow sweeps, respace, warm bookkeeping, mesh
/// finalisation — and the number of builds (slot 7).
static REGEN_NS: std::cell::RefCell<[u64; 8]> = const { std::cell::RefCell::new([0; 8]) };
}
fn regen_charge(slot: usize, start: std::time::Instant) {
REGEN_NS.with(|r| r.borrow_mut()[slot] += start.elapsed().as_nanos() as u64);
}
/// The regeneration's stage times so far on this thread (see `REGEN_NS`).
pub fn regen_profile() -> [u64; 8] {
REGEN_NS.with(|r| *r.borrow())
}
#[allow(clippy::too_many_arguments)]
pub fn cylinder_flag_patch_deformed_from_tip(
prev: Option<&PatchMesh>,
centre: [f64; 2],
r: f64,
t: f64,
edges: &FlagEdges,
x_tip_ref: f64,
h: f64,
fillet: f64,
offset: f64,
nn: usize,
stretch: f64,
winslow_sweeps: usize,
tip_corner: f64,
) -> CfdResult<(PatchMesh, (usize, f64))> {
let t0 = std::time::Instant::now();
let (inner, tip) = cylinder_flag_outline_deformed_tip(
centre, r, t, edges, x_tip_ref, fillet, 3, 16, h, 1.15, tip_corner,
);
regen_charge(0, t0);
// With a flat tip the outline's corners would put 90° corners on the
// hull, whose normal offset folds the rays at the tip; the tip disc
// (already a hull source) covers the tip, so the inner points ahead of
// the disc's centre plane are left out of the hull source.
let flat = tip_corner < tip.radius - 1e-12;
// The hull source carries the deformed outline itself: for a straight
// flag the cylinder and the tip disc alone give the flag's surfaces as
// the hull's tangents, for a BENT flag they give the chord under the
// convex side — measured on FSI2 at tip 35 mm as a patch 4.4 h thick
// where 6 h was asked, the acceptors' donors reaching into the fringe
// (P5-2's first death).
let mut hull_src = hull_source(centre, r, tip.centre, tip.radius, tip.axis);
hull_src.extend(inner.iter().copied().filter(|p| {
!flat || (p[0] - tip.centre[0]) * tip.axis[0] + (p[1] - tip.centre[1]) * tip.axis[1] < 0.0
}));
let start = prev.and_then(|m| {
(m.ns() == inner.len() && m.nn() == nn).then(|| {
(0..=nn)
.map(|k| (0..=m.ns()).map(|i| m.node_xy(m.node(k, i))).collect())
.collect::<Vec<Vec<[f64; 2]>>>()
})
});
o_grid_from_outline(
inner,
hull_src,
h,
offset,
nn,
stretch,
winslow_sweeps,
start,
)
}
/// 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 the arclength fractions `fr` (`0 ..= 1`) along an open
/// polyline from its start, EXCLUDING the end point (the rigid outline's
/// convention). The fractions are the UNDEFORMED edge's graded ones, so
/// the point count — the patch topology — is fixed across a march.
fn along_fractions(pts: &[[f64; 2]], fr: &[f64]) -> Vec<[f64; 2]> {
let cum = open_cum(pts);
let len = cum[cum.len() - 1];
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. The
/// edges' graded spacing is that of the UNDEFORMED straight edge (tip at
/// `x_tip_ref`), so the outline's point count — the patch topology the
/// composite's `set_mesh` requires fixed — does not change with the
/// deformation. 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,
x_tip_ref: f64,
fillet: f64,
k_fillet: usize,
k_tip: usize,
d_straight: f64,
grade: f64,
) -> (Vec<[f64; 2]>, TipArc) {
cylinder_flag_outline_deformed_tip(
centre, r, t, edges, x_tip_ref, fillet, k_fillet, k_tip, d_straight, grade, t,
)
}
/// [`cylinder_flag_outline_deformed`] with the tip as the benchmark's FLAT
/// face between two corner arcs of radius `tip_corner` (P5-3 option B):
/// `tip_corner = t` is the recorded semicircle, bit for bit; smaller
/// corners keep the along-wall spacing `d0` (the corner arcs and the flat
/// face are sampled at it, so the fluid step does not collapse) and a
/// point count fixed across the deformation (the flat face's count comes
/// from the UNDEFORMED thickness). The returned [`TipArc`] is still the
/// tip disc of radius half the tip edge — the hull source's shape.
#[allow(clippy::too_many_arguments)]
pub fn cylinder_flag_outline_deformed_tip(
centre: [f64; 2],
r: f64,
t: f64,
edges: &FlagEdges,
x_tip_ref: f64,
fillet: f64,
k_fillet: usize,
k_tip: usize,
d_straight: f64,
grade: f64,
tip_corner: 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 edge fractions of the undeformed straight edge (as the rigid
// outline's `len_top`), applied to each deformed edge's arclength.
let fr = graded_fractions((x_tip_ref - t) - x_f, d0, d_straight, grade);
// 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.
// The corner radius: `radius` (the semicircle) or the flat tip's. The
// branch is chosen against the UNDEFORMED half-thickness `t`, never the
// deformed tip's `radius` (which breathes by nanometres under the FEA's
// deformation and flipped the topology at the semicircle setting).
let flat = tip_corner < t - 1e-9;
let rc = if flat {
tip_corner.min(radius).max(0.0)
} else {
radius
};
// Corner arc centres and the tangent points on the faces.
let c_top = [
tc[0] - rc * axis[0] - rc * normal[0],
tc[1] - rc * axis[1] - rc * normal[1],
];
let c_bot = [
b[0] - rc * axis[0] + rc * normal[0],
b[1] - rc * axis[1] + rc * normal[1],
];
let (start_top, start_bot, s_top0, s_bot1) = if flat {
(
[c_top[0] + rc * normal[0], c_top[1] + rc * normal[1]],
[c_bot[0] - rc * normal[0], c_bot[1] - rc * normal[1]],
rc,
rc,
)
} else {
(start_top, start_bot, radius, radius)
};
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, s_top0, s_root_t);
top_run[0] = start_top;
pts.extend(along_fractions(&top_run, &fr));
// 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] - s_bot1);
let last = bot_run.len() - 1;
bot_run[last] = start_bot;
pts.extend(along_fractions(&bot_run, &fr));
if flat {
// 6'. Bottom corner arc (from the bottom face to the tip face), the
// flat tip face, the top corner arc (to the top face, whose start
// point opens the outline — excluded here). Counts from the
// UNDEFORMED thickness so the topology is fixed.
let k_c = ((PI * rc / 2.0) / d0).round().max(2.0) as usize;
let k_f = ((2.0 * (t - rc)) / d0).round().max(1.0) as usize;
let a_b0 = (-normal[1]).atan2(-normal[0]);
pts.extend(arc_points(c_bot, rc, a_b0, a_b0 + PI / 2.0, k_c));
let (p0, p1) = (
[c_bot[0] + rc * axis[0], c_bot[1] + rc * axis[1]],
[c_top[0] + rc * axis[0], c_top[1] + rc * axis[1]],
);
for m in 0..k_f {
let f = m as f64 / k_f as f64;
pts.push([p0[0] + f * (p1[0] - p0[0]), p0[1] + f * (p1[1] - p0[1])]);
}
let a_t0 = axis[1].atan2(axis[0]);
pts.extend(arc_points(c_top, rc, a_t0, a_t0 + PI / 2.0, k_c));
} else {
// 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.
#[allow(clippy::too_many_arguments)]
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,
start: Option<Vec<Vec<[f64; 2]>>>,
) -> CfdResult<(PatchMesh, (usize, f64))> {
inner.reverse(); // clockwise
let ns = inner.len();
let t0 = std::time::Instant::now();
let hull = convex_hull(hull_src);
let outer_poly = offset_convex_polygon(&hull, offset, 24);
let outer_index = PolylineIndex::new(&outer_poly);
regen_charge(1, t0);
let t0 = std::time::Instant::now();
// 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];
outer_index.nearest([cur[0] + offset * normal[0], cur[1] + offset * normal[1]])
})
.collect();
regen_charge(2, t0);
let mut inner_closed = inner.clone();
inner_closed.push(inner[0]);
// Transfinite start, or the previous mesh's interior with the new
// inner and outer rings (the warm start).
let eta = stretched_fractions(nn, stretch);
let warm = start.is_some();
let mut x: Vec<Vec<[f64; 2]>> = match start {
Some(mut rows) => {
rows[0] = inner_closed.clone();
// The outer ring warm-starts too: the previous ring's nodes
// projected onto the new offset polygon keep the sliding
// rows' converged distribution (the crude normal push would
// undo it every build — measured as the interior moving
// 0.05 h with the wall at rest).
rows[nn] = rows[nn].iter().map(|&p| outer_index.nearest(p)).collect();
rows
}
None => 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(),
};
// The cold build: Winslow, then the ray re-spacing (the P4-0 one-shot).
// The warm build must be IDEMPOTENT at rest, and the one-shot is not
// (the re-spaced mesh is not the smoother's fixed point: measured as
// the interior moving 0.05 h per build with the wall at rest), so it
// alternates one sweep with a re-spacing — the base converged as a
// fixed point of that map stays put, and a moving outline is tracked.
let report = if warm {
let mut last = f64::INFINITY;
let mut done = 0;
for k in 0..winslow_sweeps {
let t0 = std::time::Instant::now();
let before = x.clone();
regen_charge(5, t0);
let t0 = std::time::Instant::now();
winslow_smooth(&mut x, ns, 0.0, 1, Some(&outer_poly));
regen_charge(3, t0);
let t0 = std::time::Instant::now();
respace_rays(&mut x, ns, &eta);
regen_charge(4, t0);
let t0 = std::time::Instant::now();
last = x
.iter()
.zip(&before)
.flat_map(|(r, b)| r.iter().zip(b))
.map(|(p, q)| ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2)).sqrt())
.fold(0.0, f64::max);
regen_charge(5, t0);
done = k + 1;
if last < 1e-10 * h {
break;
}
}
(done, last)
} else {
let t0 = std::time::Instant::now();
let report = winslow_smooth(&mut x, ns, 1e-10 * h, winslow_sweeps, Some(&outer_poly));
regen_charge(3, t0);
let t0 = std::time::Instant::now();
respace_rays(&mut x, ns, &eta);
regen_charge(4, t0);
report
};
let t0 = std::time::Instant::now();
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]))?;
regen_charge(6, t0);
REGEN_NS.with(|r| r.borrow_mut()[7] += 1);
Ok((mesh, report))
}