Skip to main content

u_analytics/weibull/
reliability.rs

1//! Reliability analysis from fitted Weibull parameters.
2//!
3//! Provides reliability function, hazard rate, MTBF, B-life, and other
4//! common reliability engineering metrics.
5
6use u_numflow::special::gamma;
7
8/// Reliability analysis results from a fitted Weibull distribution.
9///
10/// Computes reliability engineering metrics (reliability function, hazard
11/// rate, MTBF, B-life) from Weibull shape (beta) and scale (eta) parameters.
12///
13/// # Mathematical Background
14///
15/// Given a Weibull distribution with shape beta > 0 and scale eta > 0:
16/// - Reliability: R(t) = exp(-(t/eta)^beta)
17/// - Hazard rate: lambda(t) = (beta/eta) * (t/eta)^(beta-1)
18/// - MTBF: eta * Gamma(1 + 1/beta)
19///
20/// # Examples
21///
22/// ```
23/// use u_analytics::weibull::ReliabilityAnalysis;
24/// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
25/// assert!((ra.reliability(0.0) - 1.0).abs() < 1e-10);
26/// assert!(ra.hazard_rate(50.0) > 0.0);
27/// assert!(ra.mtbf() > 0.0);
28/// let b10 = ra.b_life(0.10).unwrap();
29/// assert!(b10 > 0.0 && b10 < 100.0);
30/// ```
31///
32/// # Reference
33/// Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, Wiley.
34#[derive(Debug, Clone)]
35pub struct ReliabilityAnalysis {
36    /// Shape parameter (beta).
37    shape: f64,
38    /// Scale parameter (eta).
39    scale: f64,
40}
41
42impl ReliabilityAnalysis {
43    /// Creates a new reliability analysis from Weibull parameters.
44    ///
45    /// # Arguments
46    /// * `shape` - Shape parameter beta (must be positive and finite)
47    /// * `scale` - Scale parameter eta (must be positive and finite)
48    ///
49    /// # Returns
50    /// `None` if either parameter is non-positive or non-finite.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use u_analytics::weibull::ReliabilityAnalysis;
56    /// let ra = ReliabilityAnalysis::new(2.0, 100.0);
57    /// assert!(ra.is_some());
58    ///
59    /// // Invalid parameters return None
60    /// assert!(ReliabilityAnalysis::new(-1.0, 100.0).is_none());
61    /// assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
62    /// ```
63    pub fn new(shape: f64, scale: f64) -> Option<Self> {
64        if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
65            return None;
66        }
67        Some(Self { shape, scale })
68    }
69
70    /// Creates a reliability analysis from an MLE fitting result.
71    ///
72    /// # Panics
73    /// This function does not panic. MLE results always have valid parameters.
74    pub fn from_mle(result: &super::mle::WeibullMleResult) -> Self {
75        Self {
76            shape: result.shape,
77            scale: result.scale,
78        }
79    }
80
81    /// Creates a reliability analysis from an MRR fitting result.
82    ///
83    /// # Panics
84    /// This function does not panic. MRR results always have valid parameters.
85    pub fn from_mrr(result: &super::mrr::WeibullMrrResult) -> Self {
86        Self {
87            shape: result.shape,
88            scale: result.scale,
89        }
90    }
91
92    /// Returns the shape parameter (beta).
93    pub fn shape(&self) -> f64 {
94        self.shape
95    }
96
97    /// Returns the scale parameter (eta).
98    pub fn scale(&self) -> f64 {
99        self.scale
100    }
101
102    /// Reliability (survival) function at time t.
103    ///
104    /// ```text
105    /// R(t) = exp(-(t/eta)^beta)
106    /// ```
107    ///
108    /// For t < 0, returns 1.0 (no failure before time 0).
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use u_analytics::weibull::ReliabilityAnalysis;
114    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
115    ///
116    /// // R(0) = 1.0 (full reliability at time zero)
117    /// assert!((ra.reliability(0.0) - 1.0).abs() < 1e-10);
118    ///
119    /// // R(eta) = exp(-1) ≈ 0.368 for any shape parameter
120    /// let expected = (-1.0_f64).exp();
121    /// assert!((ra.reliability(100.0) - expected).abs() < 1e-10);
122    /// ```
123    ///
124    /// # Reference
125    /// Weibull (1951), *Journal of Applied Mechanics* 18(3), pp. 293-297.
126    pub fn reliability(&self, t: f64) -> f64 {
127        if t <= 0.0 {
128            return 1.0;
129        }
130        let z = t / self.scale;
131        (-z.powf(self.shape)).exp()
132    }
133
134    /// Failure rate (hazard function) at time t.
135    ///
136    /// ```text
137    /// lambda(t) = (beta/eta) * (t/eta)^(beta-1)
138    /// ```
139    ///
140    /// - beta < 1: Decreasing failure rate (infant mortality)
141    /// - beta = 1: Constant failure rate (random/exponential failures)
142    /// - beta > 1: Increasing failure rate (wear-out)
143    ///
144    /// For t <= 0, returns 0.0.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use u_analytics::weibull::ReliabilityAnalysis;
150    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
151    ///
152    /// // Hazard rate is positive for t > 0
153    /// assert!(ra.hazard_rate(50.0) > 0.0);
154    ///
155    /// // With beta > 1, hazard rate increases over time (wear-out)
156    /// assert!(ra.hazard_rate(50.0) < ra.hazard_rate(80.0));
157    /// ```
158    ///
159    /// # Reference
160    /// Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, Ch. 4.
161    pub fn hazard_rate(&self, t: f64) -> f64 {
162        if t <= 0.0 {
163            return 0.0;
164        }
165        let z = t / self.scale;
166        (self.shape / self.scale) * z.powf(self.shape - 1.0)
167    }
168
169    /// Mean Time Between Failures (MTBF).
170    ///
171    /// ```text
172    /// MTBF = eta * Gamma(1 + 1/beta)
173    /// ```
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use u_analytics::weibull::ReliabilityAnalysis;
179    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
180    /// let mtbf = ra.mtbf();
181    /// assert!(mtbf > 0.0);
182    ///
183    /// // For beta=1 (exponential), MTBF = eta
184    /// let exp_ra = ReliabilityAnalysis::new(1.0, 50.0).unwrap();
185    /// assert!((exp_ra.mtbf() - 50.0).abs() < 1e-8);
186    /// ```
187    ///
188    /// # Reference
189    /// Johnson, Kotz & Balakrishnan (1994), *Continuous Univariate Distributions*,
190    /// Vol. 1, Chapter 21.
191    pub fn mtbf(&self) -> f64 {
192        self.scale * gamma(1.0 + 1.0 / self.shape)
193    }
194
195    /// Time at which reliability drops to a given level.
196    ///
197    /// Solves R(t) = p for t:
198    ///
199    /// ```text
200    /// t = eta * (-ln(p))^(1/beta)
201    /// ```
202    ///
203    /// # Arguments
204    /// * `p` - Desired reliability level, must be in (0, 1).
205    ///
206    /// # Returns
207    /// `None` if `p` is outside (0, 1).
208    ///
209    /// # Reference
210    /// Abernethy (2006), *The New Weibull Handbook*, 5th ed.
211    pub fn time_to_reliability(&self, p: f64) -> Option<f64> {
212        if p <= 0.0 || p >= 1.0 {
213            return None;
214        }
215        Some(self.scale * (-p.ln()).powf(1.0 / self.shape))
216    }
217
218    /// B-life: time at which a given fraction of the population has failed.
219    ///
220    /// B10 life (10% failed) = `b_life(0.10)`, which is equivalent to
221    /// `time_to_reliability(0.90)`.
222    ///
223    /// # Arguments
224    /// * `fraction_failed` - Fraction of population that has failed, must be in (0, 1).
225    ///
226    /// # Returns
227    /// `None` if `fraction_failed` is outside (0, 1).
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use u_analytics::weibull::ReliabilityAnalysis;
233    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
234    ///
235    /// // B10: time when 10% of the population has failed
236    /// let b10 = ra.b_life(0.10).unwrap();
237    /// assert!(b10 > 0.0 && b10 < 100.0);
238    ///
239    /// // B-lives increase with failure fraction: B5 < B10 < B50
240    /// let b5 = ra.b_life(0.05).unwrap();
241    /// let b50 = ra.b_life(0.50).unwrap();
242    /// assert!(b5 < b10 && b10 < b50);
243    /// ```
244    ///
245    /// # Reference
246    /// Abernethy (2006), *The New Weibull Handbook*, 5th ed., Chapter 2.
247    pub fn b_life(&self, fraction_failed: f64) -> Option<f64> {
248        if fraction_failed <= 0.0 || fraction_failed >= 1.0 {
249            return None;
250        }
251        self.time_to_reliability(1.0 - fraction_failed)
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::weibull::{weibull_mle, weibull_mrr};
259
260    #[test]
261    fn test_new_valid() {
262        assert!(ReliabilityAnalysis::new(2.0, 50.0).is_some());
263    }
264
265    #[test]
266    fn test_new_invalid() {
267        assert!(ReliabilityAnalysis::new(0.0, 50.0).is_none());
268        assert!(ReliabilityAnalysis::new(-1.0, 50.0).is_none());
269        assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
270        assert!(ReliabilityAnalysis::new(2.0, -1.0).is_none());
271        assert!(ReliabilityAnalysis::new(f64::NAN, 50.0).is_none());
272        assert!(ReliabilityAnalysis::new(2.0, f64::INFINITY).is_none());
273    }
274
275    #[test]
276    fn test_reliability_at_zero() {
277        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
278        assert!((ra.reliability(0.0) - 1.0).abs() < 1e-15);
279    }
280
281    #[test]
282    fn test_reliability_decreasing() {
283        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
284        let mut prev = 1.0;
285        for i in 1..=100 {
286            let t = i as f64;
287            let r = ra.reliability(t);
288            assert!(
289                r <= prev + 1e-15,
290                "reliability should be non-increasing: R({}) = {} > R({}) = {}",
291                t,
292                r,
293                t - 1.0,
294                prev
295            );
296            prev = r;
297        }
298    }
299
300    #[test]
301    fn test_reliability_at_scale() {
302        // R(eta) = exp(-1) for any beta
303        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
304        let r_at_scale = ra.reliability(50.0);
305        let expected = (-1.0_f64).exp();
306        assert!(
307            (r_at_scale - expected).abs() < 1e-10,
308            "R(eta) = {}, expected exp(-1) = {}",
309            r_at_scale,
310            expected
311        );
312    }
313
314    #[test]
315    fn test_reliability_negative_time() {
316        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
317        assert!((ra.reliability(-10.0) - 1.0).abs() < 1e-15);
318    }
319
320    #[test]
321    fn test_hazard_rate_exponential() {
322        // beta=1 => constant hazard rate = 1/eta
323        let ra = ReliabilityAnalysis::new(1.0, 20.0).expect("valid parameters");
324        for t in [5.0, 10.0, 20.0, 50.0] {
325            assert!(
326                (ra.hazard_rate(t) - 1.0 / 20.0).abs() < 1e-10,
327                "hazard at t={} should be 1/eta=0.05",
328                t
329            );
330        }
331    }
332
333    #[test]
334    fn test_hazard_rate_increasing() {
335        // beta > 1 => increasing hazard rate (wear-out)
336        let ra = ReliabilityAnalysis::new(3.0, 50.0).expect("valid parameters");
337        let h1 = ra.hazard_rate(10.0);
338        let h2 = ra.hazard_rate(20.0);
339        let h3 = ra.hazard_rate(30.0);
340        assert!(
341            h1 < h2,
342            "hazard should increase: h(10)={} >= h(20)={}",
343            h1,
344            h2
345        );
346        assert!(
347            h2 < h3,
348            "hazard should increase: h(20)={} >= h(30)={}",
349            h2,
350            h3
351        );
352    }
353
354    #[test]
355    fn test_hazard_rate_zero_time() {
356        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
357        assert!((ra.hazard_rate(0.0)).abs() < 1e-15);
358        assert!((ra.hazard_rate(-5.0)).abs() < 1e-15);
359    }
360
361    #[test]
362    fn test_mtbf_exponential() {
363        // beta=1 => MTBF = eta * Gamma(2) = eta * 1 = eta
364        let ra = ReliabilityAnalysis::new(1.0, 100.0).expect("valid parameters");
365        assert!(
366            (ra.mtbf() - 100.0).abs() < 1e-8,
367            "MTBF = {}, expected 100.0 for exponential",
368            ra.mtbf()
369        );
370    }
371
372    #[test]
373    fn test_mtbf_rayleigh() {
374        // beta=2, eta=1 => MTBF = Gamma(1.5) = sqrt(pi)/2
375        let ra = ReliabilityAnalysis::new(2.0, 1.0).expect("valid parameters");
376        let expected = std::f64::consts::PI.sqrt() / 2.0;
377        assert!(
378            (ra.mtbf() - expected).abs() < 1e-10,
379            "MTBF = {}, expected sqrt(pi)/2 = {}",
380            ra.mtbf(),
381            expected
382        );
383    }
384
385    #[test]
386    fn test_time_to_reliability_median() {
387        // Median life: R(t) = 0.5 => t = eta * (ln(2))^(1/beta)
388        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
389        let t_median = ra.time_to_reliability(0.5).expect("p=0.5 is valid");
390        let expected = 50.0 * (2.0_f64.ln()).powf(0.5);
391        assert!(
392            (t_median - expected).abs() < 1e-10,
393            "median life = {}, expected {}",
394            t_median,
395            expected
396        );
397    }
398
399    #[test]
400    fn test_time_to_reliability_invalid() {
401        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
402        assert!(ra.time_to_reliability(0.0).is_none());
403        assert!(ra.time_to_reliability(1.0).is_none());
404        assert!(ra.time_to_reliability(-0.1).is_none());
405        assert!(ra.time_to_reliability(1.5).is_none());
406    }
407
408    #[test]
409    fn test_time_to_reliability_roundtrip() {
410        let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
411        for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
412            let t = ra.time_to_reliability(p).expect("valid p");
413            let r = ra.reliability(t);
414            assert!(
415                (r - p).abs() < 1e-10,
416                "roundtrip: time_to_reliability({}) = {}, reliability({}) = {}",
417                p,
418                t,
419                t,
420                r
421            );
422        }
423    }
424
425    #[test]
426    fn test_b_life_b10() {
427        // B10 = time when 10% have failed = time_to_reliability(0.90)
428        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
429        let b10 = ra.b_life(0.10).expect("valid fraction");
430        let t_r90 = ra.time_to_reliability(0.90).expect("valid p");
431        assert!(
432            (b10 - t_r90).abs() < 1e-10,
433            "B10 = {}, time_to_reliability(0.90) = {}",
434            b10,
435            t_r90
436        );
437    }
438
439    #[test]
440    fn test_b_life_invalid() {
441        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
442        assert!(ra.b_life(0.0).is_none());
443        assert!(ra.b_life(1.0).is_none());
444        assert!(ra.b_life(-0.1).is_none());
445    }
446
447    #[test]
448    fn test_from_mle() {
449        let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
450        let mle = weibull_mle(&data).expect("MLE should converge");
451        let ra = ReliabilityAnalysis::from_mle(&mle);
452
453        assert!((ra.shape() - mle.shape).abs() < 1e-15);
454        assert!((ra.scale() - mle.scale).abs() < 1e-15);
455        assert!(ra.mtbf() > 0.0);
456    }
457
458    #[test]
459    fn test_from_mrr() {
460        let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
461        let mrr = weibull_mrr(&data).expect("MRR should succeed");
462        let ra = ReliabilityAnalysis::from_mrr(&mrr);
463
464        assert!((ra.shape() - mrr.shape).abs() < 1e-15);
465        assert!((ra.scale() - mrr.scale).abs() < 1e-15);
466        assert!(ra.mtbf() > 0.0);
467    }
468
469    #[test]
470    fn test_b_life_ordering() {
471        // B5 < B10 < B50 (more failures => more time)
472        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
473        let b5 = ra.b_life(0.05).expect("valid");
474        let b10 = ra.b_life(0.10).expect("valid");
475        let b50 = ra.b_life(0.50).expect("valid");
476        assert!(b5 < b10, "B5={} should < B10={}", b5, b10);
477        assert!(b10 < b50, "B10={} should < B50={}", b10, b50);
478    }
479
480    #[test]
481    fn test_hazard_pdf_reliability_relation() {
482        // h(t) = f(t) / R(t) where f(t) is the Weibull PDF
483        // f(t) = (beta/eta) * (t/eta)^(beta-1) * exp(-(t/eta)^beta)
484        let ra = ReliabilityAnalysis::new(2.5, 50.0).expect("valid parameters");
485        for t in [5.0, 20.0, 50.0, 80.0] {
486            let z = t / ra.scale();
487            let pdf =
488                (ra.shape() / ra.scale()) * z.powf(ra.shape() - 1.0) * (-z.powf(ra.shape())).exp();
489            let h = ra.hazard_rate(t);
490            let r = ra.reliability(t);
491            assert!(
492                (h * r - pdf).abs() < 1e-10,
493                "h(t)*R(t) = {} should equal f(t) = {} at t={}",
494                h * r,
495                pdf,
496                t
497            );
498        }
499    }
500
501    #[test]
502    fn test_accessors() {
503        let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
504        assert!((ra.shape() - 2.5).abs() < 1e-15);
505        assert!((ra.scale() - 100.0).abs() < 1e-15);
506    }
507}