Files
rustytorch/crates/specialized/rtx-fsi/tests/fsi2_embedded3_impulse.rs
T
Omar SobhandClaude Opus 5.5 5572fd12fb
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 (macos-latest) (push) Waiting to run
CI / Format Check (push) Failing after 4s
CI / Build CPU-Only (Explicit) (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
CI / Build (ubuntu-latest) (push) Failing after 11s
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Clippy Check (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 20s
Merge r8e-coupled-dt-stability (R8-e: the coupled 3D FSI dt instability = the harness's structure-to-fluid mapping, fix RTX_E3FSI_KIN=surface; default-off). Merge fixes with R8-h: the surface station list over stations() (the flat tip's station A = the mean of the tip corners); the impulse test's capsule-only flat_tip(). Merged-tree gate (r6/merge_gate7): slab default / check / all-host / plate byte-identical to main's base, suites 2/2 3/3, the default coupled smoke = R8-a's march row for row, a KIN=surface + TIP=flat + LOADS_DEVICE coupled run clean
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 05:02:51 -05:00

227 lines
8.0 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.
//! R8-e: the added-mass timing instrument for the coupled 3D FSI's small-dt
//! instability. The R8-a fluid (the embedded3 cut-cell device path, the
//! slab by default) with the flag's centreline PRESCRIBED: rigid to `T0`,
//! then from one snapshot two passes of `N` steps each — the baseline (the
//! flag at rest) and the impulse (the centreline's velocity steps from 0 to
//! `V` × shape at step `N_PRE`: its wall acceleration is a single-step
//! delta V/dt; `MODE=pulse` returns the velocity to 0 one step later, a
//! +V/dt, −V/dt pair). The difference of the two passes' loads, step by
//! step, is the fluid's response to the impulse: an added-mass response
//! lives in the impulse's own step; a lagged part shows one step later.
//!
//! Knobs `RTX_E3IMP_*`: `NY` (62), `NZ` (4), `T0` (0.2 s), `N_PRE` (4),
//! `N` (24), `V` (0.05 m/s at the tip), `SHAPE` (`pitch` about the root,
//! 1 at A; `heave`; `zig`, `tip`), `MODE` (`step`, `pulse`), `CSV` (per-step series).
//! The fluid's own knobs as the R8-a harness (`RTX_E3FSI_DT_SCALE`, …).
#![cfg(feature = "cuda")]
#[path = "fsi2_embedded3/fluid.rs"]
mod fluid;
#[path = "fsi2_embedded3/state.rs"]
mod state;
use std::io::Write as _;
use fluid::{CX, CY, Contribution, E3Fluid, HALF, Line, R_CYL};
pub fn env_f(name: &str, default: f64) -> f64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
const X0: f64 = 0.25;
const STATIONS: usize = 35;
/// The shared fluid module asks its parent for the tip shape (R8-h); the
/// impulse instrument keeps the capsule on its 35-station centreline.
pub fn flat_tip() -> Option<f64> {
None
}
const CHORD: f64 = 0.35;
fn line(t: f64, c: &[f64], v: &[f64]) -> Line {
Line {
t,
pts: (0..STATIONS)
.map(|k| [X0 + 0.01 * k as f64, 0.2 + c[k]])
.collect(),
vel: (0..STATIONS).map(|k| [0.0, v[k]]).collect(),
}
}
/// The flag's loads from the route's contributions (per unit span): fy on
/// the flag, fy by part, and the generalized force Σ fy φ(x) with the
/// prescribed shape's φ.
fn flag_loads(
contrib: &[Contribution],
pts: &[[f64; 2]],
width: f64,
phi: impl Fn(f64) -> f64,
) -> (f64, f64, [f64; 4]) {
let (mut fy, mut q, mut parts) = (0.0, 0.0, [0.0; 4]);
for &(pos, c, part, v) in contrib {
if c != 1 {
continue;
}
let (x, y) = (pos[0], pos[1]);
let mut best = f64::INFINITY;
for m in 0..pts.len() - 1 {
let (a, b) = (pts[m], pts[m + 1]);
let (ex, ey) = (b[0] - a[0], b[1] - a[1]);
let u = (((x - a[0]) * ex + (y - a[1]) * ey) / (ex * ex + ey * ey)).clamp(0.0, 1.0);
best = best.min(((x - a[0] - u * ex).powi(2) + (y - a[1] - u * ey).powi(2)).sqrt());
}
let d_cyl = ((x - CX).powi(2) + (y - CY).powi(2)).sqrt() - R_CYL;
if d_cyl < best - HALF {
continue;
}
let v = v / width;
fy += v;
q += v * phi(x);
parts[part] += v * phi(x);
}
(fy, q, parts)
}
#[test]
#[ignore = "R8-e: the added-mass timing instrument (GPU, a minute)"]
fn impulse_response() {
let ny = env_f("RTX_E3IMP_NY", 62.0) as usize;
let nz = env_f("RTX_E3IMP_NZ", 4.0) as usize;
let t0 = env_f("RTX_E3IMP_T0", 0.2);
let n_pre = env_f("RTX_E3IMP_N_PRE", 4.0) as usize;
let n = env_f("RTX_E3IMP_N", 24.0) as usize;
let vel = env_f("RTX_E3IMP_V", 0.05);
let shape = std::env::var("RTX_E3IMP_SHAPE").unwrap_or_else(|_| "pitch".into());
let mode = std::env::var("RTX_E3IMP_MODE").unwrap_or_else(|_| "step".into());
// The shape per station (`zig`: alternate stations ±, the root clamped;
// `tip`: the last 5 stations), linear between stations.
let phis: Vec<f64> = (0..STATIONS)
.map(|k| {
let x = X0 + 0.01 * k as f64;
match shape.as_str() {
"heave" => 1.0,
"zig" => {
if k == 0 {
0.0
} else if k % 2 == 0 {
1.0
} else {
-1.0
}
}
"tip" => {
if k + 5 >= STATIONS {
1.0
} else {
0.0
}
}
_ => ((x - X0) / CHORD).clamp(0.0, 1.0),
}
})
.collect();
let phi = |x: f64| -> f64 {
let s = ((x - X0) / 0.01).clamp(0.0, (STATIONS - 1) as f64);
let m = (s.floor() as usize).min(STATIONS - 2);
let u = s - m as f64;
(1.0 - u) * phis[m] + u * phis[m + 1]
};
let zero = vec![0.0; STATIONS];
let rest = line(0.0, &zero, &zero);
let mut fl = E3Fluid::build(ny, nz, 3.0, rest.clone(), None);
let dt = fl.dt;
let rigid = (t0 / dt).round() as usize;
for s in 0..rigid {
let r = fl.step();
assert!(r.final_residual.is_finite(), "rigid death at {s}");
}
let t_start = fl.time();
println!(
" R8-e impulse: ny {ny} nz {nz} dt {dt:.4e}, rigid {rigid} steps to t {t_start:.4}; shape {shape}, mode {mode}, V {vel} m/s (acceleration V/dt {:.2} m/s²) at step {n_pre} of {n}",
vel / dt
);
let snap = fl.snapshot();
// Per pass: per step (t, tip velocity, fy, Q, Q parts, lift total).
let mut rows: Vec<Vec<[f64; 9]>> = Vec::new();
for pass in 0..2 {
fl.restore(&snap);
let mut c = zero.clone();
let mut prev = rest.clone();
prev.t = t_start;
let mut out = Vec::new();
for s in 0..n {
let t_new = t_start + (s + 1) as f64 * dt;
// The step's centreline velocity (constant over the step).
let v_tip = if pass == 0 || s < n_pre {
0.0
} else if mode == "pulse" {
if s == n_pre { vel } else { 0.0 }
} else {
vel
};
let v: Vec<f64> = phis.iter().map(|p| p * v_tip).collect();
for k in 0..STATIONS {
c[k] += dt * v[k];
}
let next = line(t_new, &c, &v);
fl.set_lines(prev.clone(), next.clone());
let r = fl.step();
assert!(
r.final_residual.is_finite(),
"death at pass {pass} step {s}"
);
let (tot, contrib) = fl.loads();
let (fy, q, parts) = flag_loads(&contrib, &next.pts, fl.load_width, &phi);
out.push([
t_new, v_tip, fy, q, parts[0], parts[1], parts[2], parts[3], tot[1],
]);
prev = next;
}
rows.push(out);
}
let mut csv = std::env::var("RTX_E3IMP_CSV")
.ok()
.map(|p| std::fs::File::create(p).expect("csv"));
if let Some(f) = csv.as_mut() {
writeln!(
f,
"k,t,v_tip,fy0,q0,fy,q,dfy,dq,dq_p,dq_shear,dq_xdiff,dq_xconv,dlift"
)
.unwrap();
}
let q_imp = rows[1][n_pre][3] - rows[0][n_pre][3];
for s in 0..n {
let (a, b) = (&rows[0][s], &rows[1][s]);
let k = s as i64 - n_pre as i64;
let d = |i: usize| b[i] - a[i];
if let Some(f) = csv.as_mut() {
writeln!(
f,
"{k},{:.6},{:.4e},{:.6},{:.6},{:.6},{:.6},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e}",
b[0], b[1], a[2], a[3], b[2], b[3], d(2), d(3), d(4), d(5), d(6), d(7), d(8)
)
.unwrap();
}
if (-1..=6).contains(&k) {
println!(
" k {k:+}: v_tip {:.3e} ΔQ {:+.5e} ({:+.4} of the impulse step) parts p {:+.4e} shear {:+.4e} xdiff {:+.4e} xconv {:+.4e} Δfy {:+.5e}",
b[1],
d(3),
d(3) / q_imp,
d(4),
d(5),
d(6),
d(7),
d(2)
);
}
}
println!(
" IMPULSE ΔQ(k=0) {q_imp:+.5e} N/m → generalized added mass −ΔQ·dt/V {:.4} kg/m",
-q_imp * dt / vel
);
}