R8-e: the coupled 3D FSI's small-dt instability located in the load/kinematics transfer, not a lagged fluid response

- tests/fsi2_embedded3_impulse.rs (new, ignored, cuda): the added-mass timing instrument — the R8-a fluid
  with a prescribed centreline velocity step (a one-step acceleration delta), baseline and impulse passes
  from one device snapshot, the load difference per step. Pitch shape on the slab ny 62: generalized added
  mass 39.11 / 39.09 / 39.07 kg/m at dt x 1 / 0.75 / 0.5, the step after carries 1.7 / 1.3 / 0.9 % above
  the steady tail — the cut-cell fluid answers in the same step (the lagged-added-mass hypothesis refuted).
- fsi2_embedded3.rs: `RTX_E3FSI_LINE_CSV` (per-step centreline + mid/bottom/top y per station) showed the
  growing 2dt mode is a through-thickness mode — the edge nodes move against the mid-plane nodes (~4:1):
  the fluid reads the mid-plane, the loads land on the edges, so the added mass acts on that mode with the
  wrong sign (a negative effective added mass; the scalar Newmark model is unstable for it at omega dt
  below ~1-2 with gamma 0.7, stable at larger dt or gamma 0.9 — R8-a's observed pattern).
  `RTX_E3FSI_KIN=surface` (default off, byte-identical when unset): the fluid's centreline = the mean of
  the bottom and top edge nodes (the nodes the loads land on) — dt x 0.75 / 0.5 / 0.25 stable at gamma 0.7.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-25 22:11:31 -05:00
co-authored by Claude Opus 5.5
parent 171da41ed1
commit 74fac32a91
2 changed files with 268 additions and 1 deletions
@@ -18,7 +18,11 @@
//! `RTOL` (1e-3), `FLOOR` (1.5e-7 on the centreline vector), `MAX_SUBIT`
//! (12), `STALL_ACCEPT` (5), `GAMMA` (0.7), `SPEED` (3.0 m/s, the band's
//! surface-speed bound), `CSV` (per-step series), `TRACE` (steps whose
//! passes are printed).
//! passes are printed); R8-e: `KIN` (`centre` = the mid-plane nodes drive
//! the fluid's centreline; `surface` = the mean of the bottom and top edge
//! nodes, the nodes the loads land on — the transfer-consistent pair that
//! removes the small-dt instability), `LINE_CSV` (the committed centreline
//! and the per-station mid/bottom/top y displacement per step).
//!
//! `RTX_E3FSI_NY=62 RTX_E3FSI_CSV=<path> RTX_CUDA_ARCH=sm_120 cargo test --release -p rtx-fsi \
//! --features cuda --test fsi2_embedded3 -- --ignored --nocapture`
@@ -67,6 +71,9 @@ struct Flag {
/// (reference y, wetted index), ascending (corners included).
tip: Vec<(f64, usize)>,
a_node: NodeId,
/// R8-e: the bottom and top edge nodes at each station's x (the wetted
/// surface the loads land on).
surface: Vec<[NodeId; 2]>,
}
impl Flag {
@@ -107,6 +114,12 @@ impl Flag {
top,
tip,
a_node: find(0.6, 0.2),
surface: (0..STATIONS)
.map(|k| {
let x = X0 + 0.01 * k as f64;
[find(x, 0.19), find(x, 0.21)]
})
.collect(),
}
}
}
@@ -287,6 +300,11 @@ fn fsi2_on_embedded3() {
.unwrap();
f
});
// R8-e `RTX_E3FSI_LINE_CSV`: the committed centreline per coupled step
// (t, then x, y displacement per station) — the unstable mode's shape.
let mut line_csv = std::env::var("RTX_E3FSI_LINE_CSV")
.ok()
.map(|p| std::fs::File::create(p).expect("line csv"));
let start = std::time::Instant::now();
// Phase 1: the rigid flag (target 1: the rest state vs CFD2 136.7 / 10.53).
@@ -380,8 +398,27 @@ fn fsi2_on_embedded3() {
})
.collect();
let a_dofs = flag.borrow().node_dofs(flag_geo.a_node);
// R8-e `RTX_E3FSI_KIN=surface`: the fluid's centreline is the mean of the
// bottom and top edge nodes at each station (the nodes the loads land on)
// instead of the mid-plane nodes (default `centre`).
let kin_surface = std::env::var("RTX_E3FSI_KIN").is_ok_and(|v| v == "surface");
let surface_dofs: Vec<[usize; 4]> = flag_geo
.surface
.iter()
.map(|[b, t]| {
let (db, dt_) = (flag.borrow().node_dofs(*b), flag.borrow().node_dofs(*t));
[db[0], db[1], dt_[0], dt_[1]]
})
.collect();
let extract = |s: &DynamicState| -> Vec<f64> {
let mut c = vec![0.0; 2 * STATIONS];
if kin_surface {
for (k, d) in surface_dofs.iter().enumerate() {
c[2 * k] = 0.5 * (s.displacement[d[0]] + s.displacement[d[2]]);
c[2 * k + 1] = 0.5 * (s.displacement[d[1]] + s.displacement[d[3]]);
}
return c;
}
for (k, d) in centre_dofs.iter().enumerate() {
c[2 * k] = s.displacement[d[0]];
c[2 * k + 1] = s.displacement[d[1]];
@@ -603,6 +640,16 @@ fn fsi2_on_embedded3() {
)
.unwrap();
}
if let Some(f) = line_csv.as_mut() {
// Then the y displacement of the mid-plane, bottom and top nodes per station.
let mut c: Vec<String> = c_fluid_n.iter().map(|v| format!("{v:.6e}")).collect();
for (k, d) in surface_dofs.iter().enumerate() {
let yc = flag_state.displacement[centre_dofs[k][1]];
let (yb, yt) = (flag_state.displacement[d[1]], flag_state.displacement[d[3]]);
c.extend([yc, yb, yt].map(|v| format!("{v:.6e}")));
}
writeln!(f, "{t_new:.6},{}", c.join(",")).unwrap();
}
let at_end = step + 1 == coupled_steps;
if let Some(dir) = save_dir
.as_ref()