Files
rustytorch/crates/specialized/rtx-fsi/tests/fsi2_b4_added_mass.rs
T
Omar SobhandClaude Opus 5.5 ec0e4f1573 b4am: prescribed b4 motion on the overset (modal added mass / fluid damping instrument)
- tests/fsi2_b4_added_mass.rs: #[ignore] instrument fsi2_b4_prescribed_mode. The flag's wetted
  surface moves as q(t) phi (phi from B4_MODE, M-orthonormal in-vacuo b4), q = q0 r(t) sin(w t);
  per step the CSV records q, qd, qdd and the generalised fluid force sum_k f_k . phi_k from the
  march's own nodal load (sample_load); optional per-node load CSV. B4_DT overrides the fluid step.
- fsi2_harness/overset.rs: RTX_FSI2O_STILL=1 turns the inflow off (still fluid) while keeping
  u_mean for the CFL step and the solvers' reference flux. Unset = unchanged; gate: a short
  prescribed replay (fsi2_overset_prescribed_motion, ny 62, 185 steps) byte-identical to main.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 10:23:21 -05:00

202 lines
7.6 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.
//! The FSI2 flag's 4th bending mode b4 as a PRESCRIBED motion on the
//! overset fluid (track 1 round 5, `b4am`): the fluid-side test of the
//! round-4 verdict that FSI2's 5f lift excess is the wet b4 resonating
//! near 5f = 9.67 Hz. The flag's wetted surface moves as
//! `d(t) = q(t) φ` with `φ` the in-vacuo b4 shape at the wetted DoFs
//! (M-orthonormal, from `fsi2_flag_modes`' operators) and
//! `q(t) = q0 r(t) sin(ω (t − t0))` (`r` a smooth start ramp); per fluid
//! step the CSV records `q`, `q̇`, `q̈` and the generalised fluid force
//! `F = Σ_k f_k · φ_k` over the wetted nodes (the march's own nodal load,
//! `sample_load`), from which the analysis fits the part in phase with
//! the acceleration (the modal added mass) and with the velocity (the
//! modal fluid damping).
//!
//! `#[ignore]`d instrument; test-only code, nothing in the solver or the
//! march changes. Knobs:
//!
//! * `B4_MODE` (required): lines `node_id φx φy` for the wetted nodes
//! (other nodes are ignored; missing wetted nodes are zero).
//! * `B4_Q0` modal amplitude (1e-3), `B4_FREQ` Hz (9.67), `B4_PERIODS`
//! (10), `B4_RAMP_PERIODS` (2).
//! * `B4_DT`: override the fluid step (to run still fluid at the mean
//! flow's step; unset = the case's CFL step).
//! * `RTX_FSI2O_LOAD` (optional): the rigid state to start from (the mean
//! flow); unset = start from rest (with `RTX_FSI2O_UMEAN=0`: still fluid).
//! * `B4_CSV` (required), `B4_NODE_CSV` (optional: t + fx, fy per wetted
//! node, for projections onto other modes).
//! * `RTX_FSI2O_NY` (62), and every `RTX_FSI2O_*` fluid knob the overset
//! builder reads (tip corner, warm sweeps, red-black, …).
mod fsi2_harness;
use std::io::Write as _;
use fsi2_harness::overset::OversetFluid;
use fsi2_harness::replay::ramp;
use fsi2_harness::{FSI2, case_from_env};
fn env_f(k: &str, d: f64) -> f64 {
std::env::var(k)
.ok()
.map(|v| v.parse().unwrap_or_else(|_| panic!("{k}")))
.unwrap_or(d)
}
#[test]
#[ignore = "instrument: prescribed b4 motion on the overset (needs B4_MODE, B4_CSV)"]
fn fsi2_b4_prescribed_mode() {
let mode_path = std::env::var("B4_MODE").expect("B4_MODE");
let csv_path = std::env::var("B4_CSV").expect("B4_CSV");
let ny = env_f("RTX_FSI2O_NY", 62.0) as usize;
let q0 = env_f("B4_Q0", 1e-3);
let freq = env_f("B4_FREQ", 9.67);
let periods = env_f("B4_PERIODS", 10.0);
let ramp_periods = env_f("B4_RAMP_PERIODS", 2.0);
let max_rounds = env_f("RTX_FSI2O_MAX_ROUNDS", 3.0) as usize;
let case = case_from_env("FSI2O", FSI2);
let mut fluid = OversetFluid::build_case(case, ny, 35, 100, max_rounds).expect("overset fluid");
if let Ok(v) = std::env::var("B4_DT") {
let dt: f64 = v.parse().expect("B4_DT");
println!(
" fluid step OVERRIDDEN {:.6e} → {dt:.6e} (B4_DT)",
fluid.dt_fluid
);
fluid.dt_fluid = dt;
}
let t0 = match std::env::var("RTX_FSI2O_LOAD") {
Ok(dir) => fluid.load(&dir).expect("load"),
Err(_) => {
println!(
" no RTX_FSI2O_LOAD: starting from rest (u_mean {})",
fluid.case.u_mean
);
0.0
}
};
fluid.commit_base();
fluid.solver.set_time(t0);
// φ at the wetted DoFs, in the interface's order.
let wetted = fluid.interface.wetted.clone();
let n = 2 * wetted.len();
let mut phi = vec![0.0; n];
let mut matched = 0usize;
for line in std::fs::read_to_string(&mode_path)
.expect("B4_MODE")
.lines()
{
let f: Vec<f64> = line
.split_whitespace()
.filter_map(|t| t.parse().ok())
.collect();
if f.len() < 3 {
continue;
}
if let Some(k) = wetted.iter().position(|id| id.0 == f[0] as usize) {
phi[2 * k] = f[1];
phi[2 * k + 1] = f[2];
matched += 1;
}
}
assert_eq!(
matched,
wetted.len(),
"B4_MODE must cover every wetted node"
);
let peak_y = phi
.iter()
.skip(1)
.step_by(2)
.fold(0.0f64, |a, &b| a.max(b.abs()));
let dt = fluid.dt_fluid;
let omega = 2.0 * std::f64::consts::PI * freq;
let period = 1.0 / freq;
let t_end = t0 + periods * period;
let ramp_w = ramp_periods * period;
let (drag0, lift0) = fluid.measure_force();
println!(
" B4 PRESCRIBED ny = {ny}: {} wetted nodes, q0 {q0:.4e} (peak lateral {:.4e} m), f {freq} Hz, {periods} periods from t0 = {t0:.4} (ramp {ramp_periods} periods), dt {dt:.6e} ({:.1} steps/period), u_mean {}, tip corner {} m, rounds cap {max_rounds}; start drag {drag0:.3} lift {lift0:.3}",
wetted.len(),
q0 * peak_y,
period / dt,
fluid.case.u_mean,
fsi2_harness::overset::tip_corner(),
);
let mut csv = std::fs::File::create(&csv_path).expect("B4_CSV");
writeln!(
csv,
"t,q,qd,qdd,F,Fx_sum,Fy_sum,drag,lift,rounds_max,schwarz_ok,conservation"
)
.unwrap();
let mut node_csv = std::env::var("B4_NODE_CSV").ok().map(|p| {
let mut f = std::fs::File::create(p).expect("B4_NODE_CSV");
let mut head = String::from("t");
for id in &wetted {
head.push_str(&format!(",fx{},fy{}", id.0, id.0));
}
writeln!(f, "{head}").unwrap();
f
});
let start = std::time::Instant::now();
let mut step = 0usize;
let mut t_now = t0;
while t_now < t_end - 0.5 * dt {
let t_new = t_now + dt;
let s = omega * (t_new - t0);
let (r, rd) = ramp(t_new, t0, ramp_w);
let q = q0 * r * s.sin();
let qd = q0 * (r * omega * s.cos() + rd * s.sin());
// q̈ of the steady part (the ramp's own terms are dropped: the fit
// reads only the post-ramp window).
let qdd = -q0 * r * omega * omega * s.sin();
let d: Vec<f64> = phi.iter().map(|p| q * p).collect();
let v: Vec<f64> = phi.iter().map(|p| qd * p).collect();
if let Err(e) = fluid.set_geometry(&d, &v) {
panic!("set_geometry died at step {step}, t = {t_new:.5}: {e:?}");
}
let res = fluid
.step()
.unwrap_or_else(|e| panic!("fluid step died at step {step}, t = {t_new:.5}: {e:?}"));
fluid.commit_base();
let (nodal, conservation, _) = fluid.sample_load(&d);
let mut gen_f = 0.0;
let (mut fx, mut fy) = (0.0, 0.0);
for (k, (_, f)) in nodal.iter().enumerate() {
gen_f += f.x * phi[2 * k] + f.y * phi[2 * k + 1];
fx += f.x;
fy += f.y;
}
let (drag, lift) = fluid.measure_force();
writeln!(
csv,
"{t_new:.9},{q:.9e},{qd:.9e},{qdd:.9e},{gen_f:.9e},{fx:.9e},{fy:.9e},{drag:.6},{lift:.6},{},{},{conservation:.3e}",
res.rounds.iter().copied().max().unwrap_or(0),
res.schwarz_converged as u8
)
.unwrap();
if let Some(f) = node_csv.as_mut() {
let mut line = format!("{t_new:.9}");
for (_, fv) in &nodal {
line.push_str(&format!(",{:.6e},{:.6e}", fv.x, fv.y));
}
writeln!(f, "{line}").unwrap();
}
step += 1;
if step % 500 == 0 {
println!(
" t = {t_new:.4} ({step} steps): q {q:+.3e} F {gen_f:+.4e} drag {drag:.2} lift {lift:.2}, {:.0} s wall",
start.elapsed().as_secs_f64()
);
}
t_now = t_new;
}
println!(
" B4 PRESCRIBED DONE: {step} steps to t = {t_now:.5} in {:.0} s wall → {csv_path}",
start.elapsed().as_secs_f64()
);
}