P6-b: the fictitious added mass for the partitioned loop — rtx-fea NonlinearDynamicStepper::set_added_lumped_mass (a lumped per-DOF mass in the Newmark inertial residual and effective tangent, never the consistent mass or the rest state; zero = the plain stepper bit for bit) with its pin (compensated step reproduces the plain step to 2.5e-9, uncompensated moves it 11 %); the FSI2 overset harness carries RTX_FSI2O_FICT_MASS=α (α × ρ_f π (c/2)² spread over the wetted nodes) and adds the compensating load M_f ü_k of the previous subiterate to every structure solve (predictor and passes), printed in the header
CI / Format Check (push) Failing after 7s
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Clippy Check (push) Failing after 5s
CI / Build (ubuntu-latest) (push) Failing after 5s
Performance Benchmarks / Run Benchmarks (push) Failing after 6s
Documentation / Build User Guide (push) Successful in 6s
Documentation / Build API Documentation (push) Failing after 25s
CI / Build CPU-Only (Explicit) (push) Failing after 1m15s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
This commit is contained in:
Omar Sobh
2026-09-15 08:30:55 -05:00
co-authored by Claude Fable 5.1
parent e54729241d
commit a2086a59de
3 changed files with 289 additions and 5 deletions
@@ -100,7 +100,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
// Every setting the acceptance rule reads, printed once: P5-3 lost a
// day to a floor of 2e-4 against the overnight marches' 1e-6.
println!(
" coupling: {} (reuse {}, ω0 {}, c1 {}), floor {:.1e}, rtol {:.1e}, stall accept {:.1e}, max subit {}, predictor {}, s = {}, patch offset {} h × {} rows, patch convection {:?}, bg convection {:?}, patch stretch {}, fillet {} m, tip corner {} m",
" coupling: {} (reuse {}, ω0 {}, c1 {}), floor {:.1e}, rtol {:.1e}, stall accept {:.1e}, max subit {}, predictor {}, s = {}, patch offset {} h × {} rows, patch convection {:?}, bg convection {:?}, patch stretch {}, fillet {} m, tip corner {} m, fict mass α {}",
cfg.coupler,
cfg.reuse,
cfg.initial_relaxation,
@@ -118,6 +118,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
super::overset::patch_stretch(),
super::overset::fillet(),
super::overset::tip_corner(),
std::env::var("RTX_FSI2O_FICT_MASS").unwrap_or_else(|_| "0".into()),
);
// Phase 1: rigid flag to t_release (`RTX_FSI2O_LOAD=dir` replaces the
@@ -236,6 +237,57 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
}
v
};
let extract_accel = |state: &DynamicState| -> Vec<f64> {
let mut a = vec![0.0; 2 * wetted_dofs.len()];
for (k, dofs) in wetted_dofs.iter().enumerate() {
a[2 * k] = state.acceleration[dofs[0]];
a[2 * k + 1] = state.acceleration[dofs[1]];
}
a
};
// P6-b (`docs/overset_metal_campaign.md` §5.17): the fictitious added
// mass — `RTX_FSI2O_FICT_MASS=α` puts α × ρ_f π (c/2)² (the flag's heave
// added mass per unit depth, c = 0.35) as a lumped mass spread over the
// wetted nodes, and every structure solve carries the compensating load
// M_f ü_k of the previous subiterate, so the fixed point is unchanged
// and the loop contracts at any mass ratio. α = 0 is the plain loop.
let fict_alpha: f64 = std::env::var("RTX_FSI2O_FICT_MASS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0.0);
let fict_per_node = fict_alpha * 1000.0 * std::f64::consts::PI * (0.35_f64 / 2.0).powi(2)
/ wetted_dofs.len() as f64;
let wetted_nodes: Vec<NodeId> = fluid.interface.wetted.clone();
if fict_alpha != 0.0 {
let entries: Vec<(NodeId, f64)> =
wetted_nodes.iter().map(|&n| (n, fict_per_node)).collect();
flag.borrow_mut().set_added_lumped_mass(&entries);
println!(
" fictitious added mass: α = {fict_alpha}, {:.3} kg per wetted node ({} nodes, {:.1} kg total)",
fict_per_node,
wetted_dofs.len(),
fict_per_node * wetted_dofs.len() as f64
);
}
// The load with the compensating term for a given previous-subiterate acceleration.
let with_fict =
|nodal: &[(NodeId, Vector3<f64>)], accel: &[f64]| -> Vec<(NodeId, Vector3<f64>)> {
if fict_alpha == 0.0 {
return nodal.to_vec();
}
let mut out = nodal.to_vec();
for (k, &n) in wetted_nodes.iter().enumerate() {
out.push((
n,
Vector3::new(
fict_per_node * accel[2 * k],
fict_per_node * accel[2 * k + 1],
0.0,
),
));
}
out
};
// Phase 2: release under the current load.
let (nodal0, conservation0, faces0) = fluid.sample_load(&zero_d);
@@ -293,15 +345,21 @@ 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<Vec<f64>> = 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<Vec<f64>> = cfg.c1_interface.then(|| extract_velocity(&flag_state));
let d_predicted = if cfg.predictor == "kinematic" {
let v = extract_velocity(&flag_state);
d_n.iter().zip(&v).map(|(d, v)| d + dt * v).collect()
} else {
flag.borrow_mut().set_nodal_forces(&committed_nodal);
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();
@@ -317,8 +375,9 @@ 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(&nodal);
flag_ref.set_nodal_forces(&with_fict(&nodal, &last_accel.borrow()));
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 {