use crate::math;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sample {
pub bpfi_amplitude: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Baseline {
pub mu_healthy: f64,
}
impl Baseline {
#[must_use]
pub fn from_healthy(healthy: &[f64]) -> Option<Self> {
debug_assert!(healthy.len() <= 1_000_000, "healthy window unreasonably large");
let mu = math::finite_mean(healthy)?;
debug_assert!(mu.is_finite(), "finite_mean returns Some only for finite values");
Some(Self { mu_healthy: mu })
}
#[inline]
#[must_use]
pub fn residual(&self, sample: Sample) -> f64 {
debug_assert!(self.mu_healthy.is_finite(), "calibrated baseline must be finite");
let r = math::abs_f64(sample.bpfi_amplitude - self.mu_healthy);
debug_assert!(r >= 0.0 || !r.is_finite(), "residual is non-negative or non-finite");
r
}
}
pub fn residual_stream(samples: &[Sample], baseline: Baseline, out: &mut [f64]) -> usize {
debug_assert!(baseline.mu_healthy.is_finite(), "baseline must be calibrated");
let n = samples.len().min(out.len());
debug_assert!(n <= out.len() && n <= samples.len(), "n respects both bounds");
let mut i = 0_usize;
while i < n {
out[i] = baseline.residual(samples[i]);
i += 1;
}
debug_assert_eq!(i, n, "loop must run exactly n iterations");
n
}
pub const HEALTHY_FIXTURE: [f64; 6] = [0.10, 0.11, 0.09, 0.10, 0.10, 0.11];
pub const FAULTED_FIXTURE: [Sample; 5] = [
Sample { bpfi_amplitude: 0.10 },
Sample { bpfi_amplitude: 0.12 },
Sample { bpfi_amplitude: 0.20 },
Sample { bpfi_amplitude: 0.35 },
Sample { bpfi_amplitude: 0.15 },
];
pub fn fixture_residuals(out: &mut [f64]) -> usize {
let Some(baseline) = Baseline::from_healthy(&HEALTHY_FIXTURE) else {
debug_assert!(false, "HEALTHY_FIXTURE is non-empty + finite — calibration must succeed");
return 0;
};
residual_stream(&FAULTED_FIXTURE, baseline, out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn baseline_from_empty_is_none() {
assert!(Baseline::from_healthy(&[]).is_none());
}
#[test]
fn healthy_window_gives_near_zero_residual_for_nominal_sample() {
let baseline = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
let r = baseline.residual(Sample { bpfi_amplitude: 0.10 });
assert!(r < 0.02);
}
#[test]
fn faulted_sample_has_elevated_residual() {
let baseline = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
let r = baseline.residual(Sample { bpfi_amplitude: 0.35 });
assert!(r > 0.20);
}
#[test]
fn stream_trajectory_peaks_at_fault_sample() {
let baseline = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
let mut out = [0.0_f64; 5];
let n = residual_stream(&FAULTED_FIXTURE, baseline, &mut out);
assert_eq!(n, 5);
let peak = out.iter().copied().fold(0.0_f64, f64::max);
assert!((peak - out[3]).abs() < 1e-12, "peak should be index 3");
}
}