R8-h: a flat-tipped flag for the 3D solver (default off, byte-identical when off)
DeviceSdf gains `tip_corner: Option<f64>` (None = the capsule, as before; Some(r_c) = a FLAT tip through the centreline's last point, normal to the last segment, corners rounded to r_c): the last segment becomes a ray for the lateral distance and the strip is cut by the tip plane with the span cut's rounded intersection. Host twin (plate.rs: closest/tip_axial/ flat_cap, polyline and plate bodies) and the device φ and velocity (e3_geom.cu geom_phi_at / body_velocity / plate_dist; GeomSdf flat_tip + tip_corner) expression for expression. Knobs: flag test RTX_E3_FLAG_TIP=flat + RTX_E3_FLAG_TIP_CORNER (default 0.00125 m; the tip inset defaults to 0 with the flat tip; the host φ is the device form's); R8-a harness RTX_E3FSI_TIP=flat + RTX_E3FSI_TIP_CORNER (the centreline gains node A as a 36th station). New host test embedded3_flat_tip (G2 geometry: tip plane at the last point, r_c = half = the capsule pulled back by half to 4e-17, cut volume and wall area vs the analytic rounded rectangle at ny 62/124/248). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
171da41ed1
commit
02ab155022
@@ -96,8 +96,35 @@ fn root_fillet() -> f64 {
|
||||
/// record before this date had the apex 10 mm beyond A (`=0` restores
|
||||
/// them): at ny 62 on the recorded motion that was drag 253.3 → 240.0 and
|
||||
/// the lift swing 1,005 → 836 (the overset's 867).
|
||||
///
|
||||
/// R8-h: with the flat tip the default inset is 0 (the flat face through A).
|
||||
fn tip_inset() -> f64 {
|
||||
env_f("RTX_E3_FLAG_TIP_INSET", FLAG_HALF)
|
||||
env_f(
|
||||
"RTX_E3_FLAG_TIP_INSET",
|
||||
if flat_tip().is_some() { 0.0 } else { FLAG_HALF },
|
||||
)
|
||||
}
|
||||
|
||||
/// R8-h (2026-09-25): the tip's shape. `RTX_E3_FLAG_TIP=flat` gives the
|
||||
/// flag a FLAT tip through the centreline's last point (A, the inset
|
||||
/// defaulting to 0), its corners rounded to `RTX_E3_FLAG_TIP_CORNER`
|
||||
/// metres (default 0.00125, the 2D overset's recommended line; at most
|
||||
/// `FLAG_HALF`); unset or `capsule` = the capsule (the semicircular tip).
|
||||
/// The flat tip's host φ and surface velocity are the device form's
|
||||
/// (`DeviceSdf::phi_host`, the kernel's arithmetic).
|
||||
fn flat_tip() -> Option<f64> {
|
||||
match std::env::var("RTX_E3_FLAG_TIP").as_deref() {
|
||||
Err(_) | Ok("capsule") => None,
|
||||
Ok("flat") => {
|
||||
let rc = env_f("RTX_E3_FLAG_TIP_CORNER", 0.00125);
|
||||
assert!(
|
||||
(0.0..=FLAG_HALF).contains(&rc),
|
||||
"RTX_E3_FLAG_TIP_CORNER {rc} outside [0, {FLAG_HALF}]"
|
||||
);
|
||||
Some(rc)
|
||||
}
|
||||
Ok(v) => panic!("RTX_E3_FLAG_TIP={v}: flat or capsule"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooth union with a concave fillet of radius `r` (the plain `min` at r = 0).
|
||||
@@ -491,6 +518,7 @@ fn flag_wake_on_the_device() {
|
||||
r_edge,
|
||||
half: FLAG_HALF,
|
||||
fillet: r_fillet,
|
||||
tip_corner: flat_tip(),
|
||||
poly: match (&plate, recorded()) {
|
||||
(Some(_), _) => Vec::new(),
|
||||
(None, Some(rec)) => recorded_polyline(rec, t)
|
||||
@@ -527,9 +555,9 @@ fn flag_wake_on_the_device() {
|
||||
c.1.clone().expect("sdf")
|
||||
})
|
||||
};
|
||||
let body = if plate_body() {
|
||||
let body = if plate_body() || flat_tip().is_some() {
|
||||
// R8-c: the host φ and surface velocity ARE the device form's (the
|
||||
// kernel's arithmetic on the host).
|
||||
// kernel's arithmetic on the host); R8-h: the flat tip too.
|
||||
Body::from_sdf(move |x, y, z, t| sdf_at(t).phi_host(x, y, z))
|
||||
.with_surface_velocity(move |x, y, z, t| sdf_at(t).velocity_host(x, y, z))
|
||||
} else {
|
||||
@@ -587,6 +615,9 @@ fn flag_wake_on_the_device() {
|
||||
g.cells(),
|
||||
(t_end / dt).ceil() as usize
|
||||
);
|
||||
if let Some(rc) = flat_tip() {
|
||||
println!(" R8-h: FLAT tip through the centreline's last point, corner radius {rc:.5} m");
|
||||
}
|
||||
unsafe { std::env::set_var("RTX_PROFILE", "1") };
|
||||
let mut device = DeviceStep::new(solver, g);
|
||||
device.upload(&field);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//! R8-h (2026-09-25): the flat-tipped flag's geometry (`DeviceSdf::tip_corner`,
|
||||
//! host twin `DeviceSdf::phi_host` = the kernel's `geom_phi_at`).
|
||||
//!
|
||||
//! G2: a straight flag (a five-point polyline, root a semicircle, tip flat
|
||||
//! with corners rounded to `r_c`) at two orientations, cut on the cubic grid
|
||||
//! at three resolutions: the solid volume and the wall area of the cut
|
||||
//! geometry against the analytic rounded rectangle; the tip plane through
|
||||
//! the last point; `r_c = half` = the capsule pulled back by `half`.
|
||||
|
||||
use rtx_cfd::solvers::incompressible::embedded3::{Body, CutGeometry, DeviceSdf, Grid};
|
||||
|
||||
const HALF: f64 = 0.01;
|
||||
const LEN: f64 = 0.35;
|
||||
const P0: [f64; 2] = [0.05, 0.08];
|
||||
|
||||
fn flag(theta: f64, tip: Option<f64>, pull_back: f64) -> DeviceSdf {
|
||||
let (c, s) = (theta.cos(), theta.sin());
|
||||
let len = LEN - pull_back;
|
||||
let poly = (0..5)
|
||||
.map(|m| {
|
||||
let f = len * m as f64 / 4.0;
|
||||
[P0[0] + f * c, P0[1] + f * s]
|
||||
})
|
||||
.collect();
|
||||
DeviceSdf {
|
||||
// the circle far outside the domain
|
||||
cyl: [-10.0, -10.0, 0.01],
|
||||
cyl_cut: false,
|
||||
flag_cut: false,
|
||||
zc: 0.0,
|
||||
span: 1.0,
|
||||
r_edge: 0.0,
|
||||
half: HALF,
|
||||
fillet: 0.0,
|
||||
tip_corner: tip,
|
||||
poly,
|
||||
vel: Vec::new(),
|
||||
plate: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The analytic cross-section: the strip `LEN × 2 HALF`, the root's half
|
||||
/// disk, less the two rounded corners' `(1 − π/4) r²` (capsule: a second
|
||||
/// half disk instead of the flat end).
|
||||
fn analytic(tip: Option<f64>) -> (f64, f64) {
|
||||
let pi = std::f64::consts::PI;
|
||||
match tip {
|
||||
Some(r) => (
|
||||
LEN * 2.0 * HALF + 0.5 * pi * HALF * HALF - 2.0 * (1.0 - 0.25 * pi) * r * r,
|
||||
2.0 * (LEN - r) + (2.0 * HALF - 2.0 * r) + pi * r + pi * HALF,
|
||||
),
|
||||
None => (
|
||||
LEN * 2.0 * HALF + pi * HALF * HALF,
|
||||
2.0 * LEN + 2.0 * pi * HALF,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn cut(sdf: DeviceSdf, h: f64) -> (f64, f64) {
|
||||
let (nx, ny, nz) = ((0.46 / h).ceil() as usize, (0.26 / h).ceil() as usize, 2);
|
||||
let g = Grid::cubic(nx, ny, nz, h);
|
||||
let body = Body::from_sdf(move |x, y, z, _t| sdf.phi_host(x, y, z));
|
||||
let geo = CutGeometry::build(&body, g, 0.0);
|
||||
let depth = nz as f64 * h;
|
||||
let solid = (nx * ny * nz) as f64 * h * h * h - geo.fluid_volume();
|
||||
let (wall, _) = geo.wall_area_and_closure();
|
||||
(solid / depth, wall / depth)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_tip_plane_passes_through_the_last_point() {
|
||||
for theta in [0.0, 0.3] {
|
||||
for rc in [0.0, 0.00125, 0.005, HALF] {
|
||||
let s = flag(theta, Some(rc), 0.0);
|
||||
let tip = *s.poly.last().unwrap();
|
||||
let (c, sn) = (f64::cos(theta), f64::sin(theta));
|
||||
// the face centre on the surface; 1 mm beyond it at 1 mm, inside by 1 mm
|
||||
assert!(s.phi_host(tip[0], tip[1], 0.0).abs() < 1e-15);
|
||||
if rc < HALF {
|
||||
let beyond = s.phi_host(tip[0] + 1e-3 * c, tip[1] + 1e-3 * sn, 0.0);
|
||||
assert!((beyond - 1e-3).abs() < 1e-12, "{beyond}");
|
||||
}
|
||||
let inside = s.phi_host(tip[0] - 1e-3 * c, tip[1] - 1e-3 * sn, 0.0);
|
||||
assert!(inside < 0.0);
|
||||
// the corner's apex region: the rounded corner's arc
|
||||
let (px, py) = (
|
||||
tip[0] - rc * c - (HALF - rc) * sn,
|
||||
tip[1] - rc * sn + (HALF - rc) * c,
|
||||
);
|
||||
let d = 2e-3;
|
||||
let (dx, dy) = ((c - sn) / 2f64.sqrt(), (sn + c) / 2f64.sqrt());
|
||||
let got = s.phi_host(px + (rc + d) * dx, py + (rc + d) * dy, 0.0);
|
||||
assert!((got - d).abs() < 1e-12, "corner {rc}: {got} vs {d}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_tip_at_half_is_the_capsule_pulled_back() {
|
||||
let a = flag(0.2, Some(HALF), 0.0);
|
||||
let b = flag(0.2, None, HALF);
|
||||
let mut worst = 0.0f64;
|
||||
for i in 0..400 {
|
||||
for j in 0..200 {
|
||||
let (x, y) = (0.30 + 0.0003 * i as f64, 0.10 + 0.0010 * j as f64);
|
||||
let (pa, pb) = (a.phi_host(x, y, 0.0), b.phi_host(x, y, 0.0));
|
||||
if pa.abs().min(pb.abs()) < 0.02 {
|
||||
worst = worst.max((pa - pb).abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" r_c = half vs capsule pulled back by half: max |Δφ| {worst:.2e}");
|
||||
assert!(worst < 1e-12, "{worst}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_tip_cut_volume_and_area_converge_to_the_rounded_rectangle() {
|
||||
println!(" theta tip h solid err wall err");
|
||||
for theta in [0.0, 0.3] {
|
||||
for tip in [None, Some(0.0), Some(0.00125), Some(0.005)] {
|
||||
let (va, pa) = analytic(tip);
|
||||
let mut errs = Vec::new();
|
||||
for ny in [62.0, 124.0, 248.0] {
|
||||
let h = 0.41 / ny;
|
||||
let (v, p) = cut(flag(theta, tip, 0.0), h);
|
||||
let (ev, ep) = ((v - va) / va, (p - pa) / pa);
|
||||
println!(
|
||||
" {theta:.1} {:>9} {h:.3e} {ev:+.3e} {ep:+.3e}",
|
||||
tip.map_or("capsule".to_string(), |r| format!("{r:.5}"))
|
||||
);
|
||||
assert!(ev.abs() < 1e-2 && ep.abs() < 2e-2, "{ev} {ep} at h {h}");
|
||||
errs.push((ev, ep));
|
||||
}
|
||||
// the volume converges (the rung ny 248 below ny 62's error)
|
||||
assert!(errs[2].0.abs() < errs[0].0.abs(), "volume {errs:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user