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
@@ -0,0 +1,113 @@
//! S2-9: the recorded FSI2 kinematics for the flag tests (`RTX_E3_FLAG_KINEMATICS=<csv>`
//! from `fsi2_overset_dump_centreline`): per knot t and per station (x, y, vx, vy);
//! between knots the cubic Hermite interpolant through the knots with their
//! velocities (the overset replay's form). The run's t = 0 maps to the recorded
//! `RTX_E3_FLAG_KIN_T0` (13.0 s, inside the limit cycle) and the motion ramps in
//! over `RTX_E3_FLAG_KIN_RAMP` (0.5 s) from the undeflected line at `cy`.
#![allow(dead_code)]
fn env_f(name: &str, default: f64) -> f64 {
std::env::var(name).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}
pub struct Recorded {
pub times: Vec<f64>,
/// Per knot: per station (x, y, vx, vy).
pub knots: Vec<Vec<(f64, f64, f64, f64)>>,
pub t0: f64,
pub ramp: f64,
}
pub fn recorded() -> Option<&'static Recorded> {
static REC: std::sync::OnceLock<Option<Recorded>> = std::sync::OnceLock::new();
REC.get_or_init(|| {
let path = std::env::var("RTX_E3_FLAG_KINEMATICS").ok()?;
let text = std::fs::read_to_string(&path).expect("kinematics csv");
let mut times = Vec::new();
let mut knots = Vec::new();
for line in text.lines().skip(1) {
let v: Vec<f64> = line.split(',').map(|x| x.trim().parse().expect("number")).collect();
times.push(v[0]);
knots.push(v[1..].chunks_exact(4).map(|c| (c[0], c[1], c[2], c[3])).collect());
}
assert!(times.len() >= 2, "kinematics: at least two knots");
Some(Recorded {
times,
knots,
t0: env_f("RTX_E3_FLAG_KIN_T0", 13.0),
ramp: env_f("RTX_E3_FLAG_KIN_RAMP", 0.5),
})
})
.as_ref()
}
impl Recorded {
/// The largest recorded station speed (the CFL and band bound).
pub fn max_speed(&self) -> f64 {
self.knots
.iter()
.flat_map(|k| k.iter().map(|p| (p.2 * p.2 + p.3 * p.3).sqrt()))
.fold(0.0f64, f64::max)
}
/// The stations at the run's time `t`: (x, y, vx, vy), ramped from the
/// undeflected line at `cy` (each station at its first-knot x).
pub fn at(&self, t: f64, cy: f64) -> Vec<(f64, f64, f64, f64)> {
let tr = (self.t0 + t).min(*self.times.last().unwrap());
let k = match self.times.binary_search_by(|x| x.partial_cmp(&tr).unwrap()) {
Ok(i) => i.min(self.times.len() - 2),
Err(i) => i.saturating_sub(1).min(self.times.len() - 2),
};
let h = self.times[k + 1] - self.times[k];
let s = ((tr - self.times[k]) / h).clamp(0.0, 1.0);
let (s2, s3) = (s * s, s * s * s);
let (h00, h10, h01, h11) = (2.0 * s3 - 3.0 * s2 + 1.0, s3 - 2.0 * s2 + s, -2.0 * s3 + 3.0 * s2, s3 - s2);
let (d00, d10, d01, d11) = (
(6.0 * s2 - 6.0 * s) / h,
(3.0 * s2 - 4.0 * s + 1.0) / h,
(-6.0 * s2 + 6.0 * s) / h,
(3.0 * s2 - 2.0 * s) / h,
);
let (r, rd) = if t >= self.ramp {
(1.0, 0.0)
} else {
let u = t / self.ramp;
(u * u * (3.0 - 2.0 * u), 6.0 * u * (1.0 - u) / self.ramp)
};
let (a, b) = (&self.knots[k], &self.knots[k + 1]);
let first = &self.knots[0];
a.iter()
.zip(b)
.zip(first)
.map(|((p, q), f0)| {
let x = h00 * p.0 + h10 * h * p.2 + h01 * q.0 + h11 * h * q.2;
let y = h00 * p.1 + h10 * h * p.3 + h01 * q.1 + h11 * h * q.3;
let vx = d00 * p.0 + d10 * h * p.2 + d01 * q.0 + d11 * h * q.2;
let vy = d00 * p.1 + d10 * h * p.3 + d01 * q.1 + d11 * h * q.3;
let (xr, yr) = (f0.0, cy);
(
(1.0 - r) * xr + r * x,
(1.0 - r) * yr + r * y,
r * vx + rd * (x - xr),
r * vy + rd * (y - yr),
)
})
.collect()
}
/// The point and velocity at arc fraction `s ∈ [0, 1]` along the
/// stations' polyline at `t` (linear between stations by arc length).
pub fn along(&self, pts: &[(f64, f64, f64, f64)], s: f64) -> (f64, f64, f64, f64) {
let mut cum = vec![0.0; pts.len()];
for m in 1..pts.len() {
let (dx, dy) = (pts[m].0 - pts[m - 1].0, pts[m].1 - pts[m - 1].1);
cum[m] = cum[m - 1] + (dx * dx + dy * dy).sqrt();
}
let target = s.clamp(0.0, 1.0) * cum[pts.len() - 1];
let m = cum.partition_point(|&c| c < target).clamp(1, pts.len() - 1);
let seg = cum[m] - cum[m - 1];
let u = if seg > 0.0 { ((target - cum[m - 1]) / seg).clamp(0.0, 1.0) } else { 0.0 };
let (a, b) = (pts[m - 1], pts[m]);
(a.0 + u * (b.0 - a.0), a.1 + u * (b.1 - a.1), a.2 + u * (b.2 - a.2), a.3 + u * (b.3 - a.3))
}
}