//! 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 = 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 = phi.iter().map(|p| q * p).collect(); let v: Vec = 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() ); }