rtx-fsi: time-resolved-honest load record — per-step sampling, interval median
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (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

The reported drag/lift were one instantaneous measure_force sample
every 10th coupled step, and the embedded-boundary surface force
carries zero-mean sign-flipping fresh-cell pressure transients at
step scale (measured: +-4,000-scale swings against a +-78 reference
while displacements matched the benchmark to 0.1%, and the window
MEDIANS sat near-physical). The record now samples the committed
field every step and records the interval median — the estimator the
CSV analysis supports; ten steps span 2-5% of a flap period, so
nothing physical is smeared.

Reporting only: measure_force reads the committed state. Verified on
both committed defaults against pre-change logs — every displacement,
conservation, and coupling digit identical; only the load lines
moved, and toward the benchmark: FSI3 drag 628.66 +- 545.11 ->
443.26 +- 148.50 (ref 460.2 +- 27.47, mid now within 3.7%), lift amp
2289 -> 668; FSI2 lift amp 52 -> 28.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
Omar Sobh
2026-08-27 14:58:17 -05:00
co-authored by Claude Fable 5
parent bff84ccdcc
commit 137a62c4ea
2 changed files with 35 additions and 2 deletions
@@ -16,7 +16,7 @@ use rtx_fea::materials::{LinearElastic, MaterialDatabase};
use rtx_fea::mesh::{MaterialId, NodeId};
use rtx_fsi::{IqnIls, Subiterated};
use super::{BenchmarkCase, Fsi2Harness, clamp_left, crossing_frequency, env_or, mid_amp};
use super::{BenchmarkCase, Fsi2Harness, clamp_left, crossing_frequency, env_or, median, mid_amp};
/// Everything a march run is parameterised by. `from_env` reads the
/// `RTX_<PREFIX>_*` knobs over a set of defaults.
@@ -372,6 +372,19 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
let mut force_times: Vec<f64> = Vec::new();
let mut drag_series: Vec<f64> = Vec::new();
let mut lift_series: Vec<f64> = Vec::new();
// Per-step load samples for the current recording interval. The
// recorded value is the interval MEDIAN, not one instantaneous
// sample: the embedded-boundary surface force carries zero-mean
// sign-flipping fresh-cell pressure transients at step scale
// (measured on the FSI2 s = 1 benchmark run: +-4,000-scale
// instantaneous swings against a +-78 reference while the window
// MEDIANS sat near-physical — drag 187 vs ref 215). The median
// rejects the transient outliers; ten coupled steps span 2-5% of a
// flap period, so nothing physical is smeared. Reporting only —
// measure_force reads the committed state and cannot perturb the
// trajectory.
let mut interval_drag: Vec<f64> = Vec::new();
let mut interval_lift: Vec<f64> = Vec::new();
let mut csv = csv_path
.as_ref()
.map(|p| std::fs::File::create(p).expect("csv path"));
@@ -554,8 +567,14 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
times.push(t);
ux_series.push(ux);
uy_series.push(uy);
let (drag_now, lift_now) = harness.measure_force(&solver.borrow(), &field.borrow());
interval_drag.push(drag_now);
interval_lift.push(lift_now);
if (step + 1) % 10 == 0 {
let (drag, lift) = harness.measure_force(&solver.borrow(), &field.borrow());
let drag = median(&mut interval_drag);
let lift = median(&mut interval_lift);
interval_drag.clear();
interval_lift.clear();
force_times.push(t);
drag_series.push(drag);
lift_series.push(lift);
@@ -284,6 +284,20 @@ pub fn mid_amp(series: &[f64]) -> (f64, f64) {
(0.5 * (max + min), 0.5 * (max - min))
}
/// Median of a sample buffer (sorts in place; empty buffers read 0).
pub fn median(samples: &mut [f64]) -> f64 {
if samples.is_empty() {
return 0.0;
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = samples.len();
if n % 2 == 1 {
samples[n / 2]
} else {
0.5 * (samples[n / 2 - 1] + samples[n / 2])
}
}
/// Frequency from linearly interpolated upward crossings of the mean.
pub fn crossing_frequency(times: &[f64], series: &[f64]) -> Option<f64> {
let (mean, _) = mid_amp(series);