diff --git a/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs b/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs index 494d522..111a8ec 100644 --- a/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs +++ b/crates/specialized/rtx-fsi/tests/fsi2_harness/overset_march.rs @@ -199,6 +199,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov // of the flag's high-frequency modes (the thickness breathing the // body-fitted wall couples to at s = 1); the benchmark's average // acceleration (γ = ½) has none. + let newmark_beta: f64; let analysis = match std::env::var("RTX_FSI2O_NEWMARK_GAMMA") .ok() .and_then(|v| v.parse::().ok()) @@ -206,9 +207,13 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov Some(g) => { let b = (g + 0.5).powi(2) / 4.0; println!(" Newmark γ = {g}, β = {b:.4}"); + newmark_beta = b; analysis.with_newmark_parameters(g, b) } - None => analysis, + None => { + newmark_beta = 0.25; + analysis + } }; let flag = RefCell::new(analysis.stepper().unwrap()); let wetted_dofs: Vec<[usize; 2]> = fluid @@ -269,6 +274,27 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov fict_per_node * wetted_dofs.len() as f64 ); } + // The Newmark acceleration a CANDIDATE displacement implies at the wetted + // DoFs, a(d) = (d − u_pred) / (β Δt²) with u_pred from the committed + // state — the acceleration the structure itself would report at d, so + // the compensating load M_f a(d) is consistent with the relaxed candidate + // the fluid was evaluated at, and cancels exactly at the fixed point + // (a lagged structure-output acceleration does not: 09-16's first try + // made the loop SLOWER, 1.8 → 5.2 subit/step at α 0 → 2). + let accel_of = |d_candidate: &[f64], committed: &DynamicState| -> Vec { + let inv = 1.0 / (newmark_beta * dt * dt); + let mut a = vec![0.0; 2 * wetted_dofs.len()]; + for (k, dofs) in wetted_dofs.iter().enumerate() { + for c in 0..2 { + let dof = dofs[c]; + let u_pred = committed.displacement[dof] + + dt * committed.velocity[dof] + + dt * dt * (0.5 - newmark_beta) * committed.acceleration[dof]; + a[2 * k + c] = inv * (d_candidate[2 * k + c] - u_pred); + } + } + a + }; // The load with the compensating term for a given previous-subiterate acceleration. let with_fict = |nodal: &[(NodeId, Vector3)], accel: &[f64]| -> Vec<(NodeId, Vector3)> { @@ -345,11 +371,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov .unwrap_or(0); let phase_start = std::time::Instant::now(); - // The previous subiterate's interface acceleration (the fictitious mass's - // compensating load): the committed state's at each step's start. - let last_accel: RefCell> = RefCell::new(extract_accel(&flag_state)); for step in 0..coupled_steps { - last_accel.replace(extract_accel(&flag_state)); let d_n = extract(&flag_state); let v_n: Option> = cfg.c1_interface.then(|| extract_velocity(&flag_state)); let d_predicted = if cfg.predictor == "kinematic" { @@ -359,7 +381,6 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov flag.borrow_mut() .set_nodal_forces(&with_fict(&committed_nodal, &extract_accel(&flag_state))); let (predicted, _) = flag.borrow_mut().step(&flag_state).unwrap(); - last_accel.replace(extract_accel(&predicted)); extract(&predicted) }; let saved = fluid.borrow().snapshot(); @@ -375,9 +396,8 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov t_fluid.set(t_fluid.get() + fs.elapsed().as_secs_f64()); let ss = std::time::Instant::now(); let mut flag_ref = flag.borrow_mut(); - flag_ref.set_nodal_forces(&with_fict(&nodal, &last_accel.borrow())); + flag_ref.set_nodal_forces(&with_fict(&nodal, &accel_of(d_candidate, &flag_state))); let (candidate_state, _) = flag_ref.step(&flag_state).unwrap(); - last_accel.replace(extract_accel(&candidate_state)); t_structure.set(t_structure.get() + ss.elapsed().as_secs_f64()); let d_new = extract(&candidate_state); if step < cfg.trace_steps {