1use u_numflow::special::gamma;
7
8use super::mle::{two_sided_z, WeibullInterval};
9
10#[derive(Debug, Clone)]
39pub struct ReliabilityAnalysis {
40 shape: f64,
42 scale: f64,
44 covariance: Option<[[f64; 2]; 2]>,
47}
48
49impl ReliabilityAnalysis {
50 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 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 pub fn covariance(&self) -> Option<[[f64; 2]; 2]> {
114 self.covariance
115 }
116
117 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 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 pub fn shape(&self) -> f64 {
146 self.shape
147 }
148
149 pub fn scale(&self) -> f64 {
151 self.scale
152 }
153
154 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 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 pub fn mtbf(&self) -> f64 {
244 self.scale * gamma(1.0 + 1.0 / self.shape)
245 }
246
247 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 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 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 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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 #[test]
801 fn test_reference_reliability_beta2_eta100() {
802 let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
803
804 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 let r100 = ra.reliability(100.0);
813 let expected_r100 = (-1.0_f64).exp(); assert!(
815 (r100 - expected_r100).abs() < 1e-5,
816 "R(100) = {:.6}, expected {:.6}",
817 r100,
818 expected_r100
819 );
820
821 let r50 = ra.reliability(50.0);
823 let expected_r50 = (-0.25_f64).exp(); 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 let f100 = 1.0 - ra.reliability(100.0);
838 let expected_f100 = 1.0 - (-1.0_f64).exp(); 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 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 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 let mttf = ra.mtbf();
874 let expected_mttf = 100.0 * std::f64::consts::PI.sqrt() / 2.0; 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 let b10 = ra.b_life(0.10).expect("valid fraction");
889 let expected_b10 = 100.0 * (-0.9_f64.ln()).powf(0.5); assert!(
891 (b10 - expected_b10).abs() < 0.01,
892 "B10 = {:.5}, expected {:.5}",
893 b10,
894 expected_b10
895 );
896 }
897}