CI / Format Check (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 0s
CI / Build (ubuntu-latest) (push) Failing after 6s
CI / Build CPU-Only (Explicit) (push) Failing after 53s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build API Documentation (push) Failing after 13s
CI / Clippy Check (push) Failing after 23s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01LzcjQX7tvgn87CQCyg9Cfr
652 lines
26 KiB
Rust
652 lines
26 KiB
Rust
//! 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<Vec<String>> = 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::<f64>().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<f64> = {
|
||
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<f64> = 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<usize> = 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<f64>, Vec<f64>)> = [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<f64> = 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<f64> = d.iter().map(|x| rr * x).collect();
|
||
let v_r: Vec<f64> = 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::<usize>().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::<f64>() / 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::<f64>() / 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<f64> = 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::<f64>() / 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])
|
||
);
|
||
}
|