S2-9: the flag on the recorded FSI2 kinematics — fsi2_overset_dump_centreline (rtx-fsi) exports the saved instants' wetted-node d/ḋ as a centreline per knot (clamp + 70 columns); tests/embedded3_flag_kinematics (cubic Hermite through the knots with their velocities, t = 0 at RTX_E3_FLAG_KIN_T0, a 0.5 s ramp from the undeflected line, arc-length interpolation along the stations) drives both the 3D/slab flag (RTX_E3_FLAG_KINEMATICS, both in-plane surface-velocity components, the record's period) and the 2D reference; the analytic paths unchanged (same tuples)
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 CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 6s
CI / Format Check (push) Failing after 13s
CI / Build (ubuntu-latest) (push) Failing after 1m56s
CI / Clippy Check (push) Failing after 2m12s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m54s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-20 21:27:26 -05:00
co-authored by Claude Fable 5.1
parent 03a9c9686e
commit f1714fb926
4 changed files with 268 additions and 22 deletions
@@ -19,6 +19,8 @@
//! RTX_CUDA_ARCH=sm_120 cargo test --release -p rtx-cfd --features cuda --test embedded3_flag_wake -- --ignored --nocapture`
#![cfg(feature = "cuda")]
mod embedded3_flag_kinematics;
use embedded3_flag_kinematics::{Recorded, recorded};
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::step::device::DeviceStep;
use rtx_cfd::solvers::incompressible::embedded3::{
@@ -80,8 +82,48 @@ fn amplitude() -> f64 {
/// Signed distance to the deflected flag's cross-section (a capsule
/// around the centreline polyline of `n` segments) and the centreline's
/// transverse velocity at the closest point.
fn flag_2d(x: f64, y: f64, t: f64) -> (f64, f64) {
/// velocity at the closest point (transverse only in the analytic mode).
fn flag_2d(x: f64, y: f64, t: f64) -> (f64, (f64, f64)) {
if let Some(rec) = recorded() {
return flag_2d_recorded(rec, x, y, t);
}
let (d, v) = flag_2d_analytic(x, y, t);
(d, (0.0, v))
}
/// The recorded centreline's capsule and velocity at the closest point.
fn flag_2d_recorded(rec: &Recorded, x: f64, y: f64, t: f64) -> (f64, (f64, f64)) {
thread_local! {
static POLY: std::cell::RefCell<(f64, Vec<(f64, f64, f64, f64)>)> =
const { std::cell::RefCell::new((f64::NAN, Vec::new())) };
}
POLY.with(|cell| {
let mut c = cell.borrow_mut();
if c.0.to_bits() != t.to_bits() {
c.1 = rec.at(t, CY);
c.0 = t;
}
let pts = &c.1;
let mut best = f64::INFINITY;
let mut v_best = (0.0, 0.0);
for m in 0..pts.len() - 1 {
let (ax, ay, avx, avy) = pts[m];
let (bx, by, bvx, bvy) = pts[m + 1];
let (ex, ey) = (bx - ax, by - ay);
let l2 = ex * ex + ey * ey;
let u = (((x - ax) * ex + (y - ay) * ey) / l2).clamp(0.0, 1.0);
let (px, py) = (ax + u * ex, ay + u * ey);
let d = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
if d < best {
best = d;
v_best = (avx + u * (bvx - avx), avy + u * (bvy - avy));
}
}
(best - FLAG_HALF, v_best)
})
}
fn flag_2d_analytic(x: f64, y: f64, t: f64) -> (f64, f64) {
const N: usize = 40;
// The centreline polyline at `t`, once per thread and time (PERF-3
// P1-2): the solver asks for the surface velocity at ~10⁶ faces per
@@ -130,7 +172,7 @@ fn flag_span() -> f64 {
/// The flag in 3D: the extruded capsule cut to the span with edges
/// rounded to radius `r` (no cut at the full width).
fn flag_3d(x: f64, y: f64, z: f64, t: f64, r: f64) -> (f64, f64) {
fn flag_3d(x: f64, y: f64, z: f64, t: f64, r: f64) -> (f64, (f64, f64)) {
let (d2, v) = flag_2d(x, y, t);
let span = flag_span();
if span >= H {
@@ -159,10 +201,13 @@ fn flag_wake_on_the_device() {
let slab_nz = env_f("RTX_E3_FLAG_NZ", 0.0) as usize;
let nz = if slab_nz > 0 { slab_nz } else { ny };
let r_edge = h;
let dt_cfl = 0.3 * h / (U_M.max(2.0 * std::f64::consts::PI * FREQ * AMP));
// S2-9: with a recorded kinematics the period and the speed bound are the record's.
let rec_period = env_f("RTX_E3_FLAG_KIN_PERIOD", 0.5225);
let rec_speed = recorded().map(Recorded::max_speed);
let dt_cfl = 0.3 * h / (U_M.max(rec_speed.unwrap_or(2.0 * std::f64::consts::PI * FREQ * AMP)));
// `RTX_E3_FLAG_DT_SCALE` scales the step (the dt ladder of the loads).
let dt = dt_cfl.min(0.5 * h * h / (6.0 * NU)) * env_f("RTX_E3_FLAG_DT_SCALE", 1.0);
let period = 1.0 / FREQ;
let period = if recorded().is_some() { rec_period } else { 1.0 / FREQ };
let t_end = periods * period;
let mut solver = Solver::new(
Fluid {
@@ -196,7 +241,7 @@ fn flag_wake_on_the_device() {
},
// The narrow band: the flag's tip speed bounds the surface motion.
max_surface_speed: Some(
(2.0 * std::f64::consts::PI * FREQ * amplitude() * 1.05).max(1e-3),
(rec_speed.unwrap_or(2.0 * std::f64::consts::PI * FREQ * amplitude()) * 1.05).max(1e-3),
),
..Parameters::default()
},
@@ -218,9 +263,9 @@ fn flag_wake_on_the_device() {
let cyl = move |x: f64, y: f64| ((x - CX).powi(2) + (y - CY).powi(2)).sqrt() - R_CYL;
let body = Body::from_sdf(move |x, y, z, t| cyl(x, y).min(flag_3d(x, y, z, t, r_edge).0))
.with_surface_velocity(move |x, y, z, t| {
let (df, v) = flag_3d(x, y, z, t, r_edge);
let (df, (vx, vy)) = flag_3d(x, y, z, t, r_edge);
if df <= cyl(x, y) {
(0.0, v, 0.0)
(vx, vy, 0.0)
} else {
(0.0, 0.0, 0.0)
}