#[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> {
let n = failure_times.len();
if n < 2 {
return None;
}
if !failure_times.iter().all(|&t| t.is_finite() && t > 0.0) {
return None;
}
let mut sorted = failure_times.to_vec();
sorted.sort_unstable_by(|a, b| a.partial_cmp(b).expect("NaN values already filtered above"));
let n_f = n as f64;
let mut x_vals = Vec::with_capacity(n);
let mut y_vals = Vec::with_capacity(n);
for (i, &t) in sorted.iter().enumerate() {
let rank = (i + 1) as f64;
let f_i = (rank - 0.3) / (n_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 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 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"
);
}
}