use u_numflow::special::gamma;
use super::mle::{two_sided_z, WeibullInterval};
#[derive(Debug, Clone)]
pub struct ReliabilityAnalysis {
shape: f64,
scale: f64,
covariance: Option<[[f64; 2]; 2]>,
}
impl ReliabilityAnalysis {
pub fn new(shape: f64, scale: f64) -> Option<Self> {
if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
return None;
}
Some(Self {
shape,
scale,
covariance: None,
})
}
pub fn with_covariance(self, covariance: [[f64; 2]; 2]) -> Option<Self> {
let [[a, b], [c, d]] = covariance;
let finite = [a, b, c, d].iter().all(|v| v.is_finite());
let symmetric = (b - c).abs() <= 1e-9 * b.abs().max(c.abs()).max(f64::MIN_POSITIVE);
if !(finite && symmetric && a > 0.0 && d > 0.0 && a * d - b * c > 0.0) {
return None;
}
Some(Self {
covariance: Some(covariance),
..self
})
}
pub fn covariance(&self) -> Option<[[f64; 2]; 2]> {
self.covariance
}
pub fn from_mle(result: &super::mle::WeibullMleResult) -> Self {
Self {
shape: result.shape,
scale: result.scale,
covariance: Some(result.covariance),
}
}
pub fn from_mrr(result: &super::mrr::WeibullMrrResult) -> Self {
Self {
shape: result.shape,
scale: result.scale,
covariance: None,
}
}
pub fn shape(&self) -> f64 {
self.shape
}
pub fn scale(&self) -> f64 {
self.scale
}
pub fn reliability(&self, t: f64) -> f64 {
if t <= 0.0 {
return 1.0;
}
let z = t / self.scale;
(-z.powf(self.shape)).exp()
}
pub fn hazard_rate(&self, t: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
let z = t / self.scale;
(self.shape / self.scale) * z.powf(self.shape - 1.0)
}
pub fn mtbf(&self) -> f64 {
self.scale * gamma(1.0 + 1.0 / self.shape)
}
pub fn time_to_reliability(&self, p: f64) -> Option<f64> {
if p <= 0.0 || p >= 1.0 {
return None;
}
Some(self.scale * (-p.ln()).powf(1.0 / self.shape))
}
pub fn b_life(&self, fraction_failed: f64) -> Option<f64> {
if fraction_failed <= 0.0 || fraction_failed >= 1.0 {
return None;
}
self.time_to_reliability(1.0 - fraction_failed)
}
fn variance_along(&self, g: [f64; 2]) -> Option<f64> {
let c = self.covariance?;
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))
}
pub fn b_life_interval(
&self,
fraction_failed: f64,
confidence: f64,
) -> Option<(f64, WeibullInterval)> {
let t_p = self.b_life(fraction_failed)?;
let z = two_sided_z(confidence)?;
let (beta, eta) = (self.shape, self.scale);
let w = (-(1.0 - fraction_failed).ln()).ln();
let half = z * self.variance_along([-w / (beta * beta), 1.0 / eta])?.sqrt();
Some((
t_p,
WeibullInterval {
lower: t_p * (-half).exp(),
upper: t_p * half.exp(),
},
))
}
pub fn reliability_interval(&self, t: f64, confidence: f64) -> Option<(f64, WeibullInterval)> {
let z = two_sided_z(confidence)?;
if !t.is_finite() {
return None;
}
self.covariance?;
if t <= 0.0 {
return Some((
1.0,
WeibullInterval {
lower: 1.0,
upper: 1.0,
},
));
}
let (beta, eta) = (self.shape, self.scale);
let u = beta * (t / eta).ln();
let half = z * self.variance_along([u / beta, -beta / eta])?.sqrt();
let r = |u: f64| (-u.exp()).exp();
Some((
self.reliability(t),
WeibullInterval {
lower: r(u + half),
upper: r(u - half),
},
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::weibull::{weibull_mle, weibull_mrr};
fn estimated() -> ReliabilityAnalysis {
ReliabilityAnalysis::new(1.8, 120.0)
.unwrap()
.with_covariance([[0.05, 0.9], [0.9, 60.0]])
.unwrap()
}
fn numeric_variance(ra: &ReliabilityAnalysis, f: impl Fn(f64, f64) -> f64) -> f64 {
let (b, e) = (ra.shape(), ra.scale());
let (hb, he) = (1e-6 * b, 1e-6 * e);
let g = [
(f(b + hb, e) - f(b - hb, e)) / (2.0 * hb),
(f(b, e + he) - f(b, e - he)) / (2.0 * he),
];
let c = ra.covariance().unwrap();
g[0] * g[0] * c[0][0] + 2.0 * g[0] * g[1] * c[0][1] + g[1] * g[1] * c[1][1]
}
#[test]
fn interval_widths_are_the_delta_method_of_the_function() {
let ra = estimated();
let z = u_numflow::special::inverse_normal_cdf(0.975);
let p = 0.1;
let (t_p, ci) = ra.b_life_interval(p, 0.95).unwrap();
let ln_tp = |b: f64, e: f64| e.ln() + (-(1.0 - p).ln()).ln() / b;
let half = z * numeric_variance(&ra, ln_tp).sqrt();
assert!(((ci.upper / t_p).ln() - half).abs() < 1e-7, "{ci:?}");
assert!(((t_p / ci.lower).ln() - half).abs() < 1e-7, "{ci:?}");
let t = 75.0;
let (r, ci) = ra.reliability_interval(t, 0.95).unwrap();
let u = |b: f64, e: f64| b * (t / e).ln();
let half = z * numeric_variance(&ra, u).sqrt();
let u_of = |r: f64| (-r.ln()).ln();
assert!((u_of(ci.lower) - u_of(r) - half).abs() < 1e-7, "{ci:?}");
assert!((u_of(r) - u_of(ci.upper) - half).abs() < 1e-7, "{ci:?}");
}
#[test]
fn the_reliability_interval_at_a_b_life_is_that_b_life_s_interval() {
let ra = estimated();
let p = 0.2;
let (t_p, b) = ra.b_life_interval(p, 0.9).unwrap();
let (r, ci) = ra.reliability_interval(t_p, 0.9).unwrap();
assert!((r - (1.0 - p)).abs() < 1e-12);
let half_u = (-ci.lower.ln()).ln() - (-r.ln()).ln();
let half_t = (b.upper / t_p).ln();
assert!(
(half_u - ra.shape() * half_t).abs() < 1e-9,
"{half_u} vs {half_t}"
);
}
#[test]
fn reliability_limits_stay_inside_zero_one_and_are_exact_at_time_zero() {
let ra = estimated();
for t in [1e-3, 1.0, 50.0, 120.0, 400.0, 2000.0] {
let (r, ci) = ra.reliability_interval(t, 0.99).unwrap();
assert!(
0.0 <= ci.lower && ci.lower <= r && r <= ci.upper && ci.upper <= 1.0,
"{t}: {ci:?}"
);
}
let (r, ci) = ra.reliability_interval(0.0, 0.95).unwrap();
assert_eq!((r, ci.lower, ci.upper), (1.0, 1.0, 1.0));
assert!(ra.reliability_interval(f64::NAN, 0.95).is_none());
assert!(ra.reliability_interval(10.0, 1.0).is_none());
}
#[test]
fn intervals_need_a_covariance() {
let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
assert!(ra.b_life_interval(0.1, 0.95).is_none());
assert!(ra.reliability_interval(10.0, 0.95).is_none());
let mrr = weibull_mrr(&[12.0, 25.0, 33.0, 47.0, 58.0, 71.0]).unwrap();
assert!(ReliabilityAnalysis::from_mrr(&mrr).covariance().is_none());
}
#[test]
fn with_covariance_takes_only_a_covariance() {
let ra = || ReliabilityAnalysis::new(2.0, 100.0).unwrap();
assert!(ra().with_covariance([[0.04, 0.1], [0.1, 25.0]]).is_some());
assert!(
ra().with_covariance([[0.04, 0.1], [0.2, 25.0]]).is_none(),
"asymmetric"
);
assert!(
ra().with_covariance([[0.04, 1.0], [1.0, 25.0]]).is_none(),
"singular"
);
assert!(
ra().with_covariance([[-0.04, 0.0], [0.0, 25.0]]).is_none(),
"negative variance"
);
assert!(ra()
.with_covariance([[f64::NAN, 0.0], [0.0, 25.0]])
.is_none());
}
#[test]
fn the_mle_s_b_life_interval_is_this_one() {
let fit = weibull_mle(&[55.0, 187.0, 216.0, 240.0, 244.0, 335.0, 361.0, 373.0]).unwrap();
let ra = ReliabilityAnalysis::from_mle(&fit);
assert_eq!(fit.b_life_interval(0.1, 0.9), ra.b_life_interval(0.1, 0.9));
}
#[test]
fn the_intervals_cover_near_their_level() {
use u_numflow::distributions::{Sample, Weibull};
let truth = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
let (r_true, b10_true) = (truth.reliability(60.0), truth.b_life(0.1).unwrap());
let law = Weibull::new(2.0, 100.0).unwrap();
let mut rng = u_numflow::random::create_rng(2026);
let runs = 600;
let (mut r_in, mut b_in) = (0, 0);
for _ in 0..runs {
let data: Vec<f64> = (0..40).map(|_| law.sample(&mut rng)).collect();
let ra = ReliabilityAnalysis::from_mle(&weibull_mle(&data).unwrap());
let (_, r) = ra.reliability_interval(60.0, 0.9).unwrap();
let (_, b) = ra.b_life_interval(0.1, 0.9).unwrap();
r_in += usize::from(r.lower <= r_true && r_true <= r.upper);
b_in += usize::from(b.lower <= b10_true && b10_true <= b.upper);
}
let (rc, bc) = (r_in as f64 / runs as f64, b_in as f64 / runs as f64);
assert!((0.85..=0.95).contains(&rc), "R(60) coverage {rc}");
assert!((0.85..=0.95).contains(&bc), "B10 coverage {bc}");
}
#[test]
fn test_new_valid() {
assert!(ReliabilityAnalysis::new(2.0, 50.0).is_some());
}
#[test]
fn test_new_invalid() {
assert!(ReliabilityAnalysis::new(0.0, 50.0).is_none());
assert!(ReliabilityAnalysis::new(-1.0, 50.0).is_none());
assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
assert!(ReliabilityAnalysis::new(2.0, -1.0).is_none());
assert!(ReliabilityAnalysis::new(f64::NAN, 50.0).is_none());
assert!(ReliabilityAnalysis::new(2.0, f64::INFINITY).is_none());
}
#[test]
fn test_reliability_at_zero() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
assert!((ra.reliability(0.0) - 1.0).abs() < 1e-15);
}
#[test]
fn test_reliability_decreasing() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
let mut prev = 1.0;
for i in 1..=100 {
let t = i as f64;
let r = ra.reliability(t);
assert!(
r <= prev + 1e-15,
"reliability should be non-increasing: R({}) = {} > R({}) = {}",
t,
r,
t - 1.0,
prev
);
prev = r;
}
}
#[test]
fn test_reliability_at_scale() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
let r_at_scale = ra.reliability(50.0);
let expected = (-1.0_f64).exp();
assert!(
(r_at_scale - expected).abs() < 1e-10,
"R(eta) = {}, expected exp(-1) = {}",
r_at_scale,
expected
);
}
#[test]
fn test_reliability_negative_time() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
assert!((ra.reliability(-10.0) - 1.0).abs() < 1e-15);
}
#[test]
fn test_hazard_rate_exponential() {
let ra = ReliabilityAnalysis::new(1.0, 20.0).expect("valid parameters");
for t in [5.0, 10.0, 20.0, 50.0] {
assert!(
(ra.hazard_rate(t) - 1.0 / 20.0).abs() < 1e-10,
"hazard at t={} should be 1/eta=0.05",
t
);
}
}
#[test]
fn test_hazard_rate_increasing() {
let ra = ReliabilityAnalysis::new(3.0, 50.0).expect("valid parameters");
let h1 = ra.hazard_rate(10.0);
let h2 = ra.hazard_rate(20.0);
let h3 = ra.hazard_rate(30.0);
assert!(
h1 < h2,
"hazard should increase: h(10)={} >= h(20)={}",
h1,
h2
);
assert!(
h2 < h3,
"hazard should increase: h(20)={} >= h(30)={}",
h2,
h3
);
}
#[test]
fn test_hazard_rate_zero_time() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
assert!((ra.hazard_rate(0.0)).abs() < 1e-15);
assert!((ra.hazard_rate(-5.0)).abs() < 1e-15);
}
#[test]
fn test_mtbf_exponential() {
let ra = ReliabilityAnalysis::new(1.0, 100.0).expect("valid parameters");
assert!(
(ra.mtbf() - 100.0).abs() < 1e-8,
"MTBF = {}, expected 100.0 for exponential",
ra.mtbf()
);
}
#[test]
fn test_mtbf_rayleigh() {
let ra = ReliabilityAnalysis::new(2.0, 1.0).expect("valid parameters");
let expected = std::f64::consts::PI.sqrt() / 2.0;
assert!(
(ra.mtbf() - expected).abs() < 1e-10,
"MTBF = {}, expected sqrt(pi)/2 = {}",
ra.mtbf(),
expected
);
}
#[test]
fn test_time_to_reliability_median() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
let t_median = ra.time_to_reliability(0.5).expect("p=0.5 is valid");
let expected = 50.0 * (2.0_f64.ln()).powf(0.5);
assert!(
(t_median - expected).abs() < 1e-10,
"median life = {}, expected {}",
t_median,
expected
);
}
#[test]
fn test_time_to_reliability_invalid() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
assert!(ra.time_to_reliability(0.0).is_none());
assert!(ra.time_to_reliability(1.0).is_none());
assert!(ra.time_to_reliability(-0.1).is_none());
assert!(ra.time_to_reliability(1.5).is_none());
}
#[test]
fn test_time_to_reliability_roundtrip() {
let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
let t = ra.time_to_reliability(p).expect("valid p");
let r = ra.reliability(t);
assert!(
(r - p).abs() < 1e-10,
"roundtrip: time_to_reliability({}) = {}, reliability({}) = {}",
p,
t,
t,
r
);
}
}
#[test]
fn test_b_life_b10() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
let b10 = ra.b_life(0.10).expect("valid fraction");
let t_r90 = ra.time_to_reliability(0.90).expect("valid p");
assert!(
(b10 - t_r90).abs() < 1e-10,
"B10 = {}, time_to_reliability(0.90) = {}",
b10,
t_r90
);
}
#[test]
fn test_b_life_invalid() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
assert!(ra.b_life(0.0).is_none());
assert!(ra.b_life(1.0).is_none());
assert!(ra.b_life(-0.1).is_none());
}
#[test]
fn test_from_mle() {
let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
let mle = weibull_mle(&data).expect("MLE should converge");
let ra = ReliabilityAnalysis::from_mle(&mle);
assert!((ra.shape() - mle.shape).abs() < 1e-15);
assert!((ra.scale() - mle.scale).abs() < 1e-15);
assert!(ra.mtbf() > 0.0);
}
#[test]
fn test_from_mrr() {
let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
let mrr = weibull_mrr(&data).expect("MRR should succeed");
let ra = ReliabilityAnalysis::from_mrr(&mrr);
assert!((ra.shape() - mrr.shape).abs() < 1e-15);
assert!((ra.scale() - mrr.scale).abs() < 1e-15);
assert!(ra.mtbf() > 0.0);
}
#[test]
fn test_b_life_ordering() {
let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
let b5 = ra.b_life(0.05).expect("valid");
let b10 = ra.b_life(0.10).expect("valid");
let b50 = ra.b_life(0.50).expect("valid");
assert!(b5 < b10, "B5={} should < B10={}", b5, b10);
assert!(b10 < b50, "B10={} should < B50={}", b10, b50);
}
#[test]
fn test_hazard_pdf_reliability_relation() {
let ra = ReliabilityAnalysis::new(2.5, 50.0).expect("valid parameters");
for t in [5.0, 20.0, 50.0, 80.0] {
let z = t / ra.scale();
let pdf =
(ra.shape() / ra.scale()) * z.powf(ra.shape() - 1.0) * (-z.powf(ra.shape())).exp();
let h = ra.hazard_rate(t);
let r = ra.reliability(t);
assert!(
(h * r - pdf).abs() < 1e-10,
"h(t)*R(t) = {} should equal f(t) = {} at t={}",
h * r,
pdf,
t
);
}
}
#[test]
fn test_accessors() {
let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
assert!((ra.shape() - 2.5).abs() < 1e-15);
assert!((ra.scale() - 100.0).abs() < 1e-15);
}
#[test]
fn test_reference_reliability_beta2_eta100() {
let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
assert!(
(ra.reliability(0.0) - 1.0).abs() < 1e-15,
"R(0) = {}, expected 1.0",
ra.reliability(0.0)
);
let r100 = ra.reliability(100.0);
let expected_r100 = (-1.0_f64).exp(); assert!(
(r100 - expected_r100).abs() < 1e-5,
"R(100) = {:.6}, expected {:.6}",
r100,
expected_r100
);
let r50 = ra.reliability(50.0);
let expected_r50 = (-0.25_f64).exp(); assert!(
(r50 - expected_r50).abs() < 1e-5,
"R(50) = {:.6}, expected {:.6}",
r50,
expected_r50
);
}
#[test]
fn test_reference_cdf_beta2_eta100() {
let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
let f100 = 1.0 - ra.reliability(100.0);
let expected_f100 = 1.0 - (-1.0_f64).exp(); assert!(
(f100 - expected_f100).abs() < 1e-5,
"F(100) = {:.6}, expected {:.6}",
f100,
expected_f100
);
}
#[test]
fn test_reference_hazard_rate_beta2_eta100() {
let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
let h100 = ra.hazard_rate(100.0);
assert!(
(h100 - 0.02).abs() < 1e-6,
"h(100) = {:.8}, expected 0.02",
h100
);
let h50 = ra.hazard_rate(50.0);
assert!(
(h50 - 0.01).abs() < 1e-6,
"h(50) = {:.8}, expected 0.01",
h50
);
}
#[test]
fn test_reference_mttf_beta2_eta100() {
let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
let mttf = ra.mtbf();
let expected_mttf = 100.0 * std::f64::consts::PI.sqrt() / 2.0; assert!(
(mttf - expected_mttf).abs() < 0.001,
"MTTF = {:.6}, expected {:.6}",
mttf,
expected_mttf
);
}
#[test]
fn test_reference_b10_beta2_eta100() {
let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
let b10 = ra.b_life(0.10).expect("valid fraction");
let expected_b10 = 100.0 * (-0.9_f64.ln()).powf(0.5); assert!(
(b10 - expected_b10).abs() < 0.01,
"B10 = {:.5}, expected {:.5}",
b10,
expected_b10
);
}
}