dsfb_robotics/datasets/
cwru.rs1use crate::math;
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct Sample {
30 pub bpfi_amplitude: f64,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Baseline {
43 pub mu_healthy: f64,
45}
46
47impl Baseline {
48 #[must_use]
52 pub fn from_healthy(healthy: &[f64]) -> Option<Self> {
53 debug_assert!(healthy.len() <= 1_000_000, "healthy window unreasonably large");
54 let mu = math::finite_mean(healthy)?;
55 debug_assert!(mu.is_finite(), "finite_mean returns Some only for finite values");
56 Some(Self { mu_healthy: mu })
57 }
58
59 #[inline]
61 #[must_use]
62 pub fn residual(&self, sample: Sample) -> f64 {
63 debug_assert!(self.mu_healthy.is_finite(), "calibrated baseline must be finite");
64 let r = math::abs_f64(sample.bpfi_amplitude - self.mu_healthy);
65 debug_assert!(r >= 0.0 || !r.is_finite(), "residual is non-negative or non-finite");
66 r
67 }
68}
69
70pub fn residual_stream(samples: &[Sample], baseline: Baseline, out: &mut [f64]) -> usize {
73 debug_assert!(baseline.mu_healthy.is_finite(), "baseline must be calibrated");
74 let n = samples.len().min(out.len());
75 debug_assert!(n <= out.len() && n <= samples.len(), "n respects both bounds");
76 let mut i = 0_usize;
77 while i < n {
78 out[i] = baseline.residual(samples[i]);
79 i += 1;
80 }
81 debug_assert_eq!(i, n, "loop must run exactly n iterations");
82 n
83}
84
85pub const HEALTHY_FIXTURE: [f64; 6] = [0.10, 0.11, 0.09, 0.10, 0.10, 0.11];
87
88pub const FAULTED_FIXTURE: [Sample; 5] = [
90 Sample { bpfi_amplitude: 0.10 },
91 Sample { bpfi_amplitude: 0.12 },
92 Sample { bpfi_amplitude: 0.20 },
93 Sample { bpfi_amplitude: 0.35 },
94 Sample { bpfi_amplitude: 0.15 },
95];
96
97pub fn fixture_residuals(out: &mut [f64]) -> usize {
101 let Some(baseline) = Baseline::from_healthy(&HEALTHY_FIXTURE) else {
102 debug_assert!(false, "HEALTHY_FIXTURE is non-empty + finite — calibration must succeed");
103 return 0;
104 };
105 residual_stream(&FAULTED_FIXTURE, baseline, out)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn baseline_from_empty_is_none() {
114 assert!(Baseline::from_healthy(&[]).is_none());
115 }
116
117 #[test]
118 fn healthy_window_gives_near_zero_residual_for_nominal_sample() {
119 let baseline = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
120 let r = baseline.residual(Sample { bpfi_amplitude: 0.10 });
121 assert!(r < 0.02);
122 }
123
124 #[test]
125 fn faulted_sample_has_elevated_residual() {
126 let baseline = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
127 let r = baseline.residual(Sample { bpfi_amplitude: 0.35 });
128 assert!(r > 0.20);
129 }
130
131 #[test]
132 fn stream_trajectory_peaks_at_fault_sample() {
133 let baseline = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
134 let mut out = [0.0_f64; 5];
135 let n = residual_stream(&FAULTED_FIXTURE, baseline, &mut out);
136 assert_eq!(n, 5);
137 let peak = out.iter().copied().fold(0.0_f64, f64::max);
138 assert!((peak - out[3]).abs() < 1e-12, "peak should be index 3");
139 }
140}