Skip to main content

dsfb_robotics/datasets/
ims.rs

1//! IMS Run-to-Failure bearing adapter.
2//!
3//! **Provenance.** NASA Prognostics Data Repository, *"Bearing Data
4//! Set"* provided by the Center for Intelligent Maintenance Systems
5//! (IMS), University of Cincinnati (Lee, Qiu, Yu, Lin et al. 2007).
6//! Three test-to-failure experiments, four bearings per shaft,
7//! 20 kHz sampling, 10-minute snapshots over ≈35 days total run time.
8//!
9//! **Residual DSFB structures.** The adapter consumes a per-snapshot
10//! **health-index (HI)** trajectory — typically a vibration RMS, kurtosis,
11//! or PCA-derived scalar that a Rainflow / RUL estimator collapses to
12//! a single remaining-useful-life number and discards. DSFB reads the
13//! residual between the HI trajectory and the nominal HI calibrated
14//! from an early-life healthy window:
15//!
16//! ```text
17//! r(k) = HI(k) − HI_nominal
18//! ```
19//!
20//! The sign is preserved (not absolute-valued) so DSFB can distinguish
21//! monotonic degradation (positive residual trajectory) from transient
22//! operational excursions (bi-directional residual). The grammar FSM
23//! treats both symmetrically via the `norm = |r|` view.
24
25/// Per-snapshot health-index sample.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct Sample {
28    /// Caller-computed bearing health index. The exact HI formula
29    /// (RMS, kurtosis, PCA-K1, MF-DFA, etc.) is up to the caller —
30    /// DSFB treats this as an opaque scalar residual source.
31    pub health_index: f64,
32}
33
34/// Calibrated HI baseline.
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct Baseline {
37    /// Nominal HI value calibrated from an early-life healthy
38    /// snapshot window.
39    pub hi_nominal: f64,
40}
41
42impl Baseline {
43    /// Calibrate from a healthy snapshot window.
44    #[must_use]
45    pub fn from_healthy(healthy: &[f64]) -> Option<Self> {
46        debug_assert!(healthy.len() <= 1_000_000, "calibration window unreasonably large");
47        let mu = crate::math::finite_mean(healthy)?;
48        debug_assert!(mu.is_finite(), "finite_mean returns Some only for finite values");
49        Some(Self { hi_nominal: mu })
50    }
51
52    /// Signed residual `HI(k) − HI_nominal` for one sample.
53    #[inline]
54    #[must_use]
55    pub fn residual(&self, sample: Sample) -> f64 {
56        debug_assert!(self.hi_nominal.is_finite(), "calibrated nominal must be finite");
57        sample.health_index - self.hi_nominal
58    }
59
60    /// Magnitude residual `|HI(k) − HI_nominal|` for one sample. This
61    /// is the form the engine's `‖r‖` field consumes.
62    #[inline]
63    #[must_use]
64    pub fn residual_norm(&self, sample: Sample) -> f64 {
65        debug_assert!(self.hi_nominal.is_finite(), "calibrated nominal must be finite");
66        let r = crate::math::abs_f64(self.residual(sample));
67        debug_assert!(r >= 0.0 || !r.is_finite(), "norm is non-negative or non-finite");
68        r
69    }
70}
71
72/// Stream a per-snapshot HI slice into a residual-norm buffer.
73pub fn residual_stream(samples: &[Sample], baseline: Baseline, out: &mut [f64]) -> usize {
74    debug_assert!(baseline.hi_nominal.is_finite(), "baseline must be calibrated");
75    let n = samples.len().min(out.len());
76    debug_assert!(n <= out.len() && n <= samples.len(), "n respects both bounds");
77    let mut i = 0_usize;
78    while i < n {
79        out[i] = baseline.residual_norm(samples[i]);
80        i += 1;
81    }
82    debug_assert_eq!(i, n, "loop must run exactly n iterations");
83    n
84}
85
86/// Healthy-window calibration slice for smoke-test reproductions.
87pub const HEALTHY_FIXTURE: [f64; 5] = [0.05, 0.06, 0.05, 0.05, 0.06];
88
89/// Run-to-failure trajectory for smoke-test reproductions.
90pub const RUN_TO_FAILURE_FIXTURE: [Sample; 6] = [
91    Sample { health_index: 0.05 },
92    Sample { health_index: 0.06 },
93    Sample { health_index: 0.08 },
94    Sample { health_index: 0.12 },
95    Sample { health_index: 0.20 },
96    Sample { health_index: 0.35 },
97];
98
99/// Calibrate from [`HEALTHY_FIXTURE`] and stream
100/// [`RUN_TO_FAILURE_FIXTURE`] residuals into `out`. Returns the
101/// number written.
102pub fn fixture_residuals(out: &mut [f64]) -> usize {
103    let Some(baseline) = Baseline::from_healthy(&HEALTHY_FIXTURE) else {
104        debug_assert!(false, "HEALTHY_FIXTURE is non-empty + finite — calibration must succeed");
105        return 0;
106    };
107    residual_stream(&RUN_TO_FAILURE_FIXTURE, baseline, out)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn signed_residual_preserves_direction() {
116        let b = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
117        let above = Sample { health_index: 0.10 };
118        let below = Sample { health_index: 0.01 };
119        assert!(b.residual(above) > 0.0);
120        assert!(b.residual(below) < 0.0);
121    }
122
123    #[test]
124    fn magnitude_residual_is_non_negative() {
125        let b = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
126        for h in [0.01, 0.05, 0.10, 0.35] {
127            assert!(b.residual_norm(Sample { health_index: h }) >= 0.0);
128        }
129    }
130
131    #[test]
132    fn run_to_failure_residuals_are_monotone() {
133        let b = Baseline::from_healthy(&HEALTHY_FIXTURE).expect("finite");
134        let mut out = [0.0_f64; 6];
135        let n = residual_stream(&RUN_TO_FAILURE_FIXTURE, b, &mut out);
136        assert_eq!(n, 6);
137        // Fixture is monotonically increasing — residual-norm must also be.
138        for i in 1..n {
139            assert!(out[i] >= out[i - 1], "expected non-decreasing, got {out:?}");
140        }
141    }
142}