//! 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 { 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 { 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 { 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 { 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 { 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 { 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 { // 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 { 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 { 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 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 { 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 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 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::>>() }) }); 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 { 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 = 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 Turek–Hron 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>>, ) -> 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> = 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)) }