diff --git a/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs b/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs index c06fcd9..c0b3873 100644 --- a/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs +++ b/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs @@ -952,9 +952,16 @@ pub fn cylinder_flag_outline_deformed_tip( 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. - let rc = tip_corner.min(radius).max(0.0); - let flat = rc < radius - 1e-12; + // 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], diff --git a/crates/specialized/rtx-cfd/tests/patch_cylinder_flag_flat_tip.rs b/crates/specialized/rtx-cfd/tests/patch_cylinder_flag_flat_tip.rs index 619cc84..2988f85 100644 --- a/crates/specialized/rtx-cfd/tests/patch_cylinder_flag_flat_tip.rs +++ b/crates/specialized/rtx-cfd/tests/patch_cylinder_flag_flat_tip.rs @@ -25,7 +25,14 @@ const FILLET: f64 = 0.5 * 0.41 / 41.0; const CORNER: f64 = 0.0025; fn edges(a: f64, nx: usize, ny: usize) -> FlagEdges { + edges_breathing(a, nx, ny, 0.0) +} + +/// The flag's edges with the thickness scaled by `1 + eps` (the FEA's +/// deformed tip is never exactly `t` thick). +fn edges_breathing(a: f64, nx: usize, ny: usize, eps: f64) -> FlagEdges { let len = X1 - X0; + let th = T * (1.0 + eps); let centre = |x: f64| { let xi = (x - X0) / len; let y = a * xi * xi * (3.0 - xi) / 2.0; @@ -35,7 +42,7 @@ fn edges(a: f64, nx: usize, ny: usize) -> FlagEdges { let edge = |x: f64, side: f64| -> [f64; 2] { let (y, dy) = centre(x); let n = (1.0 + dy * dy).sqrt(); - [x - side * T * dy / n, CENTRE[1] + y + side * T / n] + [x - side * th * dy / n, CENTRE[1] + y + side * th / n] }; let m = 2 * nx; let bottom: Vec<[f64; 2]> = (0..=m) @@ -234,3 +241,47 @@ fn flat_tip_patch_builds_straight_and_bent_cold_and_warm() -> CfdResult<()> { } Ok(()) } + +/// The semicircle setting (`tip_corner = t`) must keep its topology when +/// the deformed tip is a few nanometres thicker or thinner than `t` (the +/// P6-b ny 41 cap-12 marches died at every α on "topology changed +/// 143x12 -> 144x12" within the first steps of the release — the flat / +/// semicircle branch was chosen against the DEFORMED half-thickness). +#[test] +fn semicircle_setting_keeps_its_topology_under_thickness_breathing() { + for &h in &[0.01, 0.41 / 41.0, 0.41 / 62.0] { + let n0 = outline_breathing(0.0, h, T, 0.0).len(); + for &eps in &[-1e-6, -1e-9, 1e-12, 1e-9, 1e-6, 1e-4] { + for &a in &[0.0, 1e-5, -1e-5, 0.08] { + let n = outline_breathing(a, h, T, eps).len(); + assert_eq!(n, n0, "h = {h}, eps = {eps:e}, a = {a}: {n} points vs {n0}"); + } + } + // The flat tip is insensitive too. + let f0 = outline_breathing(0.0, h, CORNER, 0.0).len(); + for &eps in &[-1e-6, 1e-6, 1e-4] { + assert_eq!( + outline_breathing(0.0, h, CORNER, eps).len(), + f0, + "flat, h = {h}, eps = {eps:e}" + ); + } + } +} + +fn outline_breathing(a: f64, h: f64, corner: f64, eps: f64) -> Vec<[f64; 2]> { + cylinder_flag_outline_deformed_tip( + CENTRE, + R, + T, + &edges_breathing(a, 35, 2, eps), + X1, + FILLET, + 3, + 16, + h, + 1.15, + corner, + ) + .0 +}