use u_numflow::special::gamma;
#[derive(Debug, Clone)]
pub struct ReliabilityAnalysis {
shape: f64,
scale: f64,
}
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 })
}
pub fn from_mle(result: &super::mle::WeibullMleResult) -> Self {
Self {
shape: result.shape,
scale: result.scale,
}
}
pub fn from_mrr(result: &super::mrr::WeibullMrrResult) -> Self {
Self {
shape: result.shape,
scale: result.scale,
}
}
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::weibull::{weibull_mle, weibull_mrr};
#[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
);
}
}