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
8use super::mle::{two_sided_z, WeibullInterval};
9
10/// Reliability analysis results from a fitted Weibull distribution.
11///
12/// Computes reliability engineering metrics (reliability function, hazard
13/// rate, MTBF, B-life) from Weibull shape (beta) and scale (eta) parameters.
14///
15/// # Mathematical Background
16///
17/// Given a Weibull distribution with shape beta > 0 and scale eta > 0:
18/// - Reliability: R(t) = exp(-(t/eta)^beta)
19/// - Hazard rate: lambda(t) = (beta/eta) * (t/eta)^(beta-1)
20/// - MTBF: eta * Gamma(1 + 1/beta)
21///
22/// # Examples
23///
24/// ```
25/// use u_analytics::weibull::ReliabilityAnalysis;
26/// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
27/// assert!((ra.reliability(0.0) - 1.0).abs() < 1e-10);
28/// assert!(ra.hazard_rate(50.0) > 0.0);
29/// assert!(ra.mtbf() > 0.0);
30/// let b10 = ra.b_life(0.10).unwrap();
31/// assert!(b10 > 0.0 && b10 < 100.0);
32/// ```
33///
34/// # References
35/// - Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, Wiley.
36/// - Nelson, W. (1982). *Applied Life Data Analysis*, Wiley. §2.
37/// - Lawless, J.F. (2003). *Statistical Models and Methods for Lifetime Data*, 2nd ed.
38#[derive(Debug, Clone)]
39pub struct ReliabilityAnalysis {
40    /// Shape parameter (beta).
41    shape: f64,
42    /// Scale parameter (eta).
43    scale: f64,
44    /// Covariance of `(shape, scale)`, when the parameters were estimated --
45    /// what the confidence intervals are made from.
46    covariance: Option<[[f64; 2]; 2]>,
47}
48
49impl ReliabilityAnalysis {
50    /// Creates a new reliability analysis from Weibull parameters.
51    ///
52    /// # Arguments
53    /// * `shape` - Shape parameter beta (must be positive and finite)
54    /// * `scale` - Scale parameter eta (must be positive and finite)
55    ///
56    /// # Returns
57    /// `None` if either parameter is non-positive or non-finite.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use u_analytics::weibull::ReliabilityAnalysis;
63    /// let ra = ReliabilityAnalysis::new(2.0, 100.0);
64    /// assert!(ra.is_some());
65    ///
66    /// // Invalid parameters return None
67    /// assert!(ReliabilityAnalysis::new(-1.0, 100.0).is_none());
68    /// assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
69    /// ```
70    pub fn new(shape: f64, scale: f64) -> Option<Self> {
71        if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
72            return None;
73        }
74        Some(Self {
75            shape,
76            scale,
77            covariance: None,
78        })
79    }
80
81    /// The same analysis with the covariance of the estimated `(shape,
82    /// scale)` -- row and column order shape, scale, as
83    /// [`WeibullMleResult::covariance`](super::WeibullMleResult::covariance)
84    /// gives it -- so the B-lives and reliabilities carry confidence
85    /// intervals ([`Self::b_life_interval`], [`Self::reliability_interval`]).
86    ///
87    /// `None` unless the matrix is a covariance: finite, symmetric (to a
88    /// relative 1e-9) and positive definite.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use u_analytics::weibull::ReliabilityAnalysis;
94    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
95    /// assert!(ra.clone().with_covariance([[0.04, 0.1], [0.1, 25.0]]).is_some());
96    /// assert!(ra.with_covariance([[0.04, 2.0], [2.0, 25.0]]).is_none()); // not positive definite
97    /// ```
98    pub fn with_covariance(self, covariance: [[f64; 2]; 2]) -> Option<Self> {
99        let [[a, b], [c, d]] = covariance;
100        let finite = [a, b, c, d].iter().all(|v| v.is_finite());
101        let symmetric = (b - c).abs() <= 1e-9 * b.abs().max(c.abs()).max(f64::MIN_POSITIVE);
102        if !(finite && symmetric && a > 0.0 && d > 0.0 && a * d - b * c > 0.0) {
103            return None;
104        }
105        Some(Self {
106            covariance: Some(covariance),
107            ..self
108        })
109    }
110
111    /// The covariance of `(shape, scale)` the intervals are made from, when
112    /// there is one.
113    pub fn covariance(&self) -> Option<[[f64; 2]; 2]> {
114        self.covariance
115    }
116
117    /// Creates a reliability analysis from an MLE fitting result.
118    ///
119    /// # Panics
120    /// This function does not panic. MLE results always have valid parameters.
121    ///
122    /// The fit's covariance comes along, so the analysis gives confidence
123    /// intervals.
124    pub fn from_mle(result: &super::mle::WeibullMleResult) -> Self {
125        Self {
126            shape: result.shape,
127            scale: result.scale,
128            covariance: Some(result.covariance),
129        }
130    }
131
132    /// Creates a reliability analysis from an MRR fitting result.
133    ///
134    /// # Panics
135    /// This function does not panic. MRR results always have valid parameters.
136    pub fn from_mrr(result: &super::mrr::WeibullMrrResult) -> Self {
137        Self {
138            shape: result.shape,
139            scale: result.scale,
140            covariance: None,
141        }
142    }
143
144    /// Returns the shape parameter (beta).
145    pub fn shape(&self) -> f64 {
146        self.shape
147    }
148
149    /// Returns the scale parameter (eta).
150    pub fn scale(&self) -> f64 {
151        self.scale
152    }
153
154    /// Reliability (survival) function at time t.
155    ///
156    /// ```text
157    /// R(t) = exp(-(t/eta)^beta)
158    /// ```
159    ///
160    /// For t < 0, returns 1.0 (no failure before time 0).
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use u_analytics::weibull::ReliabilityAnalysis;
166    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
167    ///
168    /// // R(0) = 1.0 (full reliability at time zero)
169    /// assert!((ra.reliability(0.0) - 1.0).abs() < 1e-10);
170    ///
171    /// // R(eta) = exp(-1) ≈ 0.368 for any shape parameter
172    /// let expected = (-1.0_f64).exp();
173    /// assert!((ra.reliability(100.0) - expected).abs() < 1e-10);
174    /// ```
175    ///
176    /// # Reference
177    /// Weibull (1951), *Journal of Applied Mechanics* 18(3), pp. 293-297.
178    pub fn reliability(&self, t: f64) -> f64 {
179        if t <= 0.0 {
180            return 1.0;
181        }
182        let z = t / self.scale;
183        (-z.powf(self.shape)).exp()
184    }
185
186    /// Failure rate (hazard function) at time t.
187    ///
188    /// ```text
189    /// lambda(t) = (beta/eta) * (t/eta)^(beta-1)
190    /// ```
191    ///
192    /// - beta < 1: Decreasing failure rate (infant mortality)
193    /// - beta = 1: Constant failure rate (random/exponential failures)
194    /// - beta > 1: Increasing failure rate (wear-out)
195    ///
196    /// For t <= 0, returns 0.0.
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// use u_analytics::weibull::ReliabilityAnalysis;
202    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
203    ///
204    /// // Hazard rate is positive for t > 0
205    /// assert!(ra.hazard_rate(50.0) > 0.0);
206    ///
207    /// // With beta > 1, hazard rate increases over time (wear-out)
208    /// assert!(ra.hazard_rate(50.0) < ra.hazard_rate(80.0));
209    /// ```
210    ///
211    /// # Reference
212    /// Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, Ch. 4.
213    pub fn hazard_rate(&self, t: f64) -> f64 {
214        if t <= 0.0 {
215            return 0.0;
216        }
217        let z = t / self.scale;
218        (self.shape / self.scale) * z.powf(self.shape - 1.0)
219    }
220
221    /// Mean Time Between Failures (MTBF).
222    ///
223    /// ```text
224    /// MTBF = eta * Gamma(1 + 1/beta)
225    /// ```
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use u_analytics::weibull::ReliabilityAnalysis;
231    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
232    /// let mtbf = ra.mtbf();
233    /// assert!(mtbf > 0.0);
234    ///
235    /// // For beta=1 (exponential), MTBF = eta
236    /// let exp_ra = ReliabilityAnalysis::new(1.0, 50.0).unwrap();
237    /// assert!((exp_ra.mtbf() - 50.0).abs() < 1e-8);
238    /// ```
239    ///
240    /// # Reference
241    /// Johnson, Kotz & Balakrishnan (1994), *Continuous Univariate Distributions*,
242    /// Vol. 1, Chapter 21.
243    pub fn mtbf(&self) -> f64 {
244        self.scale * gamma(1.0 + 1.0 / self.shape)
245    }
246
247    /// Time at which reliability drops to a given level.
248    ///
249    /// Solves R(t) = p for t:
250    ///
251    /// ```text
252    /// t = eta * (-ln(p))^(1/beta)
253    /// ```
254    ///
255    /// # Arguments
256    /// * `p` - Desired reliability level, must be in (0, 1).
257    ///
258    /// # Returns
259    /// `None` if `p` is outside (0, 1).
260    ///
261    /// # Reference
262    /// Abernethy (2006), *The New Weibull Handbook*, 5th ed.
263    pub fn time_to_reliability(&self, p: f64) -> Option<f64> {
264        if p <= 0.0 || p >= 1.0 {
265            return None;
266        }
267        Some(self.scale * (-p.ln()).powf(1.0 / self.shape))
268    }
269
270    /// B-life: time at which a given fraction of the population has failed.
271    ///
272    /// B10 life (10% failed) = `b_life(0.10)`, which is equivalent to
273    /// `time_to_reliability(0.90)`.
274    ///
275    /// # Arguments
276    /// * `fraction_failed` - Fraction of population that has failed, must be in (0, 1).
277    ///
278    /// # Returns
279    /// `None` if `fraction_failed` is outside (0, 1).
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use u_analytics::weibull::ReliabilityAnalysis;
285    /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
286    ///
287    /// // B10: time when 10% of the population has failed
288    /// let b10 = ra.b_life(0.10).unwrap();
289    /// assert!(b10 > 0.0 && b10 < 100.0);
290    ///
291    /// // B-lives increase with failure fraction: B5 < B10 < B50
292    /// let b5 = ra.b_life(0.05).unwrap();
293    /// let b50 = ra.b_life(0.50).unwrap();
294    /// assert!(b5 < b10 && b10 < b50);
295    /// ```
296    ///
297    /// # Reference
298    /// Abernethy (2006), *The New Weibull Handbook*, 5th ed., Chapter 2.
299    pub fn b_life(&self, fraction_failed: f64) -> Option<f64> {
300        if fraction_failed <= 0.0 || fraction_failed >= 1.0 {
301            return None;
302        }
303        self.time_to_reliability(1.0 - fraction_failed)
304    }
305
306    /// `g' Σ g` for a gradient in `(shape, scale)`.
307    fn variance_along(&self, g: [f64; 2]) -> Option<f64> {
308        let c = self.covariance?;
309        Some((g[0] * g[0] * c[0][0] + 2.0 * g[0] * g[1] * c[0][1] + g[1] * g[1] * c[1][1]).max(0.0))
310    }
311
312    /// The B-life at `fraction_failed` with its two-sided interval at
313    /// `confidence`, by the delta method on
314    /// `ln t_p = ln η + ln(−ln(1 − p)) / β` (normal on the log scale, so the
315    /// limits stay positive).
316    ///
317    /// Returns `(estimate, interval)`; `None` without a covariance
318    /// ([`Self::with_covariance`], [`Self::from_mle`]) or when either argument
319    /// is outside (0, 1).
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use u_analytics::weibull::ReliabilityAnalysis;
325    /// let ra = ReliabilityAnalysis::new(2.0, 100.0)
326    ///     .unwrap()
327    ///     .with_covariance([[0.04, 0.1], [0.1, 25.0]])
328    ///     .unwrap();
329    /// let (b10, ci) = ra.b_life_interval(0.10, 0.90).unwrap();
330    /// assert!(ci.lower < b10 && b10 < ci.upper);
331    /// ```
332    ///
333    /// # Reference
334    /// Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, §8.4.
335    pub fn b_life_interval(
336        &self,
337        fraction_failed: f64,
338        confidence: f64,
339    ) -> Option<(f64, WeibullInterval)> {
340        let t_p = self.b_life(fraction_failed)?;
341        let z = two_sided_z(confidence)?;
342        let (beta, eta) = (self.shape, self.scale);
343        let w = (-(1.0 - fraction_failed).ln()).ln();
344        // Gradient of ln t_p in (beta, eta).
345        let half = z * self.variance_along([-w / (beta * beta), 1.0 / eta])?.sqrt();
346        Some((
347            t_p,
348            WeibullInterval {
349                lower: t_p * (-half).exp(),
350                upper: t_p * half.exp(),
351            },
352        ))
353    }
354
355    /// The reliability `R(t)` with its two-sided interval at `confidence`, by
356    /// the delta method on `u = ln(−ln R(t)) = β (ln t − ln η)`, which is
357    /// unbounded -- so the limits `exp(−exp(u ± z·se))` stay inside (0, 1).
358    /// At `t ≤ 0` the model has no failures and the interval is `[1, 1]`.
359    ///
360    /// Returns `(estimate, interval)`; `None` without a covariance or when
361    /// `confidence` is outside (0, 1) or `t` is not finite.
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use u_analytics::weibull::ReliabilityAnalysis;
367    /// let ra = ReliabilityAnalysis::new(2.0, 100.0)
368    ///     .unwrap()
369    ///     .with_covariance([[0.04, 0.1], [0.1, 25.0]])
370    ///     .unwrap();
371    /// let (r, ci) = ra.reliability_interval(50.0, 0.95).unwrap();
372    /// assert!(0.0 < ci.lower && ci.lower < r && r < ci.upper && ci.upper < 1.0);
373    /// ```
374    ///
375    /// # Reference
376    /// Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, §8.4.
377    pub fn reliability_interval(&self, t: f64, confidence: f64) -> Option<(f64, WeibullInterval)> {
378        let z = two_sided_z(confidence)?;
379        if !t.is_finite() {
380            return None;
381        }
382        self.covariance?;
383        if t <= 0.0 {
384            return Some((
385                1.0,
386                WeibullInterval {
387                    lower: 1.0,
388                    upper: 1.0,
389                },
390            ));
391        }
392        let (beta, eta) = (self.shape, self.scale);
393        let u = beta * (t / eta).ln();
394        // Gradient of u in (beta, eta).
395        let half = z * self.variance_along([u / beta, -beta / eta])?.sqrt();
396        let r = |u: f64| (-u.exp()).exp();
397        Some((
398            self.reliability(t),
399            WeibullInterval {
400                lower: r(u + half),
401                upper: r(u - half),
402            },
403        ))
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use crate::weibull::{weibull_mle, weibull_mrr};
411
412    fn estimated() -> ReliabilityAnalysis {
413        ReliabilityAnalysis::new(1.8, 120.0)
414            .unwrap()
415            .with_covariance([[0.05, 0.9], [0.9, 60.0]])
416            .unwrap()
417    }
418
419    /// The delta-method variance with the gradient taken by central
420    /// differences of the function itself, not the closed form.
421    fn numeric_variance(ra: &ReliabilityAnalysis, f: impl Fn(f64, f64) -> f64) -> f64 {
422        let (b, e) = (ra.shape(), ra.scale());
423        let (hb, he) = (1e-6 * b, 1e-6 * e);
424        let g = [
425            (f(b + hb, e) - f(b - hb, e)) / (2.0 * hb),
426            (f(b, e + he) - f(b, e - he)) / (2.0 * he),
427        ];
428        let c = ra.covariance().unwrap();
429        g[0] * g[0] * c[0][0] + 2.0 * g[0] * g[1] * c[0][1] + g[1] * g[1] * c[1][1]
430    }
431
432    #[test]
433    fn interval_widths_are_the_delta_method_of_the_function() {
434        let ra = estimated();
435        let z = u_numflow::special::inverse_normal_cdf(0.975);
436        let p = 0.1;
437        let (t_p, ci) = ra.b_life_interval(p, 0.95).unwrap();
438        let ln_tp = |b: f64, e: f64| e.ln() + (-(1.0 - p).ln()).ln() / b;
439        let half = z * numeric_variance(&ra, ln_tp).sqrt();
440        assert!(((ci.upper / t_p).ln() - half).abs() < 1e-7, "{ci:?}");
441        assert!(((t_p / ci.lower).ln() - half).abs() < 1e-7, "{ci:?}");
442
443        let t = 75.0;
444        let (r, ci) = ra.reliability_interval(t, 0.95).unwrap();
445        let u = |b: f64, e: f64| b * (t / e).ln();
446        let half = z * numeric_variance(&ra, u).sqrt();
447        let u_of = |r: f64| (-r.ln()).ln();
448        assert!((u_of(ci.lower) - u_of(r) - half).abs() < 1e-7, "{ci:?}");
449        assert!((u_of(r) - u_of(ci.upper) - half).abs() < 1e-7, "{ci:?}");
450    }
451
452    /// At t = B_p the two intervals are one statement: u = β(ln t − ln η) is
453    /// −β times (ln t_p − ln η − w/β) moved, so se(u) = β · se(ln t_p).
454    #[test]
455    fn the_reliability_interval_at_a_b_life_is_that_b_life_s_interval() {
456        let ra = estimated();
457        let p = 0.2;
458        let (t_p, b) = ra.b_life_interval(p, 0.9).unwrap();
459        let (r, ci) = ra.reliability_interval(t_p, 0.9).unwrap();
460        assert!((r - (1.0 - p)).abs() < 1e-12);
461        let half_u = (-ci.lower.ln()).ln() - (-r.ln()).ln();
462        let half_t = (b.upper / t_p).ln();
463        assert!(
464            (half_u - ra.shape() * half_t).abs() < 1e-9,
465            "{half_u} vs {half_t}"
466        );
467    }
468
469    #[test]
470    fn reliability_limits_stay_inside_zero_one_and_are_exact_at_time_zero() {
471        let ra = estimated();
472        for t in [1e-3, 1.0, 50.0, 120.0, 400.0, 2000.0] {
473            let (r, ci) = ra.reliability_interval(t, 0.99).unwrap();
474            assert!(
475                0.0 <= ci.lower && ci.lower <= r && r <= ci.upper && ci.upper <= 1.0,
476                "{t}: {ci:?}"
477            );
478        }
479        let (r, ci) = ra.reliability_interval(0.0, 0.95).unwrap();
480        assert_eq!((r, ci.lower, ci.upper), (1.0, 1.0, 1.0));
481        assert!(ra.reliability_interval(f64::NAN, 0.95).is_none());
482        assert!(ra.reliability_interval(10.0, 1.0).is_none());
483    }
484
485    #[test]
486    fn intervals_need_a_covariance() {
487        let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
488        assert!(ra.b_life_interval(0.1, 0.95).is_none());
489        assert!(ra.reliability_interval(10.0, 0.95).is_none());
490        let mrr = weibull_mrr(&[12.0, 25.0, 33.0, 47.0, 58.0, 71.0]).unwrap();
491        assert!(ReliabilityAnalysis::from_mrr(&mrr).covariance().is_none());
492    }
493
494    #[test]
495    fn with_covariance_takes_only_a_covariance() {
496        let ra = || ReliabilityAnalysis::new(2.0, 100.0).unwrap();
497        assert!(ra().with_covariance([[0.04, 0.1], [0.1, 25.0]]).is_some());
498        assert!(
499            ra().with_covariance([[0.04, 0.1], [0.2, 25.0]]).is_none(),
500            "asymmetric"
501        );
502        assert!(
503            ra().with_covariance([[0.04, 1.0], [1.0, 25.0]]).is_none(),
504            "singular"
505        );
506        assert!(
507            ra().with_covariance([[-0.04, 0.0], [0.0, 25.0]]).is_none(),
508            "negative variance"
509        );
510        assert!(ra()
511            .with_covariance([[f64::NAN, 0.0], [0.0, 25.0]])
512            .is_none());
513    }
514
515    #[test]
516    fn the_mle_s_b_life_interval_is_this_one() {
517        let fit = weibull_mle(&[55.0, 187.0, 216.0, 240.0, 244.0, 335.0, 361.0, 373.0]).unwrap();
518        let ra = ReliabilityAnalysis::from_mle(&fit);
519        assert_eq!(fit.b_life_interval(0.1, 0.9), ra.b_life_interval(0.1, 0.9));
520    }
521
522    /// Monte Carlo: n = 40 Weibull(2, 100) samples, MLE, 90 % interval for
523    /// R(60) and for B10. A Wald interval at this n covers within a few
524    /// points of nominal (Meeker & Escobar §8.4 — the log/loglog scales are
525    /// chosen for that); 600 samples give a binomial s.e. of 1.2 %.
526    #[test]
527    fn the_intervals_cover_near_their_level() {
528        use u_numflow::distributions::{Sample, Weibull};
529        let truth = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
530        let (r_true, b10_true) = (truth.reliability(60.0), truth.b_life(0.1).unwrap());
531        let law = Weibull::new(2.0, 100.0).unwrap();
532        let mut rng = u_numflow::random::create_rng(2026);
533        let runs = 600;
534        let (mut r_in, mut b_in) = (0, 0);
535        for _ in 0..runs {
536            let data: Vec<f64> = (0..40).map(|_| law.sample(&mut rng)).collect();
537            let ra = ReliabilityAnalysis::from_mle(&weibull_mle(&data).unwrap());
538            let (_, r) = ra.reliability_interval(60.0, 0.9).unwrap();
539            let (_, b) = ra.b_life_interval(0.1, 0.9).unwrap();
540            r_in += usize::from(r.lower <= r_true && r_true <= r.upper);
541            b_in += usize::from(b.lower <= b10_true && b10_true <= b.upper);
542        }
543        let (rc, bc) = (r_in as f64 / runs as f64, b_in as f64 / runs as f64);
544        assert!((0.85..=0.95).contains(&rc), "R(60) coverage {rc}");
545        assert!((0.85..=0.95).contains(&bc), "B10 coverage {bc}");
546    }
547
548    #[test]
549    fn test_new_valid() {
550        assert!(ReliabilityAnalysis::new(2.0, 50.0).is_some());
551    }
552
553    #[test]
554    fn test_new_invalid() {
555        assert!(ReliabilityAnalysis::new(0.0, 50.0).is_none());
556        assert!(ReliabilityAnalysis::new(-1.0, 50.0).is_none());
557        assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
558        assert!(ReliabilityAnalysis::new(2.0, -1.0).is_none());
559        assert!(ReliabilityAnalysis::new(f64::NAN, 50.0).is_none());
560        assert!(ReliabilityAnalysis::new(2.0, f64::INFINITY).is_none());
561    }
562
563    #[test]
564    fn test_reliability_at_zero() {
565        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
566        assert!((ra.reliability(0.0) - 1.0).abs() < 1e-15);
567    }
568
569    #[test]
570    fn test_reliability_decreasing() {
571        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
572        let mut prev = 1.0;
573        for i in 1..=100 {
574            let t = i as f64;
575            let r = ra.reliability(t);
576            assert!(
577                r <= prev + 1e-15,
578                "reliability should be non-increasing: R({}) = {} > R({}) = {}",
579                t,
580                r,
581                t - 1.0,
582                prev
583            );
584            prev = r;
585        }
586    }
587
588    #[test]
589    fn test_reliability_at_scale() {
590        // R(eta) = exp(-1) for any beta
591        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
592        let r_at_scale = ra.reliability(50.0);
593        let expected = (-1.0_f64).exp();
594        assert!(
595            (r_at_scale - expected).abs() < 1e-10,
596            "R(eta) = {}, expected exp(-1) = {}",
597            r_at_scale,
598            expected
599        );
600    }
601
602    #[test]
603    fn test_reliability_negative_time() {
604        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
605        assert!((ra.reliability(-10.0) - 1.0).abs() < 1e-15);
606    }
607
608    #[test]
609    fn test_hazard_rate_exponential() {
610        // beta=1 => constant hazard rate = 1/eta
611        let ra = ReliabilityAnalysis::new(1.0, 20.0).expect("valid parameters");
612        for t in [5.0, 10.0, 20.0, 50.0] {
613            assert!(
614                (ra.hazard_rate(t) - 1.0 / 20.0).abs() < 1e-10,
615                "hazard at t={} should be 1/eta=0.05",
616                t
617            );
618        }
619    }
620
621    #[test]
622    fn test_hazard_rate_increasing() {
623        // beta > 1 => increasing hazard rate (wear-out)
624        let ra = ReliabilityAnalysis::new(3.0, 50.0).expect("valid parameters");
625        let h1 = ra.hazard_rate(10.0);
626        let h2 = ra.hazard_rate(20.0);
627        let h3 = ra.hazard_rate(30.0);
628        assert!(
629            h1 < h2,
630            "hazard should increase: h(10)={} >= h(20)={}",
631            h1,
632            h2
633        );
634        assert!(
635            h2 < h3,
636            "hazard should increase: h(20)={} >= h(30)={}",
637            h2,
638            h3
639        );
640    }
641
642    #[test]
643    fn test_hazard_rate_zero_time() {
644        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
645        assert!((ra.hazard_rate(0.0)).abs() < 1e-15);
646        assert!((ra.hazard_rate(-5.0)).abs() < 1e-15);
647    }
648
649    #[test]
650    fn test_mtbf_exponential() {
651        // beta=1 => MTBF = eta * Gamma(2) = eta * 1 = eta
652        let ra = ReliabilityAnalysis::new(1.0, 100.0).expect("valid parameters");
653        assert!(
654            (ra.mtbf() - 100.0).abs() < 1e-8,
655            "MTBF = {}, expected 100.0 for exponential",
656            ra.mtbf()
657        );
658    }
659
660    #[test]
661    fn test_mtbf_rayleigh() {
662        // beta=2, eta=1 => MTBF = Gamma(1.5) = sqrt(pi)/2
663        let ra = ReliabilityAnalysis::new(2.0, 1.0).expect("valid parameters");
664        let expected = std::f64::consts::PI.sqrt() / 2.0;
665        assert!(
666            (ra.mtbf() - expected).abs() < 1e-10,
667            "MTBF = {}, expected sqrt(pi)/2 = {}",
668            ra.mtbf(),
669            expected
670        );
671    }
672
673    #[test]
674    fn test_time_to_reliability_median() {
675        // Median life: R(t) = 0.5 => t = eta * (ln(2))^(1/beta)
676        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
677        let t_median = ra.time_to_reliability(0.5).expect("p=0.5 is valid");
678        let expected = 50.0 * (2.0_f64.ln()).powf(0.5);
679        assert!(
680            (t_median - expected).abs() < 1e-10,
681            "median life = {}, expected {}",
682            t_median,
683            expected
684        );
685    }
686
687    #[test]
688    fn test_time_to_reliability_invalid() {
689        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
690        assert!(ra.time_to_reliability(0.0).is_none());
691        assert!(ra.time_to_reliability(1.0).is_none());
692        assert!(ra.time_to_reliability(-0.1).is_none());
693        assert!(ra.time_to_reliability(1.5).is_none());
694    }
695
696    #[test]
697    fn test_time_to_reliability_roundtrip() {
698        let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
699        for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
700            let t = ra.time_to_reliability(p).expect("valid p");
701            let r = ra.reliability(t);
702            assert!(
703                (r - p).abs() < 1e-10,
704                "roundtrip: time_to_reliability({}) = {}, reliability({}) = {}",
705                p,
706                t,
707                t,
708                r
709            );
710        }
711    }
712
713    #[test]
714    fn test_b_life_b10() {
715        // B10 = time when 10% have failed = time_to_reliability(0.90)
716        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
717        let b10 = ra.b_life(0.10).expect("valid fraction");
718        let t_r90 = ra.time_to_reliability(0.90).expect("valid p");
719        assert!(
720            (b10 - t_r90).abs() < 1e-10,
721            "B10 = {}, time_to_reliability(0.90) = {}",
722            b10,
723            t_r90
724        );
725    }
726
727    #[test]
728    fn test_b_life_invalid() {
729        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
730        assert!(ra.b_life(0.0).is_none());
731        assert!(ra.b_life(1.0).is_none());
732        assert!(ra.b_life(-0.1).is_none());
733    }
734
735    #[test]
736    fn test_from_mle() {
737        let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
738        let mle = weibull_mle(&data).expect("MLE should converge");
739        let ra = ReliabilityAnalysis::from_mle(&mle);
740
741        assert!((ra.shape() - mle.shape).abs() < 1e-15);
742        assert!((ra.scale() - mle.scale).abs() < 1e-15);
743        assert!(ra.mtbf() > 0.0);
744    }
745
746    #[test]
747    fn test_from_mrr() {
748        let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
749        let mrr = weibull_mrr(&data).expect("MRR should succeed");
750        let ra = ReliabilityAnalysis::from_mrr(&mrr);
751
752        assert!((ra.shape() - mrr.shape).abs() < 1e-15);
753        assert!((ra.scale() - mrr.scale).abs() < 1e-15);
754        assert!(ra.mtbf() > 0.0);
755    }
756
757    #[test]
758    fn test_b_life_ordering() {
759        // B5 < B10 < B50 (more failures => more time)
760        let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
761        let b5 = ra.b_life(0.05).expect("valid");
762        let b10 = ra.b_life(0.10).expect("valid");
763        let b50 = ra.b_life(0.50).expect("valid");
764        assert!(b5 < b10, "B5={} should < B10={}", b5, b10);
765        assert!(b10 < b50, "B10={} should < B50={}", b10, b50);
766    }
767
768    #[test]
769    fn test_hazard_pdf_reliability_relation() {
770        // h(t) = f(t) / R(t) where f(t) is the Weibull PDF
771        // f(t) = (beta/eta) * (t/eta)^(beta-1) * exp(-(t/eta)^beta)
772        let ra = ReliabilityAnalysis::new(2.5, 50.0).expect("valid parameters");
773        for t in [5.0, 20.0, 50.0, 80.0] {
774            let z = t / ra.scale();
775            let pdf =
776                (ra.shape() / ra.scale()) * z.powf(ra.shape() - 1.0) * (-z.powf(ra.shape())).exp();
777            let h = ra.hazard_rate(t);
778            let r = ra.reliability(t);
779            assert!(
780                (h * r - pdf).abs() < 1e-10,
781                "h(t)*R(t) = {} should equal f(t) = {} at t={}",
782                h * r,
783                pdf,
784                t
785            );
786        }
787    }
788
789    #[test]
790    fn test_accessors() {
791        let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
792        assert!((ra.shape() - 2.5).abs() < 1e-15);
793        assert!((ra.scale() - 100.0).abs() < 1e-15);
794    }
795
796    /// Reference value tests: beta=2, eta=100.
797    ///
798    /// Expected values derived analytically:
799    /// - Nelson (1982) §2; Lawless (2003) Ch. 2.
800    #[test]
801    fn test_reference_reliability_beta2_eta100() {
802        let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
803
804        // R(0) = 1.0 exactly
805        assert!(
806            (ra.reliability(0.0) - 1.0).abs() < 1e-15,
807            "R(0) = {}, expected 1.0",
808            ra.reliability(0.0)
809        );
810
811        // R(100) = exp(-1) ≈ 0.367879
812        let r100 = ra.reliability(100.0);
813        let expected_r100 = (-1.0_f64).exp(); // 0.36787944117144233
814        assert!(
815            (r100 - expected_r100).abs() < 1e-5,
816            "R(100) = {:.6}, expected {:.6}",
817            r100,
818            expected_r100
819        );
820
821        // R(50) = exp(-0.25) ≈ 0.778801
822        let r50 = ra.reliability(50.0);
823        let expected_r50 = (-0.25_f64).exp(); // 0.7788007830714049
824        assert!(
825            (r50 - expected_r50).abs() < 1e-5,
826            "R(50) = {:.6}, expected {:.6}",
827            r50,
828            expected_r50
829        );
830    }
831
832    #[test]
833    fn test_reference_cdf_beta2_eta100() {
834        let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
835
836        // F(100) = 1 - R(100) = 1 - exp(-1) ≈ 0.632121
837        let f100 = 1.0 - ra.reliability(100.0);
838        let expected_f100 = 1.0 - (-1.0_f64).exp(); // 0.6321205588285578
839        assert!(
840            (f100 - expected_f100).abs() < 1e-5,
841            "F(100) = {:.6}, expected {:.6}",
842            f100,
843            expected_f100
844        );
845    }
846
847    #[test]
848    fn test_reference_hazard_rate_beta2_eta100() {
849        let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
850
851        // h(100) = (2/100)*(100/100)^1 = 0.02
852        let h100 = ra.hazard_rate(100.0);
853        assert!(
854            (h100 - 0.02).abs() < 1e-6,
855            "h(100) = {:.8}, expected 0.02",
856            h100
857        );
858
859        // h(50) = (2/100)*(50/100)^1 = 0.01
860        let h50 = ra.hazard_rate(50.0);
861        assert!(
862            (h50 - 0.01).abs() < 1e-6,
863            "h(50) = {:.8}, expected 0.01",
864            h50
865        );
866    }
867
868    #[test]
869    fn test_reference_mttf_beta2_eta100() {
870        let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
871
872        // MTTF = 100 * Gamma(1.5) = 100 * sqrt(pi)/2 ≈ 88.6227
873        let mttf = ra.mtbf();
874        let expected_mttf = 100.0 * std::f64::consts::PI.sqrt() / 2.0; // 88.62269254527580...
875        assert!(
876            (mttf - expected_mttf).abs() < 0.001,
877            "MTTF = {:.6}, expected {:.6}",
878            mttf,
879            expected_mttf
880        );
881    }
882
883    #[test]
884    fn test_reference_b10_beta2_eta100() {
885        let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
886
887        // B10: F(t)=0.10 => t = 100*(-ln(0.9))^0.5 ≈ 32.458
888        let b10 = ra.b_life(0.10).expect("valid fraction");
889        let expected_b10 = 100.0 * (-0.9_f64.ln()).powf(0.5); // 100 * sqrt(0.10536) ≈ 32.4576
890        assert!(
891            (b10 - expected_b10).abs() < 0.01,
892            "B10 = {:.5}, expected {:.5}",
893            b10,
894            expected_b10
895        );
896    }
897}