//! P5 (`docs/overset_metal_campaign.md` §5.12): Turek–Hron FSI2 with the //! fluid on the OVERSET — the body-fitted patch following the flag. The //! structure, coupling and acceptance are the FSI2 harness's; the fluid //! side is `fsi2_harness::overset`. Defaults are the P5-1 gate run //! (ny = 41, s = 1, a short march); `RTX_FSI2O_*` knobs as the harness's. //! Reference (Turek–Hron FSI2): uy(A) 1.23 ± 80.6 mm at 2.0 Hz, ux(A) //! −14.58 ± 12.44 mm, drag 208.83 ± 73.75, lift 0.88 ± 234.2. mod fsi2_harness; use fsi2_harness::overset_march::{OversetMarchConfig, run_march_overset}; use fsi2_harness::{FSI2, FSI3, case_from_env}; /// The benchmark's reference cycle (FEATFLOW's tables, level 4 — FSI2 at /// Δt = 0.01: uy 1.24 ± 81.7 mm at 1.93 Hz, ux −14.87 ± 12.73, drag 215.18 ± /// 77.78, lift 0.87 ± 238.0; FSI3 at Δt = 0.0005: uy 1.45 ± 34.90 at 5.46 Hz, /// ux −2.86 ± 2.70, drag 460.2 ± 27.47, lift 2.37 ± 153.75; /// `docs/research_sweep_2026-09.md` §1. The 2006 paper's FSI2 row is level 2 /// (80.6 mm, drag 208.83) with the frequency rounded to 2.0). struct Refs { uy_mean: f64, uy_amp: f64, uy_freq: f64, ux_mean: f64, ux_amp: f64, drag_mean: f64, drag_amp: f64, lift_mean: f64, lift_amp: f64, } fn refs(case: &str) -> Refs { if case == "FSI3" { Refs { uy_mean: 1.45e-3, uy_amp: 34.90e-3, uy_freq: 5.46, ux_mean: -2.86e-3, ux_amp: 2.70e-3, drag_mean: 460.2, drag_amp: 27.47, lift_mean: 2.37, lift_amp: 153.75, } } else { Refs { uy_mean: 1.24e-3, uy_amp: 81.7e-3, uy_freq: 1.93, ux_mean: -14.87e-3, ux_amp: 12.73e-3, drag_mean: 215.18, drag_amp: 77.78, lift_mean: 0.87, lift_amp: 238.0, } } } /// `RTX_FSI2O_AUDIT=dir`: the solver-metric chain on every saved instant /// under `dir` (`inst_*`, from `RTX_FSI2O_SAVE_EVERY`), no march. #[test] fn fsi2_overset_audit_of_saved_instants() { let Ok(dir) = std::env::var("RTX_FSI2O_AUDIT") else { return; }; let ny = std::env::var("RTX_FSI2O_NY") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(41); let flag_nx = std::env::var("RTX_FSI2O_FLAG_NX") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(35); let case = case_from_env("FSI2O", FSI2); let mut dirs: Vec<_> = std::fs::read_dir(&dir) .expect("audit dir") .filter_map(|e| e.ok().map(|e| e.path())) .filter(|p| { p.file_name() .and_then(|n| n.to_str()) .is_some_and(|n| n.starts_with("inst_")) }) .collect(); dirs.sort(); // `RTX_FSI2O_AUDIT_ONLY=a,b,c`: only the named instants. let only: Option> = std::env::var("RTX_FSI2O_AUDIT_ONLY") .ok() .map(|v| v.split(',').map(|s| s.trim().to_string()).collect()); for d in dirs { if let Some(only) = &only { let name = d.file_name().unwrap().to_string_lossy().to_string(); if !only.iter().any(|o| name.contains(o.as_str())) { continue; } } let (fluid, dvec, t) = fsi2_harness::overset::OversetFluid::from_instant(case, ny, flag_nx, &d) .expect("instant"); let tip: f64 = dvec.iter().map(|v| v * v).sum::().sqrt(); println!( " AUDIT {} t = {t:.4} |d| {tip:.3e}: {}", d.file_name().unwrap().to_string_lossy(), fluid.chain_line() ); // `RTX_FSI2O_AUDIT_FACES=1`: the wall load by region (P5-3). if std::env::var("RTX_FSI2O_AUDIT_FACES").is_ok() { let (regions, level) = fluid.wall_regions(&dvec); let uy_tip = dvec[2 * fluid.interface.tip[fluid.interface.tip.len() / 2] + 1]; let mut line = format!(" REGIONS t = {t:.4} tip uy {uy_tip:+.4}: p level {level:+.1} Pa |"); for (name, n, len, fx, fy, tn_mean, tn_min, tn_max) in regions { line += &format!( " {name}: {n} faces {len:.3} m, drag {fx:+.1}, lift {fy:+.1}, t_n mean {tn_mean:+.0} [{tn_min:+.0}, {tn_max:+.0}] Pa |" ); } println!("{line}"); } } } #[test] fn fsi2_on_the_overset() { let config = OversetMarchConfig::from_env( "FSI2O", OversetMarchConfig { ny: 41, flag_nx: 35, t_release: 6.0, t_end: 7.0, subcycle: 1, tol_floor: 2e-4, rtol: 1e-2, stall_accept: 5.0, max_subiterations: 12, coupler: "aitken".into(), reuse: 2, initial_relaxation: 0.5, c1_interface: false, predictor: "structure".into(), sweeps: 100, max_rounds: 3, csv_path: None, trace_steps: 0, }, ); // `RTX_FSI2O_CASE=FSI3` runs the FSI3 constants (Re 200, ρ_s = ρ_f, E 5.6e6). let base = if std::env::var("RTX_FSI2O_CASE").as_deref() == Ok("FSI3") { FSI3 } else { FSI2 }; let case = case_from_env("FSI2O", base); let rf = refs(case.name); let (cname, ruf, rdm, rda, rlm, rla) = ( case.name, rf.uy_freq, rf.drag_mean, rf.drag_amp, rf.lift_mean, rf.lift_amp, ); let r = run_march_overset(case, &config); let m = &r.result; let w = m.window(3.0); println!( " {cname} OVERSET (ny = {}, flag {}x2 Quad8, dt = {:.2e}, s = {}, sweeps {}, rounds cap {}, {}): coupled {} steps in {:.0} s wall; {:.1} subit/step (max {}); {} stalled, {} retries (worst residual {:.2e}); conservation {:.2e}; {} wall faces; Newton rescues {:?}; rounds mean {:.2}; reclassified/step {:.2} (fresh {:.2}); patch regenerations {} in {:.0} s; fluid {:.0} s, structure {:.0} s; death {:?}\n measured over [{:.1}, {:.1}] s: uy(A) = {:.3} ± {:.3} mm (ref {:.2} ± {:.1}), ux(A) = {:.3} ± {:.3} mm (ref {:.2} ± {:.2}), f = {:?} Hz (ref {ruf}); drag {:.2} ± {:.2} (ref {rdm} ± {rda}), lift {:.2} ± {:.2} (ref {rlm} ± {rla}); onset amp {:.3e} → {:.3e} m; rigid drag {:.2}", config.ny, config.flag_nx, m.dt, config.subcycle, config.sweeps, config.max_rounds, config.coupler, m.coupled_steps, m.elapsed, m.mean_subiterations, m.max_subiterations, m.stalled_steps, m.retried_steps, m.worst_stall, m.worst_conservation, r.faces_used, m.newton_rescues, r.rounds_mean, r.reclassified_mean, r.fresh_mean, r.regen_count, r.regen_seconds, r.fluid_seconds, r.structure_seconds, r.death, w.t_start, config.t_end, w.uy_mid * 1e3, w.uy_amp * 1e3, rf.uy_mean * 1e3, rf.uy_amp * 1e3, w.ux_mid * 1e3, w.ux_amp * 1e3, rf.ux_mean * 1e3, rf.ux_amp * 1e3, w.frequency, w.drag_mid, w.drag_amp, w.lift_mid, w.lift_amp, w.amp_early, w.amp_late, m.rigid_drag ); assert!(m.final_state_finite, "the flag's state is not finite"); assert!(r.death.is_none(), "the coupling died: {:?}", r.death); } /// `RTX_FSI2O_PROBE_INSTANT=dir`: the P5-2 warm replica died at t = 10.99 /// s (tip −35 mm) with "acceptor p donor cell not active". From the /// instant, extrapolate the interface along its velocity and find where /// the overlap first refuses — with the warm chain from the instant's /// mesh and with cold builds — reporting the acceptor, its donor cell's /// class, and the patch's thickness there. #[test] fn fsi2_overset_probe_death_from_instant() { use rtx_cfd::mesh::patch_gen::{ cylinder_flag_patch_deformed, cylinder_flag_patch_deformed_from, }; use rtx_cfd::solvers::incompressible::{CellClass, OverlapMap}; let Ok(dir) = std::env::var("RTX_FSI2O_PROBE_INSTANT") else { return; }; let dir = std::path::Path::new(&dir); let case = case_from_env("FSI2O", FSI2); let (fluid, d, t) = fsi2_harness::overset::OversetFluid::from_instant(case, 41, 35, dir).expect("instant"); let dd: Vec = { let bytes = std::fs::read(dir.join("patch_dd.bin")).expect("dd"); bytes .chunks_exact(8) .map(|c| f64::from_le_bytes(c.try_into().unwrap())) .collect() }; let h = fluid.h; let nx = fluid.nx; let tip_y = d[2 * fluid.interface.tip[fluid.interface.tip.len() / 2] + 1]; println!( " instant t = {t:.4}: tip uy {tip_y:+.4} m, hole {} fringe {} acceptors {}", fluid.solver.overlap().hole_cells(), fluid.solver.overlap().fringe_count(), fluid.solver.overlap().acceptors.len() ); let mut warm_prev = fluid.solver.patch().mesh().clone(); for k in 0..=12 { let tau = 0.005 * k as f64; let dk: Vec = d.iter().zip(&dd).map(|(a, v)| a + v * tau).collect(); let edges = fluid.interface.edges(&dk); let fillet = 0.5 * 0.41 / 41.0; let cold = cylinder_flag_patch_deformed( [0.2, 0.2], 0.05, 0.01, &edges, 0.6, h, fillet, 6.0 * h, 12, 4.0, 100, ) .expect("cold"); let warm = cylinder_flag_patch_deformed_from( Some(&warm_prev), [0.2, 0.2], 0.05, 0.01, &edges, 0.6, h, fillet, 6.0 * h, 12, 4.0, 20, ) .expect("warm"); let tip_now = dk[2 * fluid.interface.tip[fluid.interface.tip.len() / 2] + 1]; for (name, mesh) in [("cold", &cold.0), ("warm", &warm.0)] { let valid = mesh.validate(80.0).err(); let r = OverlapMap::build(mesh, nx, 41, h, h, 4); match r { Ok(map) => println!( " τ = {tau:.3} tip {tip_now:+.4}: {name} valid {:?}, overlap ok (hole {} fringe {})", valid.is_none(), map.hole_cells(), map.fringe_count() ), Err(e) => { let msg = format!("{e:?}"); // The failing acceptor's geometry, and the overlap depth // that would build. let mut extra = String::new(); if let Some(a0) = msg.find("acceptor ") { let k: usize = msg[a0 + 9..].split(' ').next().unwrap().parse().unwrap(); let c = mesh.cell(mesh.nn() - 1, k); let ac = mesh.centre(c); let inner = mesh.node_xy(mesh.node(0, k)); let outer = mesh.node_xy(mesh.node(mesh.nn(), k)); let r8 = mesh.node_xy(mesh.node(8, k)); let thick = ((outer[0] - inner[0]).powi(2) + (outer[1] - inner[1]).powi(2)).sqrt(); let hole_depth = ((r8[0] - inner[0]).powi(2) + (r8[1] - inner[1]).powi(2)).sqrt(); let mut rows_ok = Vec::new(); for rows in [5usize, 6, 7, 8] { if OverlapMap::build(mesh, nx, 41, h, h, rows).is_ok() { rows_ok.push(rows); } } extra = format!( " — acceptor {k} centre ({:.3}, {:.3}); its ray: wall node ({:.3}, {:.3}), outer ({:.3}, {:.3}), thickness {:.2} h, hole depth {:.2} h, band {:.2} h; overlap_rows that build: {rows_ok:?}", ac[0], ac[1], inner[0], inner[1], outer[0], outer[1], thick / h, hole_depth / h, (thick - hole_depth) / h ); } if let Some(i0) = msg.find("cell (") { let coords: Vec = msg[i0 + 6..] .split(')') .next() .unwrap() .split(',') .map(|s| s.trim().parse().unwrap()) .collect(); let (j, i) = (coords[0], coords[1]); let (x, y) = ((i as f64 + 0.5) * h, (j as f64 + 0.5) * h); // The nearest inner-ring node and the patch thickness along its ray. let mut best = (f64::INFINITY, 0usize); for s in 0..=mesh.ns() { let p = mesh.node_xy(mesh.node(0, s)); let dd2 = (p[0] - x).powi(2) + (p[1] - y).powi(2); if dd2 < best.0 { best = (dd2, s); } } let s = best.1; let inner = mesh.node_xy(mesh.node(0, s)); let outer = mesh.node_xy(mesh.node(mesh.nn(), s)); let r8 = mesh.node_xy(mesh.node(8, s)); let thick = ((outer[0] - inner[0]).powi(2) + (outer[1] - inner[1]).powi(2)).sqrt(); let hole_depth = ((r8[0] - inner[0]).powi(2) + (r8[1] - inner[1]).powi(2)).sqrt(); extra += &format!( " — cell ({j}, {i}) at ({x:.3}, {y:.3}); nearest wall node s = {s} at ({:.3}, {:.3}); ray thickness {:.3} h, hole depth (row 8) {:.3} h, overlap band {:.3} h; outer node ({:.3}, {:.3})", inner[0], inner[1], thick / h, hole_depth / h, (thick - hole_depth) / h, outer[0], outer[1] ); let _ = CellClass::Hole; } println!( " τ = {tau:.3} tip {tip_now:+.4}: {name} valid {:?}, overlap REFUSED: {msg}{extra}", valid.is_none() ); } } } warm_prev = warm.0; } } /// The replay's Hermite interpolant passes through every knot with its /// saved velocity, and its prescribed velocity is the interpolant's exact /// derivative between knots (the GCL's premise for a prescribed wall). #[test] fn replay_passes_through_knots_with_their_velocities() { use fsi2_harness::replay::{Replay, ramp}; let f = |t: f64| vec![(3.0 * t).sin(), 0.5 * t * t]; let df = |t: f64| vec![3.0 * (3.0 * t).cos(), t]; let knots: Vec<(f64, Vec, Vec)> = [0.0, 0.4, 0.9, 1.5] .iter() .map(|&t| (t, f(t), df(t))) .collect(); let r = Replay::from_knots(knots); for &t in &[0.0, 0.4, 0.9, 1.5] { let (d, v) = r.at(t); for i in 0..2 { assert!((d[i] - f(t)[i]).abs() < 1e-12, "d at knot {t}"); assert!((v[i] - df(t)[i]).abs() < 1e-12, "ḋ at knot {t}"); } } let eps = 1e-6; for &t in &[0.1, 0.55, 1.2] { let (_, v) = r.at(t); let (dp, _) = r.at(t + eps); let (dm, _) = r.at(t - eps); for i in 0..2 { let fd = (dp[i] - dm[i]) / (2.0 * eps); assert!( (v[i] - fd).abs() < 1e-6, "ḋ is the derivative at {t}: {} vs {fd}", v[i] ); } } assert_eq!(ramp(-1.0, 0.0, 0.5), (0.0, 0.0)); assert_eq!(ramp(0.7, 0.0, 0.5), (1.0, 0.0)); let (r0, r1) = ramp(0.25, 0.0, 0.5); assert!((r0 - 0.5).abs() < 1e-12 && r1 > 0.0); } /// `RTX_FSI2O_REPLAY=dir`: the P5-3 energy discriminator — the saved /// instants' kinematics (`d`, `ḋ` every N steps of a march) replayed on /// the fluid ALONE at `RTX_FSI2O_NY`, from `RTX_FSI2O_LOAD`'s rigid state, /// from `RTX_FSI2O_REPLAY_T0` (9.0) with a `RTX_FSI2O_REPLAY_RAMP` (0.5 s) /// ramp to `RTX_FSI2O_T_END` (16). Per step the CSV (`RTX_FSI2O_CSV`) /// records t, tip uy, drag, lift, the fluid's power on the flag Σ F·ḋ, /// the wall net flux and the polygon area rate; the summary reads the /// window [13, 16]: mean power, lift swing, drag median. #[test] fn fsi2_overset_prescribed_motion() { use fsi2_harness::overset::OversetFluid; use fsi2_harness::replay::{Replay, ramp}; let Ok(replay_dir) = std::env::var("RTX_FSI2O_REPLAY") else { println!(" RTX_FSI2O_REPLAY unset — nothing to replay"); return; }; 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_FSI2O_NY", 41.0) as usize; let t0 = env_f("RTX_FSI2O_REPLAY_T0", 9.0); let ramp_w = env_f("RTX_FSI2O_REPLAY_RAMP", 0.5); let t_end = env_f("RTX_FSI2O_T_END", 16.0); let case = case_from_env("FSI2O", FSI2); let replay = Replay::load(&replay_dir); let (r0, r1) = replay.span(); println!( " replay: {} instants over [{r0:.4}, {r1:.4}] s from {replay_dir}", replay.times.len() ); let t_end = t_end.min(r1); assert!(t0 >= r0, "replay starts at {r0}, after t0 = {t0}"); // `RTX_FSI2O_MAX_ROUNDS` (3): the Schwarz rounds cap — the coupled march's // 3-round cap is load-bearing for the coupling's stability (every tighter // cap dies at release), so the fluid side of that question is measured here. let max_rounds = env_f("RTX_FSI2O_MAX_ROUNDS", 3.0) as usize; let mut fluid = OversetFluid::build_case(case, ny, 35, 100, max_rounds).expect("overset fluid"); let n = 2 * fluid.interface.wetted.len(); assert_eq!( replay.d[0].len(), n, "the instants' d does not match this interface" ); let load_dir = std::env::var("RTX_FSI2O_LOAD").expect("RTX_FSI2O_LOAD (the rigid state)"); fluid.load(&load_dir).expect("load"); fluid.commit_base(); fluid.solver.set_time(t0); let dt = fluid.dt_fluid; let (d_rigid, l_rigid) = fluid.measure_force(); println!( " PRESCRIBED ny = {ny}: dt {dt:.3e}, rigid drag {d_rigid:.2} lift {l_rigid:.2}, replay from t = {t0} (ramp {ramp_w} s) to {t_end}; patch offset {} h × {} rows, patch convection {:?}, bg convection {:?}, patch stretch {}, fillet {} m, tip corner {} m, rounds cap {max_rounds}", std::env::var("RTX_FSI2O_PATCH_OFFSET").unwrap_or_else(|_| "6".into()), std::env::var("RTX_FSI2O_PATCH_ROWS").unwrap_or_else(|_| "12".into()), fsi2_harness::overset::patch_convection(), fsi2_harness::overset::bg_convection(), fsi2_harness::overset::patch_stretch(), fsi2_harness::overset::fillet(), fsi2_harness::overset::tip_corner(), ); let mut csv = std::env::var("RTX_FSI2O_CSV").ok().map(|p| { use std::io::Write as _; let mut f = std::fs::File::create(p).expect("csv"); writeln!( f, "t,uy_tip,drag,lift,power,wall_flux,area_rate,p_faces,p_normal,p_tangential,p_root,p_mid,p_tip,rounds_max,schwarz_ok,bg_residual,patch_div,defect_patch,defect_bg,reclassified,e_stamp,e_reclass" ) .unwrap(); f }); let tip = fluid.interface.tip.clone(); let start = std::time::Instant::now(); let mut prev_area = fluid.shared.read().unwrap().area(); let mut window: Vec<(f64, f64, f64, f64)> = Vec::new(); let mut faces_window: Vec<(f64, f64, f64, [f64; 3])> = Vec::new(); let save_at: Vec = std::env::var("RTX_FSI2O_SAVE_AT") .ok() .map(|v| v.split(',').filter_map(|x| x.trim().parse().ok()).collect()) .unwrap_or_default(); let mut step = 0usize; let mut t_now = t0; while t_now < t_end - 0.5 * dt { let t_new = t_now + dt; let (d, v) = replay.at(t_new.min(r1)); let (rr, rd) = ramp(t_new, t0, ramp_w); let d_r: Vec = d.iter().map(|x| rr * x).collect(); let v_r: Vec = v.iter().zip(&d).map(|(vi, di)| rr * vi + rd * di).collect(); if let Err(e) = fluid.set_geometry(&d_r, &v_r) { panic!("set_geometry died at step {step}, t = {t_new:.4}: {e:?}"); } let (rounds_max, schwarz_ok, bg_res, patch_div, reclassified, e_stamp, e_reclass) = match fluid.step() { Ok(r) => ( r.rounds.iter().copied().max().unwrap_or(0), r.schwarz_converged, r.background_residual, r.patch_max_divergence, r.reclassified_cells, r.stamp_energy, r.reclass_energy, ), Err(e) => panic!("fluid step died at step {step}, t = {t_new:.4}: {e:?}"), }; let (defect_patch, defect_bg, _) = fluid.last_defects.get(); fluid.commit_base(); let (nodal, _, _) = fluid.sample_load(&d_r); let power: f64 = nodal .iter() .enumerate() .map(|(k, (_, f))| f.x * v_r[2 * k] + f.y * v_r[2 * k + 1]) .sum(); // The same power face by face: normal vs tangential traction, and // the flag's root / middle / tip thirds (x < 0.3667 / < 0.4833 / // the rest, the tip arc included); the cylinder's faces excluded. let (mut p_faces, mut p_normal, mut p_tangential) = (0.0, 0.0, 0.0); let mut p_thirds = [0.0; 3]; { let wall = fluid.shared.read().unwrap(); for (centre, normal, len, traction) in fluid.solver.patch().wall_tractions( &fluid.field.patch, rtx_cfd::mesh::PatchSide::Inner, fluid.solver.time(), ) { let on_cylinder = ((centre[0] - 0.2).powi(2) + (centre[1] - 0.2).powi(2)).sqrt() < 0.05 + 1e-9; if on_cylinder { continue; } let (u, v) = wall.velocity_at(centre[0], centre[1]); let tn = traction[0] * normal[0] + traction[1] * normal[1]; let vn = u * normal[0] + v * normal[1]; let pf = (traction[0] * u + traction[1] * v) * len; let pn = tn * vn * len; p_faces += pf; p_normal += pn; p_tangential += pf - pn; let third = if centre[0] < 0.3667 { 0 } else if centre[0] < 0.4833 { 1 } else { 2 }; p_thirds[third] += pf; } } let (drag, lift) = fluid.measure_force(); let (_, wall_flux, _, _, poly_area) = fluid.level_and_wall_flux(); // `RTX_FSI2O_SAVE=dir` + `RTX_FSI2O_SAVE_EVERY=N` or // `RTX_FSI2O_SAVE_AT=t1,t2,…` (the step that first reaches each // time): replay instants for the per-region pressure audit at one // physical instant across h. if let Ok(dir) = std::env::var("RTX_FSI2O_SAVE") { let hit = save_at.iter().any(|&ts| t_now < ts && ts <= t_new + 1e-12); if hit { fluid .save_instant(&dir, step + 1, &d_r, &v_r) .expect("save instant"); } } if let (Ok(dir), Some(every)) = ( std::env::var("RTX_FSI2O_SAVE"), std::env::var("RTX_FSI2O_SAVE_EVERY") .ok() .and_then(|v| v.parse::().ok()) .filter(|&n| n > 0), ) { if (step + 1) % every == 0 { fluid .save_instant(&dir, step + 1, &d_r, &v_r) .expect("save instant"); } } let area_rate = (poly_area - prev_area) / dt; prev_area = poly_area; let uy_tip = tip.iter().map(|&k| d_r[2 * k + 1]).sum::() / tip.len().max(1) as f64; if let Some(f) = csv.as_mut() { use std::io::Write as _; writeln!( f, "{t_new:.6},{uy_tip:.6e},{drag:.4},{lift:.4},{power:.6e},{wall_flux:.6e},{area_rate:.6e},{p_faces:.6e},{p_normal:.6e},{p_tangential:.6e},{:.6e},{:.6e},{:.6e},{rounds_max},{},{bg_res:.3e},{patch_div:.3e},{defect_patch:.3e},{defect_bg:.3e},{reclassified},{e_stamp:.4e},{e_reclass:.4e}", p_thirds[0], p_thirds[1], p_thirds[2], schwarz_ok as u8 ) .unwrap(); } if t_new >= 13.0 { window.push((t_new, drag, lift, power)); faces_window.push((p_faces, p_normal, p_tangential, p_thirds)); } step += 1; if step % 500 == 0 { println!( " t = {t_new:.3} s ({step} steps): uy_tip {uy_tip:+.4}, drag {drag:.1} lift {lift:.1} power {power:+.2e} W/m, wall flux {wall_flux:+.2e} vs area rate {area_rate:+.2e}, {:.0} s wall", start.elapsed().as_secs_f64() ); } t_now = t_new; } let n_w = window.len().max(1) as f64; let mean_power = window.iter().map(|w| w.3).sum::() / n_w; let (lmin, lmax) = window .iter() .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), w| { (a.min(w.2), b.max(w.2)) }); let mut drags: Vec = window.iter().map(|w| w.1).collect(); drags.sort_by(|a, b| a.partial_cmp(b).unwrap()); let drag_med = drags.get(drags.len() / 2).copied().unwrap_or(f64::NAN); println!( " PRESCRIBED ny = {ny} over [13, 16] s ({} samples): mean fluid power on the flag {mean_power:+.3} W/m, lift swing [{lmin:.1}, {lmax:.1}] (± {:.1}), drag median {drag_med:.1}; {step} steps in {:.0} s wall", window.len(), 0.5 * (lmax - lmin), start.elapsed().as_secs_f64() ); let nf = faces_window.len().max(1) as f64; let mean = |f: &dyn Fn(&(f64, f64, f64, [f64; 3])) -> f64| { faces_window.iter().map(f).sum::() / nf }; println!( " PRESCRIBED ny = {ny} power by face over [13, 16] s: faces {:+.3} W/m (nodal {mean_power:+.3}) = normal {:+.3} + tangential {:+.3}; root {:+.3} / middle {:+.3} / tip {:+.3}", mean(&|w| w.0), mean(&|w| w.1), mean(&|w| w.2), mean(&|w| w.3[0]), mean(&|w| w.3[1]), mean(&|w| w.3[2]) ); }