Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_flag_reference_2d.rs
T
Omar SobhandClaude Fable 5.1 d9239961ad
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 / Format Check (push) Failing after 4s
CI / Build (ubuntu-latest) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 5s
CI / Build CPU-Only (Explicit) (push) Failing after 1m27s
CI / Clippy Check (push) Failing after 1m46s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m10s
S2-9c: embedded3_flag_reference_2d takes RTX_E3_FLAG_NY (default 62), its dt scaling with h so the reference and the slab march the same step at every rung
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-21 02:24:33 -05:00

267 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! S2-3c: the 2D solver on the flag wake's exact kinematics — the same
//! cylinder + capsule flag, the same first-mode motion (tip 84 mm at
//! 1.930 Hz), the same inflow mean (Ū 1.0, the 2D parabola), TVD, on the
//! ny 62 spacing (378 × 62, h 6.6 mm). Its loads are the 2D answer for
//! this kinematics; the 3D full-span run must reproduce them. Loads by
//! the traction sampler over the flag's and the cylinder's surface
//! samples per unit span, every 10 steps; the last period's mean drag and
//! median-filtered lift swing.
//!
//! `cargo test --release -p rtx-cfd --test embedded3_flag_reference_2d -- --ignored --nocapture`
mod embedded3_flag_kinematics;
use embedded3_flag_kinematics::{Recorded, recorded};
use rtx_cfd::solvers::incompressible::{
AleBoundaries, ConvectionScheme, EmbeddedBody, EmbeddedParameters, EmbeddedPisoSolver,
FlowField, MgPrecision, MgSmoother, PoissonSolverKind, SideBoundary,
};
use rtx_cfd::{CfdConfig, CfdResult};
const H: f64 = 0.41;
const L: f64 = 2.5;
const CX: f64 = 0.2;
const CY: f64 = 0.2;
const R_CYL: f64 = 0.05;
/// The flag's ROOT: the cylinder's rear (the TurekHron flag runs from the
/// cylinder to its tip A at x = 0.6). Until 2026-09-18 this was 0.6 — the
/// flag sat DETACHED, its root where the benchmark's tip is; every flag
/// record before that date is of that geometry.
const FLAG_X0: f64 = 0.25;
const FLAG_LEN: f64 = 0.35;
const FLAG_HALF: f64 = 0.01;
const AMP: f64 = 0.084;
const FREQ: f64 = 1.930;
const U_MEAN: f64 = 1.0;
const RHO: f64 = 1000.0;
const NU: f64 = 1e-3;
const BETA_L: f64 = 1.875_104_069;
fn mode(s: f64) -> f64 {
let b = BETA_L;
let sigma = (b.sinh() - b.sin()) / (b.cosh() + b.cos());
let phi = |s: f64| (b * s).cosh() - (b * s).cos() - sigma * ((b * s).sinh() - (b * s).sin());
phi(s) / phi(1.0)
}
fn deflection(s: f64, t: f64) -> (f64, f64) {
let w = 2.0 * std::f64::consts::PI * FREQ;
(
AMP * mode(s) * (w * t).sin(),
AMP * mode(s) * w * (w * t).cos(),
)
}
/// The centreline point `m` of `n` at `t`: (x, y, vx, vy) — the first
/// mode, or (S2-9) the recorded FSI2 kinematics along its stations.
fn centreline(m: usize, n: usize, t: f64) -> (f64, f64, f64, f64) {
let s = m as f64 / n as f64;
if let Some(rec) = recorded() {
thread_local! {
static PTS: std::cell::RefCell<(f64, Vec<(f64, f64, f64, f64)>)> =
const { std::cell::RefCell::new((f64::NAN, Vec::new())) };
}
return PTS.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;
}
rec.along(&c.1, s)
});
}
let (d, v) = deflection(s, t);
(FLAG_X0 + s * FLAG_LEN, CY + d, 0.0, v)
}
/// Distance to the capsule flag and the centreline velocity at the foot.
fn flag_sdf(x: f64, y: f64, t: f64) -> (f64, (f64, f64)) {
let n = 40;
let mut best = f64::INFINITY;
let mut v_best = (0.0, 0.0);
for m in 0..n {
let (ax, ay, avx, avy) = centreline(m, n, t);
let (bx, by, bvx, bvy) = centreline(m + 1, n, t);
let (ex, ey) = (bx - ax, by - ay);
let u = (((x - ax) * ex + (y - ay) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0);
let d = ((x - ax - u * ex).powi(2) + (y - ay - u * ey).powi(2)).sqrt();
if d < best {
best = d;
v_best = (avx + u * (bvx - avx), avy + u * (bvy - avy));
}
}
(best - FLAG_HALF, v_best)
}
fn cyl_sdf(x: f64, y: f64) -> f64 {
((x - CX).powi(2) + (y - CY).powi(2)).sqrt() - R_CYL
}
/// Surface samples `(x, y, nx, ny, ds)` at `t`: the flag's two sides and
/// tip along the deflected centreline, the cylinder's circle; samples
/// inside the other body are dropped.
fn samples(t: f64, ds: f64) -> Vec<(f64, f64, f64, f64, f64)> {
let mut out = Vec::new();
let n = ((FLAG_LEN / ds).ceil() as usize).max(8);
for m in 0..n {
let (ax, ay, _, _) = centreline(m, n, t);
let (bx, by, _, _) = centreline(m + 1, n, t);
let (ex, ey) = (bx - ax, by - ay);
let len = (ex * ex + ey * ey).sqrt();
let (tx, ty) = (ex / len, ey / len);
let (nx, ny) = (-ty, tx);
let (mx, my) = (0.5 * (ax + bx), 0.5 * (ay + by));
for sign in [1.0, -1.0] {
let (px, py) = (mx + sign * FLAG_HALF * nx, my + sign * FLAG_HALF * ny);
if cyl_sdf(px, py) > 0.0 {
out.push((px, py, sign * nx, sign * ny, len));
}
}
}
// The tip: a semicircle around the last centreline point.
let (tx0, ty0, _, _) = centreline(n, n, t);
let (px, py, _, _) = centreline(n - 1, n, t);
let ang0 = (ty0 - py).atan2(tx0 - px);
let n_arc = ((std::f64::consts::PI * FLAG_HALF / ds).ceil() as usize).max(4);
for k in 0..n_arc {
let a = ang0 - std::f64::consts::FRAC_PI_2
+ (k as f64 + 0.5) / n_arc as f64 * std::f64::consts::PI;
out.push((
tx0 + FLAG_HALF * a.cos(),
ty0 + FLAG_HALF * a.sin(),
a.cos(),
a.sin(),
std::f64::consts::PI * FLAG_HALF / n_arc as f64,
));
}
let n_c = ((2.0 * std::f64::consts::PI * R_CYL / ds).ceil() as usize).max(16);
for k in 0..n_c {
let a = (k as f64 + 0.5) / n_c as f64 * 2.0 * std::f64::consts::PI;
let (px, py) = (CX + R_CYL * a.cos(), CY + R_CYL * a.sin());
if flag_sdf(px, py, t).0 > 0.0 {
out.push((
px,
py,
a.cos(),
a.sin(),
2.0 * std::f64::consts::PI * R_CYL / n_c as f64,
));
}
}
out
}
fn median(v: &[f64]) -> f64 {
let mut s = v.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
s[s.len() / 2]
}
#[tokio::test]
#[ignore = "S2-3c: the 2D reference for the flag wake's kinematics (minutes on the host)"]
async fn flag_wake_2d_reference() -> CfdResult<()> {
// S2-9c: `RTX_E3_FLAG_NY` (default 62); the step scales with h so the
// reference and the slab march the same dt at every rung.
let env_f = |k: &str, d: f64| std::env::var(k).ok().and_then(|v| v.parse().ok()).unwrap_or(d);
let ny = env_f("RTX_E3_FLAG_NY", 62.0) as usize;
let h = H / ny as f64;
let nx = (L / h).round() as usize;
let dt = 8.817e-4 * 62.0 / ny as f64;
// S2-9: the record's period and the requested number of periods.
let period = if recorded().is_some() { env_f("RTX_E3_FLAG_KIN_PERIOD", 0.5225) } else { 1.0 / FREQ };
let t_end = env_f("RTX_E3_FLAG_PERIODS", 2.0) * period;
let config = CfdConfig::new()
.with_density(RHO)
.with_viscosity(RHO * NU)
.with_reference_velocity(U_MEAN)
.with_reference_length(2.0 * R_CYL);
let mut solver = EmbeddedPisoSolver::new(
config,
EmbeddedParameters {
corrector_steps: 3,
tolerance: 1e-8,
boundaries: AleBoundaries {
right: SideBoundary::PressureOutlet,
..AleBoundaries::default()
},
poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: MgPrecision::F64,
poisson_smoother: MgSmoother::Lexicographic,
convection_scheme: ConvectionScheme::TvdVanAlbada,
},
)?;
solver.set_boundary_velocity(|x, y, _t| {
if x <= 0.0 {
(1.5 * U_MEAN * y * (H - y) / (0.5 * H).powi(2), 0.0)
} else {
(0.0, 0.0)
}
});
let flag = EmbeddedBody::from_sdf(|x, y, t| flag_sdf(x, y, t).0)
.with_surface_velocity(|x, y, t| flag_sdf(x, y, t).1);
solver.set_moving_body(EmbeddedBody::union(
EmbeddedBody::circle(CX, CY, R_CYL),
flag,
));
let mut field = FlowField::new(nx, ny, h, h)?;
for j in 0..ny {
let u0 =
1.5 * U_MEAN * ((j as f64 + 0.5) * h) * (H - (j as f64 + 0.5) * h) / (0.5 * H).powi(2);
for i in 0..=nx {
field.u[(j, i)] = u0;
}
}
solver.initialize(&mut field)?;
let steps = (t_end / dt).ceil() as usize;
println!(" 2D reference: {nx}×{ny}, h {h:.4e}, dt {dt:.3e}, {steps} steps");
let mu = RHO * NU;
let start = std::time::Instant::now();
let mut drag = Vec::new();
let mut lift = Vec::new();
let mut worst = 0.0_f64;
for step in 0..steps {
let r = solver.advance(&mut field, dt).await?;
worst = worst.max(r.solver_result.final_residual);
let t = (step + 1) as f64 * dt;
if (step + 1) % 10 == 0 {
let mask = solver.mask().expect("mask");
let body = solver.body().expect("body");
let (mut fx, mut fy, mut skipped) = (0.0, 0.0, 0usize);
for (x, y, nx_, ny_, ds) in samples(t, 0.5 * h) {
match mask.traction_at(body, &field.u, &field.v, &field.p, mu, t, x, y, nx_, ny_) {
Some((tx, ty)) => {
fx += tx * ds;
fy += ty * ds;
}
None => skipped += 1,
}
}
if t >= t_end - period {
drag.push(fx);
lift.push(fy);
}
if (step + 1) % 100 == 0 {
println!(
" t {t:6.3} tip {:+.4}: drag {fx:7.1} lift {fy:+8.1} N/m (skipped {skipped}); residual {:.1e}; [{:.0} s]",
deflection(1.0, t).0,
r.solver_result.final_residual,
start.elapsed().as_secs_f64()
);
}
}
}
let filt: Vec<f64> = (0..lift.len())
.map(|i| median(&lift[i.saturating_sub(5)..(i + 6).min(lift.len())]))
.collect();
let lo = filt.iter().cloned().fold(f64::INFINITY, f64::min);
let hi = filt.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
println!(
" FINAL 2D reference: last period drag mean {:.1} N/m, lift swing {:+.1}{:+.1} (raw {:+.1}{:+.1}); worst residual {worst:.1e}; {:.0} s",
drag.iter().sum::<f64>() / drag.len() as f64,
lo,
hi,
lift.iter().cloned().fold(f64::INFINITY, f64::min),
lift.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
start.elapsed().as_secs_f64()
);
Ok(())
}