Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_flag_wake.rs
T
Omar SobhandClaude Opus 5.5 2c054ccc91 embedded3 R6-2 step 2: the band's surface velocity at the foot (ub) and the rebuild's imposition on the device (RTX_E3_UB_DEVICE=1, default off)
e3_geom_ub / e3_geom_impose / e3_geom_seam: the flag body's surface velocity
(DeviceSdf::vel, the centreline's velocity per point) at the foot from the
trilinear interpolant of the device corner phi, and at the face centre for the
band's solid faces of the uploaded field (a cut mask has no ghosts); the host
mirror of ub by a packed download. RTX_E3_BAND_CHECK=1 compares the device ub
with Mask::surface_velocity_at and the imposed u, v, w with impose_from, bit for
bit; check_against now compares ub on every band face (zeros included).

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

631 lines
25 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.
//! embedded3 S2-3: the free-ended flag with prescribed motion — the first
//! honest 3D wake. The Turek–Hron channel (2.5 × 0.41) extruded to depth
//! 0.41 with the cylinder (D 0.1 at (0.2, 0.2)) across the width, the flag
//! (`RTX_E3_FLAG_HEIGHT` / `RTX_E3_FLAG_DEPTH` grow the duct around the
//! unchanged body, re-centred: the 2026-09-22/23 explorations), the flag
//! 0.35 × 0.02 × 0.2 centred in z (z 0.105–0.305), its centreline deflected
//! as the first clamped-free beam mode with the 2D FSI2 flat-tip record's
//! tip amplitude 84 mm at 1.930 Hz (motion prescribed, no structure), the
//! flag's span edges rounded to one cell and its tip a semicircle (the
//! linear cut geometry needs smooth edges; disclosed). Inflow parabolic
//! in y and z with U_m 2.25 (Ū 1.0 = FSI2's mean, Re 100 on D), ρ 1000,
//! ν 1e-3. CutCell wall with merging, TVD, moving body on the device
//! (S2-2a: the host rebuild per step).
//!
//! Gate (`docs/embedded3_campaign.md` S2-3): ny 62, two full periods, no
//! death, mass residual ≤ 1e-8 every step; the mid-plane per-span loads
//! within 30 % of the 2D FSI2 record (drag mean 224.6 N/m, lift swing
//! ±215 flat tip / ±256 semicircle); 32 VTK phases of the last period.
//!
//! `RTX_E3_FLAG_NY=62 RTX_E3_FLAG_PERIODS=2 RTX_E3_FLAG_VTK=<dir> RTX_E3_FLAG_CSV=<path> \
//! RTX_CUDA_ARCH=sm_120 cargo test --release -p rtx-cfd --features cuda --test embedded3_flag_wake -- --ignored --nocapture`
#![cfg(feature = "cuda")]
mod embedded3_flag_kinematics;
use embedded3_flag_kinematics::{Recorded, recorded};
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::step::device::DeviceStep;
use rtx_cfd::solvers::incompressible::embedded3::{
Body, Boundaries, DeviceSdf, Field, Fluid, Grid, Parameters, Side, Solver, WallScheme,
write_vtk,
};
use std::io::Write as _;
const H: f64 = 0.41;
const L: f64 = 2.5;
const CX: f64 = 0.2;
const CY: f64 = 0.2;
const R_CYL: f64 = 0.05;
/// The flag's ROOT: the cylinder's rear (the Turek–Hron flag runs from the
/// cylinder to its tip A at x = 0.6). Until 2026-09-18 this was 0.6 — the
/// flag sat DETACHED, its root where the benchmark's tip is; every flag
/// record before that date is of that geometry.
const FLAG_X0: f64 = 0.25;
const FLAG_LEN: f64 = 0.35;
const FLAG_HALF: f64 = 0.01;
const FLAG_SPAN: f64 = 0.2;
const AMP: f64 = 0.084;
const FREQ: f64 = 1.930;
const U_M: f64 = 2.25;
const RHO: f64 = 1000.0;
const NU: f64 = 1e-3;
/// The first clamped-free beam mode's `β L`.
const BETA_L: f64 = 1.875_104_069;
fn env_f(name: &str, default: f64) -> f64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
/// The first mode shape normalised to 1 at the tip, `s ∈ [0, 1]`.
fn mode(s: f64) -> f64 {
let b = BETA_L;
let sigma = (b.sinh() - b.sin()) / (b.cosh() + b.cos());
let phi = |s: f64| (b * s).cosh() - (b * s).cos() - sigma * ((b * s).sinh() - (b * s).sin());
phi(s) / phi(1.0)
}
/// Centreline deflection and its velocity at arc parameter `s`, time `t`.
fn deflection(s: f64, t: f64) -> (f64, f64) {
let w = 2.0 * std::f64::consts::PI * FREQ;
let amp = amplitude();
(
amp * mode(s) * (w * t).sin(),
amp * mode(s) * w * (w * t).cos(),
)
}
/// The tip amplitude: `RTX_E3_FLAG_AMP` (default 0.084; 0 freezes the flag —
/// the static control of the load routes).
fn amplitude() -> f64 {
env_f("RTX_E3_FLAG_AMP", AMP)
}
/// R3: the root fillet radius (`RTX_E3_FLAG_FILLET`, metres; 0 = the sharp
/// concave corner of the plain union; the overset outline carries 5 mm).
fn root_fillet() -> f64 {
env_f("RTX_E3_FLAG_FILLET", 0.0)
}
/// R3 (2026-09-21): the capsule's tip inset (`RTX_E3_FLAG_TIP_INSET`,
/// metres, default `FLAG_HALF`): the centreline polyline is shortened by
/// this much along its last segment so the capsule's apex sits ON the
/// benchmark's tip A (the overset outline's semicircle apex). Every flag
/// record before this date had the apex 10 mm beyond A (`=0` restores
/// them): at ny 62 on the recorded motion that was drag 253.3 → 240.0 and
/// the lift swing 1,005 → 836 (the overset's 867).
fn tip_inset() -> f64 {
env_f("RTX_E3_FLAG_TIP_INSET", FLAG_HALF)
}
/// Smooth union with a concave fillet of radius `r` (the plain `min` at r = 0).
fn fillet_union(d1: f64, d2: f64, r: f64) -> f64 {
if r > 0.0 && d1 < r && d2 < r {
r - ((r - d1).powi(2) + (r - d2).powi(2)).sqrt()
} else {
d1.min(d2)
}
}
/// Pull the polyline's last point back by `inset` along its last segment.
fn inset_last(pts: &mut [(f64, f64, f64, f64)], inset: f64) {
if inset <= 0.0 || pts.len() < 2 {
return;
}
let n = pts.len();
let (ax, ay, _, _) = pts[n - 2];
let (bx, by, bvx, bvy) = pts[n - 1];
let len = ((bx - ax).powi(2) + (by - ay).powi(2)).sqrt();
let f = (1.0 - inset / len).max(0.0);
pts[n - 1] = (ax + f * (bx - ax), ay + f * (by - ay), bvx, bvy);
}
/// Signed distance to the deflected flag's cross-section (a capsule
/// around the centreline polyline of `n` segments) and the centreline's
/// velocity at the closest point (transverse only in the analytic mode).
fn flag_2d(x: f64, y: f64, t: f64) -> (f64, (f64, f64)) {
if let Some(rec) = recorded() {
return flag_2d_recorded(rec, x, y, t);
}
let (d, v) = flag_2d_analytic(x, y, t);
(d, (0.0, v))
}
/// The recorded centreline's capsule and velocity at the closest point.
fn flag_2d_recorded(rec: &Recorded, x: f64, y: f64, t: f64) -> (f64, (f64, f64)) {
thread_local! {
static POLY: std::cell::RefCell<(f64, Vec<(f64, f64, f64, f64)>)> =
const { std::cell::RefCell::new((f64::NAN, Vec::new())) };
}
POLY.with(|cell| {
let mut c = cell.borrow_mut();
if c.0.to_bits() != t.to_bits() {
c.1 = recorded_polyline(rec, t);
c.0 = t;
}
let pts = &c.1;
let mut best = f64::INFINITY;
let mut v_best = (0.0, 0.0);
for m in 0..pts.len() - 1 {
let (ax, ay, avx, avy) = pts[m];
let (bx, by, bvx, bvy) = pts[m + 1];
let (ex, ey) = (bx - ax, by - ay);
let l2 = ex * ex + ey * ey;
let u = (((x - ax) * ex + (y - ay) * ey) / l2).clamp(0.0, 1.0);
let (px, py) = (ax + u * ex, ay + u * ey);
let d = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
if d < best {
best = d;
v_best = (avx + u * (bvx - avx), avy + u * (bvy - avy));
}
}
(best - FLAG_HALF, v_best)
})
}
/// The recorded centreline at `t` with the tip inset.
fn recorded_polyline(rec: &Recorded, t: f64) -> Vec<(f64, f64, f64, f64)> {
let mut pts = rec.at(t, body_cy());
inset_last(&mut pts, tip_inset());
pts
}
/// The analytic centreline's segments.
const N: usize = 40;
/// The analytic centreline at `t` (x, y, transverse velocity), the tip inset.
fn analytic_polyline(t: f64) -> [(f64, f64, f64); N + 1] {
let mut p = [(0.0, 0.0, 0.0); N + 1];
for (m, q) in p.iter_mut().enumerate() {
let s = m as f64 / N as f64;
let (d, v) = deflection(s, t);
*q = (FLAG_X0 + s * FLAG_LEN, body_cy() + d, v);
}
let inset = tip_inset();
if inset > 0.0 {
let (ax, ay, _) = p[N - 1];
let (bx, by, bv) = p[N];
let len = ((bx - ax).powi(2) + (by - ay).powi(2)).sqrt();
let f = (1.0 - inset / len).max(0.0);
p[N] = (ax + f * (bx - ax), ay + f * (by - ay), bv);
}
p
}
fn flag_2d_analytic(x: f64, y: f64, t: f64) -> (f64, f64) {
// The centreline polyline at `t`, once per thread and time (PERF-3
// P1-2): the solver asks for the surface velocity at ~10⁶ faces per
// step and each call rebuilt the 41 points (four hyperbolic / trigonometric
// evaluations each). Same arithmetic, same digits.
thread_local! {
static POLYLINE: std::cell::RefCell<(f64, [(f64, f64, f64); N + 1])> =
const { std::cell::RefCell::new((f64::NAN, [(0.0, 0.0, 0.0); N + 1])) };
}
let pts = POLYLINE.with(|cell| {
let mut c = cell.borrow_mut();
if c.0.to_bits() != t.to_bits() {
c.1 = analytic_polyline(t);
c.0 = t;
}
c.1
});
let mut best = f64::INFINITY;
let mut v_best = 0.0;
let point = |m: usize| pts[m];
for m in 0..N {
let (ax, ay, av) = point(m);
let (bx, by, bv) = point(m + 1);
let (ex, ey) = (bx - ax, by - ay);
let l2 = ex * ex + ey * ey;
let u = (((x - ax) * ex + (y - ay) * ey) / l2).clamp(0.0, 1.0);
let (px, py) = (ax + u * ex, ay + u * ey);
let d = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
if d < best {
best = d;
v_best = av + u * (bv - av);
}
}
(best - FLAG_HALF, v_best)
}
/// The flag's span: `RTX_E3_FLAG_SPAN` (default 0.2); at the duct's
/// full depth the flag is the 2D geometry extruded (S2-3b).
fn flag_span() -> f64 {
env_f("RTX_E3_FLAG_SPAN", FLAG_SPAN)
}
/// The duct's depth (the z extent): `RTX_E3_FLAG_DEPTH` (default H, the
/// square duct). A wider duct than the flag's span (2026-09-22, the
/// "wide span" exploration) puts a finite-span plate in free flow between
/// the side walls — not the benchmark's geometry.
fn duct_depth() -> f64 {
env_f("RTX_E3_FLAG_DEPTH", H)
}
/// The duct's height (the y extent): `RTX_E3_FLAG_HEIGHT` (default H).
/// The cell size stays `H / ny` (the benchmark's rung definition); the
/// body is re-centred in the taller duct (2026-09-23, the "big duct").
fn duct_height() -> f64 {
env_f("RTX_E3_FLAG_HEIGHT", H)
}
/// The cylinder's centre and the flag's rest centreline: `CY` in the
/// benchmark duct, lifted by half the added height otherwise.
fn body_cy() -> f64 {
CY + 0.5 * (duct_height() - H)
}
/// The flag in 3D: the extruded capsule cut to the span with edges
/// rounded to radius `r` (no cut at the full width).
fn flag_3d(x: f64, y: f64, z: f64, t: f64, r: f64) -> (f64, (f64, f64)) {
let (d2, v) = flag_2d(x, y, t);
let span = flag_span();
if span >= duct_depth() {
return (d2, v);
}
let zc = 0.5 * duct_depth();
let q1 = d2 + r;
let q2 = (z - zc).abs() - 0.5 * span + r;
let outside = (q1.max(0.0).powi(2) + q2.max(0.0).powi(2)).sqrt();
(outside + q1.max(q2).min(0.0) - r, v)
}
/// The finite cylinder: the 2D circle cut to `span` in z with the same
/// rounded edges as the flag (`RTX_E3_FLAG_CYL_SPAN=flag`, 2026-09-23: the
/// whole body a finite object in the wider ducts); wall to wall otherwise.
fn cylinder_3d(d2: f64, z: f64, r: f64) -> f64 {
let span = flag_span();
if span >= duct_depth() || !std::env::var("RTX_E3_FLAG_CYL_SPAN").is_ok_and(|v| v == "flag") {
return d2;
}
let zc = 0.5 * duct_depth();
let q1 = d2 + r;
let q2 = (z - zc).abs() - 0.5 * span + r;
let outside = (q1.max(0.0).powi(2) + q2.max(0.0).powi(2)).sqrt();
outside + q1.max(q2).min(0.0) - r
}
fn inflow(y: f64, z: f64) -> f64 {
let (hd, d) = (duct_height(), duct_depth());
16.0 * U_M * y * z * (hd - y) * (d - z) / (hd * hd * d * d)
}
#[test]
#[ignore = "S2-3: the flag wake on the device (about an hour at ny 62)"]
fn flag_wake_on_the_device() {
let ny = env_f("RTX_E3_FLAG_NY", 62.0) as usize;
let periods = env_f("RTX_E3_FLAG_PERIODS", 2.0);
let h = H / ny as f64;
let nx = (L / h).round() as usize;
// The grid's rows: the benchmark's `ny` unless the duct is taller.
let ny_grid = (duct_height() / h).round() as usize;
// `RTX_E3_FLAG_NZ=4`: a thin slab periodic in z with the 2D inflow (Ū = 1) — the
// flag as a 2D problem, minutes per rung: the instrument for the load routes' parts.
let slab_nz = env_f("RTX_E3_FLAG_NZ", 0.0) as usize;
let nz = if slab_nz > 0 {
slab_nz
} else {
(duct_depth() / h).round() as usize
};
// S2-9b: the 3D run with the slab's 2D inflow (uniform in z) and/or slip
// side walls — the decomposition of the 3D-over-slab drag.
let inflow_2d = std::env::var("RTX_E3_FLAG_INFLOW").is_ok_and(|v| v == "2d");
let z_slip = std::env::var("RTX_E3_FLAG_ZSIDES").is_ok_and(|v| v == "slip");
let r_edge = h;
// S2-9: with a recorded kinematics the period and the speed bound are the record's.
let rec_period = env_f("RTX_E3_FLAG_KIN_PERIOD", 0.5225);
let rec_speed = recorded().map(Recorded::max_speed);
let dt_cfl = 0.3 * h / (U_M.max(rec_speed.unwrap_or(2.0 * std::f64::consts::PI * FREQ * AMP)));
// `RTX_E3_FLAG_DT_SCALE` scales the step (the dt ladder of the loads).
let dt = dt_cfl.min(0.5 * h * h / (6.0 * NU)) * env_f("RTX_E3_FLAG_DT_SCALE", 1.0);
let period = if recorded().is_some() { rec_period } else { 1.0 / FREQ };
let t_end = periods * period;
let mut solver = Solver::new(
Fluid {
density: RHO,
viscosity: RHO * NU,
reference_velocity: 1.0,
reference_length: 2.0 * R_CYL,
},
Parameters {
// Three correctors at a 1e-3 inner stop hold the moving cut
// wall's mass residual under 1e-8 (the moving circle: 7.6e-9
// against 1.5e-6 with two at 1e-2); `RTX_E3_FLAG_CORRECTORS`
// overrides for the comparison runs.
corrector_steps: env_f("RTX_E3_FLAG_CORRECTORS", 3.0) as usize,
inner_stop_factor: env_f("RTX_E3_FLAG_INNER", 1e-3),
tolerance: 1e-8,
convection_scheme: ConvectionScheme::TvdVanAlbada,
wall_scheme: WallScheme::CutCell,
boundaries: if slab_nz > 0 {
Boundaries {
x1: Side::PressureOutlet,
z0: Side::Periodic,
z1: Side::Periodic,
..Boundaries::default()
}
} else if z_slip {
// S2-9b: slip side walls (the 3D solver on a spanwise-uniform problem).
Boundaries {
x1: Side::PressureOutlet,
z0: Side::SlipWall,
z1: Side::SlipWall,
..Boundaries::default()
}
} else {
Boundaries {
x1: Side::PressureOutlet,
..Boundaries::default()
}
},
// The narrow band: the flag's tip speed bounds the surface motion.
max_surface_speed: Some(
(rec_speed.unwrap_or(2.0 * std::f64::consts::PI * FREQ * amplitude()) * 1.05).max(1e-3),
),
..Parameters::default()
},
);
let inflow_at = move |y: f64, z: f64| {
if slab_nz > 0 || inflow_2d {
let hd = duct_height();
6.0 * y * (hd - y) / (hd * hd)
} else {
inflow(y, z)
}
};
solver.set_boundary_velocity(move |x, y, z, _t| {
if x <= 0.0 {
(inflow_at(y, z), 0.0, 0.0)
} else {
(0.0, 0.0, 0.0)
}
});
let cy = body_cy();
let cyl = move |x: f64, y: f64| ((x - CX).powi(2) + (y - cy).powi(2)).sqrt() - R_CYL;
let r_fillet = root_fillet();
let body = Body::from_sdf(move |x, y, z, t| {
fillet_union(cylinder_3d(cyl(x, y), z, r_edge), flag_3d(x, y, z, t, r_edge).0, r_fillet)
})
.with_surface_velocity(move |x, y, z, t| {
let (df, (vx, vy)) = flag_3d(x, y, z, t, r_edge);
if df <= cylinder_3d(cyl(x, y), z, r_edge) {
(vx, vy, 0.0)
} else {
(0.0, 0.0, 0.0)
}
});
// R6-1: the same φ in the device's form (the device geometry, default ON): the
// circle, the capsule around the step's centreline, the span cuts.
let body = body.with_device_sdf(move |t| DeviceSdf {
cyl: [CX, cy, R_CYL],
cyl_cut: !(flag_span() >= duct_depth()
|| !std::env::var("RTX_E3_FLAG_CYL_SPAN").is_ok_and(|v| v == "flag")),
flag_cut: flag_span() < duct_depth(),
zc: 0.5 * duct_depth(),
span: flag_span(),
r_edge,
half: FLAG_HALF,
fillet: r_fillet,
poly: match recorded() {
Some(rec) => recorded_polyline(rec, t)
.iter()
.map(|p| [p.0, p.1])
.collect(),
None => analytic_polyline(t).iter().map(|p| [p.0, p.1]).collect(),
},
// R6-2 step 2: the centreline's velocity per point (the analytic mode is transverse).
vel: match recorded() {
Some(rec) => recorded_polyline(rec, t)
.iter()
.map(|p| [p.2, p.3])
.collect(),
None => analytic_polyline(t).iter().map(|p| [0.0, p.2]).collect(),
},
});
solver.set_moving_body(body);
let g = Grid::cubic(nx, ny_grid, nz, h);
let mut field = Field::new(g);
for k in 0..nz {
for j in 0..ny_grid {
let u0 = inflow_at((j as f64 + 0.5) * h, (k as f64 + 0.5) * h);
for i in 0..=nx {
field.u[g.uface(k, j, i)] = u0;
}
}
}
solver.initialize(&mut field);
println!(
" flag wake ny {ny} (span {}, cylinder {}; duct {:.2} × {:.2} m, inflow {}, z sides {}; root fillet {:.4} m, tip inset {:.4} m): {nx}×{ny_grid}×{nz} = {} cells, h {h:.4e}, dt {dt:.3e}, {periods} periods = {t_end:.3} s, {} steps",
flag_span(),
if std::env::var("RTX_E3_FLAG_CYL_SPAN").is_ok_and(|v| v == "flag") { "cut to the span" } else { "wall to wall" },
duct_height(),
duct_depth(),
if slab_nz > 0 || inflow_2d { "2d" } else { "3d" },
if slab_nz > 0 {
"periodic"
} else if z_slip {
"slip"
} else {
"wall"
},
root_fillet(),
tip_inset(),
g.cells(),
(t_end / dt).ceil() as usize
);
unsafe { std::env::set_var("RTX_PROFILE", "1") };
let mut device = DeviceStep::new(solver, g);
device.upload(&field);
let steps = (t_end / dt).ceil() as usize;
let mut csv = std::env::var("RTX_E3_FLAG_CSV").ok().map(|p| {
let mut f = std::fs::File::create(p).expect("csv");
writeln!(
f,
"t,tip,drag_span,lift_span,drag_total,lift_total,residual,cg,fresh,drag_rec,lift_rec"
)
.unwrap();
f
});
let vtk_dir = std::env::var("RTX_E3_FLAG_VTK").ok();
// `RTX_E3_FLAG_PHASES` phases per period (32); `RTX_E3_FLAG_VTK_FROM` = the period
// index the export starts at (the last period by default; 0 = the whole onset from
// rest, the viewer's sequence mode of 2026-09-22).
let phases = env_f("RTX_E3_FLAG_PHASES", 32.0) as usize;
let vtk_from = env_f("RTX_E3_FLAG_VTK_FROM", (periods - 1.0).max(0.0)) as f64;
let last_period_start = vtk_from * period;
let total_phases = ((periods - vtk_from) * phases as f64).round() as usize;
let mut next_phase = 0;
let mid = nz / 2;
let slab = if slab_nz > 0 {
(0, nz)
} else {
(mid - 2, mid + 2)
};
let width = nz as f64 * h;
let start = std::time::Instant::now();
let mut drag_rec_sum = 0.0;
// The routes' PARTS over the whole body (x, per unit width): operator
// pressure / shear / exchange, reconstructed pressure / shear.
let mut parts = [[0.0_f64; 6]; 3];
let (mut drag_sum, mut lift_min, mut lift_max, mut samples) =
(0.0, f64::INFINITY, f64::NEG_INFINITY, 0usize);
let mut worst_residual = 0.0_f64;
for step in 0..steps {
let r = device.advance(dt);
worst_residual = worst_residual.max(r.final_residual);
assert!(r.final_residual.is_finite(), "death at step {step}");
let t = device.solver.time();
let sample = (step + 1) % 10 == 0 || step + 1 == steps;
let phase_due = vtk_dir.is_some()
&& t >= last_period_start + next_phase as f64 * period / phases as f64
&& next_phase < total_phases;
if sample || phase_due {
device.download(&mut field);
let solver = &device.solver;
let mask = solver.mask().expect("mask");
let body = solver.body().expect("body");
let fs = mask
.cut_wall_force_per_span(body, &field, RHO * NU, t, slab)
.expect("wall");
// The reconstructed wall route on the same slab, per span.
let fr = mask
.cut_wall_force_reconstructed(body, &field, RHO * NU, t, Some(slab))
.map(|v| {
let lz = (slab.1 - slab.0) as f64 * h;
[v[0] / lz, v[1] / lz, v[2] / lz]
})
.expect("reconstructed");
let ft = mask
.cut_wall_force(body, &field, RHO * NU, t)
.expect("wall");
// The tip's transverse deflection: the record's last station in
// recorded mode (until 2026-09-21 this column held the analytic
// first mode even then — R2's fits use the record directly).
let tip = match recorded() {
Some(rec) => rec.at(t, body_cy()).last().map_or(0.0, |p| p.1 - body_cy()),
None => deflection(1.0, t).0,
};
if sample {
println!(
" t {t:7.4} (tip {tip:+.4}): drag/span {:.1} lift/span {:+.1} N/m (reconstructed {:.1} {:+.1}); total {:.3} {:+.3} N; residual {:.1e} CG {} fresh {}; [{:.0} s]",
fs[0],
fs[1],
fr[0],
fr[1],
ft[0],
ft[1],
r.final_residual,
r.poisson_iterations,
r.fresh_cells,
start.elapsed().as_secs_f64()
);
if let Some(f) = csv.as_mut() {
writeln!(
f,
"{t:.5},{tip:.5},{:.4},{:.4},{:.5},{:.5},{:.3e},{},{},{:.4},{:.4}",
fs[0],
fs[1],
ft[0],
ft[1],
r.final_residual,
r.poisson_iterations,
r.fresh_cells,
fr[0],
fr[1]
)
.unwrap();
}
if t >= last_period_start {
use rtx_cfd::solvers::incompressible::embedded3::exchange::set_load_window;
// whole body, the cylinder (x < 0.252), the flag
for (w, window) in [None, Some((0.0, 0.252)), Some((0.252, 10.0))]
.into_iter()
.enumerate()
{
set_load_window(window);
let (po, so) = mask
.cut_wall_force_parts(body, &field, RHO * NU, t)
.expect("parts");
let (xd, xc) = mask
.cut_wall_exchange_parts(body, &field, RHO * NU, RHO, t, None)
.expect("exchange");
let (pr, sr) = mask
.cut_wall_force_reconstructed_parts(body, &field, RHO * NU, t, None)
.expect("reconstructed parts");
for (acc, v) in parts[w]
.iter_mut()
.zip([po[0], so[0], xd[0], xc[0], pr[0], sr[0]])
{
*acc += v / width;
}
}
set_load_window(None);
drag_sum += fs[0];
drag_rec_sum += fr[0];
lift_min = lift_min.min(fs[1]);
lift_max = lift_max.max(fs[1]);
samples += 1;
}
}
if phase_due {
let tag = if flag_span() >= duct_depth() { "full" } else { "free" };
let path = std::path::Path::new(vtk_dir.as_ref().unwrap())
.join(if total_phases > 100 {
format!("flag_{tag}_ny{ny}_phase{next_phase:03}.vtk")
} else {
format!("flag_{tag}_ny{ny}_phase{next_phase:02}.vtk")
});
write_vtk(&path, &field, Some(mask)).expect("vtk");
next_phase += 1;
}
}
}
let drag_mean = drag_sum / samples.max(1) as f64;
let drag_rec = drag_rec_sum / samples.max(1) as f64;
println!(
" FINAL ny {ny}: last period drag/span mean {drag_mean:.1} N/m (reconstructed {drag_rec:.1}; 2D FSI2 224.6, the 2D reference of this kinematics 208.3), lift/span {lift_min:+.1} … {lift_max:+.1} (2D ±215 flat tip, ±256 semicircle); worst residual {worst_residual:.1e}; {} phases written; {:.0} s",
next_phase,
start.elapsed().as_secs_f64()
);
let n = samples.max(1) as f64;
for (name, q) in ["whole body", "cylinder", "flag"].iter().zip(parts) {
println!(
" PARTS ny {ny} amp {:.3} {name} (x, N/m of width): operator pressure {:.2} + shear {:.2} + exchange diffusive {:.2} + convective {:.2} = {:.2}; reconstructed pressure {:.2} + shear {:.2} = {:.2}",
amplitude(),
q[0] / n,
q[1] / n,
q[2] / n,
q[3] / n,
(q[0] + q[1] + q[2] + q[3]) / n,
q[4] / n,
q[5] / n,
(q[4] + q[5]) / n
);
}
if let Some(t) = device.timers() {
println!(" timers: {t:?}");
}
}