#[derive(Debug, Clone)]
pub struct WeibullMrrResult {
pub shape: f64,
pub scale: f64,
pub r_squared: f64,
}
pub fn weibull_mrr(failure_times: &[f64]) -> Option<WeibullMrrResult> {
weibull_mrr_censored(failure_times, &[])
}
pub fn weibull_mrr_censored(
failure_times: &[f64],
suspension_times: &[f64],
) -> Option<WeibullMrrResult> {
if failure_times.len() < 2 {
return None;
}
let valid = |t: &f64| t.is_finite() && *t > 0.0;
if !failure_times.iter().all(valid) || !suspension_times.iter().all(valid) {
return None;
}
let mut units: Vec<(f64, bool)> = failure_times
.iter()
.map(|&t| (t, true))
.chain(suspension_times.iter().map(|&t| (t, false)))
.collect();
units.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.expect("NaN values already filtered above")
.then(b.1.cmp(&a.1))
});
let total = units.len();
let total_f = total as f64;
let mut x_vals = Vec::with_capacity(failure_times.len());
let mut y_vals = Vec::with_capacity(failure_times.len());
let mut adjusted = 0.0_f64;
for (i, &(t, failed)) in units.iter().enumerate() {
if !failed {
continue;
}
let reverse_rank = (total - i) as f64;
adjusted += (total_f + 1.0 - adjusted) / (1.0 + reverse_rank);
let f_i = (adjusted - 0.3) / (total_f + 0.4);
let x = t.ln();
let y = (-(1.0 - f_i).ln()).ln();
if !x.is_finite() || !y.is_finite() {
return None;
}
x_vals.push(x);
y_vals.push(y);
}
let n_f = x_vals.len() as f64;
let sum_x: f64 = x_vals.iter().sum();
let sum_y: f64 = y_vals.iter().sum();
let sum_xy: f64 = x_vals.iter().zip(y_vals.iter()).map(|(x, y)| x * y).sum();
let sum_x2: f64 = x_vals.iter().map(|x| x * x).sum();
let sum_y2: f64 = y_vals.iter().map(|y| y * y).sum();
let denom = n_f * sum_x2 - sum_x * sum_x;
if denom.abs() < 1e-30 {
return None; }
let b = (n_f * sum_xy - sum_x * sum_y) / denom;
let a = (sum_y - b * sum_x) / n_f;
let beta = b;
if beta <= 0.0 || !beta.is_finite() {
return None;
}
let eta = (-a / b).exp();
if !eta.is_finite() || eta <= 0.0 {
return None;
}
let mean_y = sum_y / n_f;
let ss_tot = sum_y2 - n_f * mean_y * mean_y;
let ss_res = sum_y2 - 2.0 * a * sum_y - 2.0 * b * sum_xy
+ n_f * a * a
+ 2.0 * a * b * sum_x
+ b * b * sum_x2;
let r_squared = if ss_tot.abs() < 1e-30 {
1.0 } else {
1.0 - ss_res / ss_tot
};
Some(WeibullMrrResult {
shape: beta,
scale: eta,
r_squared,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn without_suspensions_it_is_the_plain_fit_bit_for_bit() {
let data = [
72.0, 82.0, 97.0, 103.0, 113.0, 117.0, 126.0, 127.0, 127.0, 139.0,
];
let plain = weibull_mrr(&data).unwrap();
let censored = weibull_mrr_censored(&data, &[]).unwrap();
assert_eq!(
(
plain.shape.to_bits(),
plain.scale.to_bits(),
plain.r_squared.to_bits()
),
(
censored.shape.to_bits(),
censored.scale.to_bits(),
censored.r_squared.to_bits()
)
);
}
#[test]
fn adjusted_ranks_are_johnson_s() {
let fit = weibull_mrr_censored(&[10.0, 30.0, 40.0], &[20.0, 50.0]).unwrap();
let points: Vec<(f64, f64)> = [(10.0, 1.0), (30.0, 2.25), (40.0, 3.5)]
.iter()
.map(|&(t, a): &(f64, f64)| {
let f = (a - 0.3) / 5.4;
(t.ln(), (-(1.0 - f).ln()).ln())
})
.collect();
let n = 3.0;
let (sx, sy) = (
points.iter().map(|p| p.0).sum::<f64>(),
points.iter().map(|p| p.1).sum::<f64>(),
);
let sxy: f64 = points.iter().map(|p| p.0 * p.1).sum();
let sxx: f64 = points.iter().map(|p| p.0 * p.0).sum();
let b = (n * sxy - sx * sy) / (n * sxx - sx * sx);
let a = (sy - b * sx) / n;
assert!((fit.shape - b).abs() < 1e-12, "{} vs {b}", fit.shape);
assert!((fit.scale - (-a / b).exp()).abs() < 1e-9);
}
#[test]
fn a_failure_is_ranked_before_a_suspension_at_its_time() {
let tied = weibull_mrr_censored(&[10.0, 30.0, 40.0], &[10.0]).unwrap();
let after = weibull_mrr_censored(&[10.0, 30.0, 40.0], &[10.0 + 1e-9]).unwrap();
assert!((tied.shape - after.shape).abs() < 1e-12);
}
#[test]
fn suspensions_lengthen_the_scale_like_the_mle() {
let failures = [55.0, 187.0, 216.0, 240.0, 244.0, 335.0];
let survivors = [361.0, 373.0, 375.0, 386.0];
let mrr = weibull_mrr_censored(&failures, &survivors).unwrap();
let dropped = weibull_mrr(&failures).unwrap();
let mle = crate::weibull::weibull_mle_censored(&failures, &survivors).unwrap();
assert!(mrr.scale > dropped.scale);
assert!(
(mrr.scale / mle.scale - 1.0).abs() < 0.25,
"{} vs {}",
mrr.scale,
mle.scale
);
}
#[test]
fn censored_refuses_what_the_plain_fit_refuses() {
assert!(weibull_mrr_censored(&[10.0], &[20.0, 30.0]).is_none());
assert!(weibull_mrr_censored(&[10.0, 20.0], &[-1.0]).is_none());
assert!(weibull_mrr_censored(&[10.0, 20.0], &[f64::NAN]).is_none());
}
#[test]
fn test_mrr_uniform_spacing() {
let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
let result = weibull_mrr(&data).expect("MRR should succeed");
assert!(
result.shape > 1.0 && result.shape < 5.0,
"shape = {}, expected in [1.0, 5.0]",
result.shape
);
assert!(
result.scale > 40.0 && result.scale < 100.0,
"scale = {}, expected in [40, 100]",
result.scale
);
assert!(
result.r_squared > 0.9,
"R^2 = {}, expected > 0.9",
result.r_squared
);
}
#[test]
fn test_mrr_near_exponential() {
let data = [5.0, 10.0, 15.0, 25.0, 35.0, 50.0, 75.0, 100.0];
let result = weibull_mrr(&data).expect("MRR should succeed");
assert!(
result.shape > 0.5 && result.shape < 2.0,
"shape = {}, expected near 1.0",
result.shape
);
}
#[test]
fn test_mrr_insufficient_data() {
assert!(weibull_mrr(&[]).is_none());
assert!(weibull_mrr(&[10.0]).is_none());
}
#[test]
fn test_mrr_invalid_data() {
assert!(weibull_mrr(&[0.0, 10.0, 20.0]).is_none());
assert!(weibull_mrr(&[-5.0, 10.0, 20.0]).is_none());
assert!(weibull_mrr(&[f64::NAN, 10.0, 20.0]).is_none());
assert!(weibull_mrr(&[f64::INFINITY, 10.0, 20.0]).is_none());
}
#[test]
fn test_mrr_r_squared_range() {
let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
let result = weibull_mrr(&data).expect("MRR should succeed");
assert!(
result.r_squared >= 0.0 && result.r_squared <= 1.0,
"R^2 = {}, expected in [0, 1]",
result.r_squared
);
}
#[test]
fn test_mrr_known_weibull_data() {
let data: Vec<f64> = (1..=10)
.map(|i| {
let f = (i as f64 - 0.5) / 10.0;
50.0 * (-(1.0 - f).ln()).powf(0.5)
})
.collect();
let result = weibull_mrr(&data).expect("MRR should succeed");
assert!(
(result.shape - 2.0).abs() < 0.5,
"shape = {}, expected near 2.0",
result.shape
);
assert!(
(result.scale - 50.0).abs() < 15.0,
"scale = {}, expected near 50.0",
result.scale
);
assert!(
result.r_squared > 0.95,
"R^2 = {}, expected > 0.95 for exact Weibull data",
result.r_squared
);
}
#[test]
fn test_mrr_unsorted_input() {
let data1 = [10.0, 20.0, 30.0, 40.0, 50.0];
let data2 = [50.0, 10.0, 40.0, 20.0, 30.0];
let r1 = weibull_mrr(&data1).expect("MRR should succeed");
let r2 = weibull_mrr(&data2).expect("MRR should succeed");
assert!(
(r1.shape - r2.shape).abs() < 1e-10,
"shape should be order-independent"
);
assert!(
(r1.scale - r2.scale).abs() < 1e-10,
"scale should be order-independent"
);
}
#[test]
fn test_mrr_mle_agreement() {
let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
let mrr_result = weibull_mrr(&data).expect("MRR should succeed");
let mle_result = super::super::mle::weibull_mle(&data).expect("MLE should converge");
assert!(
(mrr_result.shape - mle_result.shape).abs() < 1.5,
"MRR shape = {}, MLE shape = {}",
mrr_result.shape,
mle_result.shape
);
assert!(
(mrr_result.scale - mle_result.scale).abs() / mle_result.scale < 0.3,
"MRR scale = {}, MLE scale = {}",
mrr_result.scale,
mle_result.scale
);
}
fn benard_rank(i: usize, n: usize) -> f64 {
(i as f64 - 0.3) / (n as f64 + 0.4)
}
#[test]
fn test_benard_median_rank_values() {
let n = 10_usize;
assert!(
(benard_rank(1, n) - 0.067_308).abs() < 1e-5,
"rank 1: got {}, expected 0.067308",
benard_rank(1, n)
);
assert!(
(benard_rank(5, n) - 0.451_923).abs() < 1e-5,
"rank 5: got {}, expected 0.451923",
benard_rank(5, n)
);
assert!(
(benard_rank(10, n) - 0.932_692).abs() < 1e-5,
"rank 10: got {}, expected 0.932692",
benard_rank(10, n)
);
}
#[test]
fn test_mrr_identical_values() {
let data = [10.0, 10.0, 10.0, 10.0, 10.0];
assert!(
weibull_mrr(&data).is_none(),
"MRR should return None for identical values"
);
}
}