#[derive(Debug, Clone)]
pub struct WeibullMleResult {
pub shape: f64,
pub scale: f64,
pub log_likelihood: f64,
pub iterations: usize,
}
const MAX_ITER: usize = 100;
const TOL: f64 = 1e-10;
pub fn weibull_mle(failure_times: &[f64]) -> Option<WeibullMleResult> {
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 ln_t: Vec<f64> = failure_times.iter().map(|t| t.ln()).collect();
let sum_ln_t: f64 = ln_t.iter().sum();
let n_f = n as f64;
let mut beta = 1.2_f64; let mut iterations = 0;
for iter in 0..MAX_ITER {
iterations = iter + 1;
let mut s0 = 0.0_f64;
let mut s1 = 0.0_f64;
let mut s2 = 0.0_f64;
for (i, &t) in failure_times.iter().enumerate() {
let t_beta = t.powf(beta);
let lt = ln_t[i];
s0 += t_beta;
s1 += t_beta * lt;
s2 += t_beta * lt * lt;
}
if s0 == 0.0 {
return None;
}
let f_val = n_f / beta + sum_ln_t - n_f * s1 / s0;
let f_prime = -n_f / (beta * beta) - n_f * (s2 * s0 - s1 * s1) / (s0 * s0);
if f_prime.abs() < 1e-30 {
return None; }
let delta = f_val / f_prime;
beta -= delta;
if beta <= 0.0 {
beta = 0.01;
}
if delta.abs() < TOL {
break;
}
if iter == MAX_ITER - 1 {
return None; }
}
let s0: f64 = failure_times.iter().map(|t| t.powf(beta)).sum();
let eta = (s0 / n_f).powf(1.0 / beta);
if !eta.is_finite() || eta <= 0.0 {
return None;
}
let log_likelihood = n_f * beta.ln() - n_f * beta * eta.ln() + (beta - 1.0) * sum_ln_t
- failure_times
.iter()
.map(|&t| (t / eta).powf(beta))
.sum::<f64>();
Some(WeibullMleResult {
shape: beta,
scale: eta,
log_likelihood,
iterations,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mle_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_mle(&data).expect("MLE should converge");
assert!(
result.shape > 1.5 && result.shape < 5.0,
"shape = {}, expected in [1.5, 5.0]",
result.shape
);
assert!(
result.scale > 40.0 && result.scale < 100.0,
"scale = {}, expected in [40, 100]",
result.scale
);
assert!(result.log_likelihood.is_finite());
}
#[test]
fn test_mle_near_exponential() {
let data = [5.0, 10.0, 15.0, 25.0, 35.0, 50.0, 75.0, 100.0];
let result = weibull_mle(&data).expect("MLE should converge");
assert!(
result.shape > 0.5 && result.shape < 2.0,
"shape = {}, expected near 1.0",
result.shape
);
}
#[test]
fn test_mle_converges_with_iterations() {
let data = [10.0, 20.0, 30.0, 40.0, 50.0];
let result = weibull_mle(&data).expect("MLE should converge");
assert!(result.iterations > 0);
assert!(result.iterations <= MAX_ITER);
}
#[test]
fn test_mle_insufficient_data() {
assert!(weibull_mle(&[]).is_none());
assert!(weibull_mle(&[10.0]).is_none());
}
#[test]
fn test_mle_invalid_data() {
assert!(weibull_mle(&[0.0, 10.0, 20.0]).is_none());
assert!(weibull_mle(&[-5.0, 10.0, 20.0]).is_none());
assert!(weibull_mle(&[f64::NAN, 10.0, 20.0]).is_none());
assert!(weibull_mle(&[f64::INFINITY, 10.0, 20.0]).is_none());
}
#[test]
fn test_mle_log_likelihood_is_negative() {
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_mle(&data).expect("MLE should converge");
assert!(
result.log_likelihood < 0.0,
"log_likelihood = {}, expected < 0",
result.log_likelihood
);
}
#[test]
fn test_mle_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_mle(&data).expect("MLE should converge");
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
);
}
#[test]
fn test_mle_identical_values() {
let data = [10.0, 10.0, 10.0, 10.0, 10.0];
if let Some(result) = weibull_mle(&data) {
assert!(result.shape.is_finite() && result.shape > 0.0);
assert!(result.scale.is_finite() && result.scale > 0.0);
}
}
#[test]
fn test_mle_lawless_ball_bearing() {
let data = vec![
17.88, 28.92, 33.0, 41.52, 42.12, 45.6, 48.48, 51.84, 51.96, 54.12, 55.56, 67.8, 68.64,
68.64, 68.88, 84.12, 93.12, 98.64, 105.12, 105.84, 127.92, 128.04, 173.4_f64,
];
let result = weibull_mle(&data).expect("MLE should converge on Lawless §4.2 data");
assert!(
(result.shape - 2.102).abs() < 0.01,
"shape β = {:.6}, expected 2.102 ± 0.01",
result.shape
);
assert!(
(result.scale - 81.88).abs() < 0.5,
"scale η = {:.6}, expected 81.88 ± 0.5",
result.scale
);
assert!(result.log_likelihood.is_finite());
assert!(
result.iterations <= 20,
"expected convergence in ≤ 20 iterations, got {}",
result.iterations
);
}
}