#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sample {
pub vib_hi: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Baseline {
pub hi_calib: f64,
}
impl Baseline {
#[must_use]
pub fn from_healthy(healthy: &[f64]) -> Option<Self> {
debug_assert!(healthy.len() <= 1_000_000, "calibration window unreasonably large");
let mu = crate::math::finite_mean(healthy)?;
debug_assert!(mu.is_finite(), "finite_mean returns Some only for finite values");
Some(Self { hi_calib: mu })
}
#[inline]
#[must_use]
pub fn residual_norm(&self, sample: Sample) -> f64 {
debug_assert!(self.hi_calib.is_finite(), "calibrated HI must be finite");
let r = crate::math::abs_f64(sample.vib_hi - self.hi_calib);
debug_assert!(r >= 0.0 || !r.is_finite(), "norm is non-negative or non-finite");
r
}
}
pub fn residual_stream(samples: &[Sample], baseline: Baseline, out: &mut [f64]) -> usize {
debug_assert!(baseline.hi_calib.is_finite(), "baseline must be calibrated");
let n = samples.len().min(out.len());
debug_assert!(n <= out.len() && n <= samples.len(), "n bounded by both buffers");
let mut i = 0_usize;
while i < n {
out[i] = baseline.residual_norm(samples[i]);
i += 1;
}
debug_assert_eq!(i, n, "loop must run exactly n iterations");
n
}
pub const HEALTHY_FIXTURE: [f64; 5] = [0.02, 0.02, 0.03, 0.02, 0.02];
pub const ACCELERATED_FIXTURE: [Sample; 5] = [
Sample { vib_hi: 0.02 },
Sample { vib_hi: 0.05 },
Sample { vib_hi: 0.12 },
Sample { vib_hi: 0.30 },
Sample { vib_hi: 0.80 },
];
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(&ACCELERATED_FIXTURE, baseline, out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn residual_increases_through_accelerated_aging() {
let b = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
let mut out = [0.0_f64; 5];
let n = residual_stream(&ACCELERATED_FIXTURE, b, &mut out);
assert_eq!(n, 5);
for i in 1..n {
assert!(out[i] >= out[i - 1], "accelerated aging → monotone residual, got {out:?}");
}
}
#[test]
fn nominal_healthy_calibration_matches_baseline_mean() {
let b = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
assert!((b.hi_calib - 0.022).abs() < 1e-3);
}
#[test]
fn residual_norm_is_non_negative() {
let b = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
for h in [-1.0, 0.0, 0.5, 1.0] {
let r = b.residual_norm(Sample { vib_hi: h });
assert!(r >= 0.0);
}
}
}