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