///|
/// Robust baseline estimate for a force plate recording.
pub struct BaselineCalibration {
offset_fx : Double
offset_fy : Double
offset_fz : Double
noise_fx : Double
noise_fy : Double
noise_fz : Double
baseline_samples : Int
stable : Bool
} derive(Debug, Eq)
///|
fn finite_prefix(values : Array[Double], count : Int) -> Array[Double] {
let range = clamp_range(values.length(), 0, count)
let result : Array[Double] = []
for i = range.start; i < range.end; i = i + 1 {
if !values[i].is_nan() && !values[i].is_inf() {
result.push(values[i])
}
}
result
}
///|
/// Estimates tare offsets with medians and noise with scaled MAD.
pub fn estimate_baseline_calibration(
trial : ForceTrial,
baseline_samples : Int,
stability_threshold_n : Double,
) -> BaselineCalibration {
let fx = finite_prefix(trial.get_fx(), baseline_samples)
let fy = finite_prefix(trial.get_fy(), baseline_samples)
let fz = finite_prefix(trial.get_fz(), baseline_samples)
let offset_fx = median(fx)
let offset_fy = median(fy)
let offset_fz = median(fz)
let noise_fx = mad(fx) * 1.4826
let noise_fy = mad(fy) * 1.4826
let noise_fz = mad(fz) * 1.4826
let threshold = if stability_threshold_n < 0.0 {
0.0
} else {
stability_threshold_n
}
let stable = (fx.length() > 0 || fy.length() > 0 || fz.length() > 0) &&
noise_fx <= threshold &&
noise_fy <= threshold &&
noise_fz <= threshold
{
offset_fx,
offset_fy,
offset_fz,
noise_fx,
noise_fy,
noise_fz,
baseline_samples: if fx.length() < fy.length() {
if fx.length() < fz.length() {
fx.length()
} else {
fz.length()
}
} else if fy.length() < fz.length() {
fy.length()
} else {
fz.length()
},
stable,
}
}
///|
/// Applies robust baseline offsets while preserving the trial timebase.
pub fn apply_baseline_tare(
trial : ForceTrial,
baseline : BaselineCalibration,
) -> ForceTrial {
trial.apply_calibration(
CalibrationParams::new(
1.0,
1.0,
1.0,
baseline.offset_fx,
baseline.offset_fy,
baseline.offset_fz,
0.0,
),
)
}
///|
/// Raw-signal diagnostics for sensor saturation and finite data coverage.
pub struct CalibrationDiagnostics {
sample_count : Int
finite_samples : Int
saturated_samples : Int
maximum_abs_force_n : Double
saturation_limit_n : Double
} derive(Debug, Eq)
///|
pub fn inspect_calibration(
trial : ForceTrial,
saturation_limit_n : Double,
) -> CalibrationDiagnostics {
let limit = if saturation_limit_n <= 0.0 || saturation_limit_n.is_nan() {
0.0
} else {
saturation_limit_n
}
let mut finite = 0
let mut saturated = 0
let mut maximum = 0.0
for sample in trial.samples {
let force = sample.force
let valid = !force.x.is_nan() &&
!force.x.is_inf() &&
!force.y.is_nan() &&
!force.y.is_inf() &&
!force.z.is_nan() &&
!force.z.is_inf()
if valid {
finite = finite + 1
let ax = force.x.abs()
let ay = force.y.abs()
let az = force.z.abs()
if ax > maximum {
maximum = ax
}
if ay > maximum {
maximum = ay
}
if az > maximum {
maximum = az
}
if limit > 0.0 && (ax >= limit || ay >= limit || az >= limit) {
saturated = saturated + 1
}
}
}
{
sample_count: trial.length(),
finite_samples: finite,
saturated_samples: saturated,
maximum_abs_force_n: maximum,
saturation_limit_n: limit,
}
}