1use u_numflow::special::inverse_normal_cdf;
9
10#[derive(Debug, Clone)]
12pub struct WeibullMleResult {
13 pub shape: f64,
15 pub scale: f64,
17 pub log_likelihood: f64,
19 pub iterations: usize,
21 pub covariance: [[f64; 2]; 2],
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct WeibullInterval {
30 pub lower: f64,
32 pub upper: f64,
34}
35
36impl WeibullMleResult {
37 pub fn shape_interval(&self, confidence: f64) -> Option<WeibullInterval> {
41 log_normal_interval(self.shape, self.covariance[0][0], confidence)
42 }
43
44 pub fn scale_interval(&self, confidence: f64) -> Option<WeibullInterval> {
47 log_normal_interval(self.scale, self.covariance[1][1], confidence)
48 }
49
50 pub fn b_life_interval(
71 &self,
72 fraction_failed: f64,
73 confidence: f64,
74 ) -> Option<(f64, WeibullInterval)> {
75 super::ReliabilityAnalysis::from_mle(self).b_life_interval(fraction_failed, confidence)
76 }
77}
78
79pub(super) fn two_sided_z(confidence: f64) -> Option<f64> {
82 (confidence > 0.0 && confidence < 1.0)
83 .then(|| inverse_normal_cdf(1.0 - (1.0 - confidence) / 2.0))
84}
85
86fn log_normal_interval(estimate: f64, variance: f64, confidence: f64) -> Option<WeibullInterval> {
87 let z = two_sided_z(confidence)?;
88 let half = z * variance.max(0.0).sqrt() / estimate;
89 Some(WeibullInterval {
90 lower: estimate * (-half).exp(),
91 upper: estimate * half.exp(),
92 })
93}
94
95const MAX_ITER: usize = 100;
97
98const TOL: f64 = 1e-10;
100
101pub fn weibull_mle(failure_times: &[f64]) -> Option<WeibullMleResult> {
160 weibull_mle_censored(failure_times, &[])
161}
162
163pub fn weibull_mle_censored(
203 failure_times: &[f64],
204 suspension_times: &[f64],
205) -> Option<WeibullMleResult> {
206 let r = failure_times.len();
207 if r < 2 {
208 return None;
209 }
210
211 if !failure_times
213 .iter()
214 .chain(suspension_times)
215 .all(|&t| t.is_finite() && t > 0.0)
216 {
217 return None;
218 }
219
220 let all: Vec<f64> = failure_times
223 .iter()
224 .chain(suspension_times)
225 .copied()
226 .collect();
227 let ln_t: Vec<f64> = all.iter().map(|t| t.ln()).collect();
228 let sum_ln_t: f64 = ln_t[..r].iter().sum();
229 let n_f = r as f64;
230
231 let mut beta = 1.2_f64; let mut iterations = 0;
240
241 for iter in 0..MAX_ITER {
242 iterations = iter + 1;
243
244 let mut s0 = 0.0_f64;
246 let mut s1 = 0.0_f64;
247 let mut s2 = 0.0_f64;
248
249 for (i, &t) in all.iter().enumerate() {
250 let t_beta = t.powf(beta);
251 let lt = ln_t[i];
252 s0 += t_beta;
253 s1 += t_beta * lt;
254 s2 += t_beta * lt * lt;
255 }
256
257 if s0 == 0.0 {
258 return None;
259 }
260
261 let f_val = n_f / beta + sum_ln_t - n_f * s1 / s0;
262 let f_prime = -n_f / (beta * beta) - n_f * (s2 * s0 - s1 * s1) / (s0 * s0);
263
264 if f_prime.abs() < 1e-30 {
265 return None; }
267
268 let delta = f_val / f_prime;
269 beta -= delta;
270
271 if beta <= 0.0 {
273 beta = 0.01;
274 }
275
276 if delta.abs() < TOL {
277 break;
278 }
279
280 if iter == MAX_ITER - 1 {
281 return None; }
283 }
284
285 let s0: f64 = all.iter().map(|t| t.powf(beta)).sum();
287 let eta = (s0 / n_f).powf(1.0 / beta);
288
289 if !eta.is_finite() || eta <= 0.0 {
290 return None;
291 }
292
293 let log_likelihood = n_f * beta.ln() - n_f * beta * eta.ln() + (beta - 1.0) * sum_ln_t
295 - all.iter().map(|&t| (t / eta).powf(beta)).sum::<f64>();
296
297 let covariance = observed_covariance(&all, r, beta, eta)?;
298
299 Some(WeibullMleResult {
300 shape: beta,
301 scale: eta,
302 log_likelihood,
303 iterations,
304 covariance,
305 })
306}
307
308fn observed_covariance(all: &[f64], r: usize, beta: f64, eta: f64) -> Option<[[f64; 2]; 2]> {
321 let r = r as f64;
322 let (mut sz, mut szy, mut szy2) = (0.0, 0.0, 0.0);
323 for &t in all {
324 let y = (t / eta).ln();
325 let z = (beta * y).exp();
326 sz += z;
327 szy += z * y;
328 szy2 += z * y * y;
329 }
330 let i_bb = r / (beta * beta) + szy2;
331 let i_ee = -r * beta / (eta * eta) + beta * (beta + 1.0) / (eta * eta) * sz;
332 let i_be = r / eta - sz / eta - beta / eta * szy;
333 let det = i_bb * i_ee - i_be * i_be;
334 if !(det > 0.0 && i_bb > 0.0 && det.is_finite()) {
335 return None;
336 }
337 Some([[i_ee / det, -i_be / det], [-i_be / det, i_bb / det]])
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
351 fn covariance_approaches_the_asymptotic_weibull_variances() {
352 let (beta, eta, n) = (2.0_f64, 100.0_f64, 2000usize);
353 let data: Vec<f64> = (1..=n)
354 .map(|i| {
355 let p = (i as f64 - 0.5) / n as f64;
356 eta * (-(1.0 - p).ln()).powf(1.0 / beta)
357 })
358 .collect();
359 let fit = weibull_mle(&data).unwrap();
360 let nf = n as f64;
361 let vb = nf * fit.covariance[0][0] / (fit.shape * fit.shape);
362 let ve = nf * fit.covariance[1][1] * fit.shape * fit.shape / (fit.scale * fit.scale);
363 let gamma = 0.577_215_664_901_532_9_f64;
364 let pi2 = std::f64::consts::PI.powi(2);
365 assert!((vb - 6.0 / pi2).abs() < 0.02, "{vb}");
366 assert!(
367 (ve - (1.0 + 6.0 * (1.0 - gamma).powi(2) / pi2)).abs() < 0.03,
368 "{ve}"
369 );
370 }
371
372 #[test]
375 fn covariance_is_the_inverse_observed_information() {
376 let fit = weibull_mle_censored(&FAILURES, &SUSPENSIONS).unwrap();
377 let (b, e) = (fit.shape, fit.scale);
378 let l = |b: f64, e: f64| censored_loglik(&FAILURES, &SUSPENSIONS, b, e);
379 let (hb, he) = (1e-4, 1e-2);
380 let d_bb = (l(b + hb, e) - 2.0 * l(b, e) + l(b - hb, e)) / (hb * hb);
381 let d_ee = (l(b, e + he) - 2.0 * l(b, e) + l(b, e - he)) / (he * he);
382 let d_be = (l(b + hb, e + he) - l(b + hb, e - he) - l(b - hb, e + he) + l(b - hb, e - he))
383 / (4.0 * hb * he);
384 let (i_bb, i_ee, i_be) = (-d_bb, -d_ee, -d_be);
385 let det = i_bb * i_ee - i_be * i_be;
386 let want = [[i_ee / det, -i_be / det], [-i_be / det, i_bb / det]];
387 for (row, wrow) in fit.covariance.iter().zip(&want) {
388 for (got, w) in row.iter().zip(wrow) {
389 assert!(
390 (got - w).abs() <= 1e-3 * w.abs().max(1e-6),
391 "{:?} vs {want:?}",
392 fit.covariance
393 );
394 }
395 }
396 }
397
398 #[test]
399 fn intervals_bracket_and_widen() {
400 let fit = weibull_mle_censored(&FAILURES, &SUSPENSIONS).unwrap();
401 let (s90, s99) = (
402 fit.shape_interval(0.90).unwrap(),
403 fit.shape_interval(0.99).unwrap(),
404 );
405 assert!(
406 s99.lower < s90.lower
407 && s90.lower < fit.shape
408 && fit.shape < s90.upper
409 && s90.upper < s99.upper
410 );
411 let c = fit.scale_interval(0.95).unwrap();
412 assert!(c.lower < fit.scale && fit.scale < c.upper && c.lower > 0.0);
413 let (b10, ci) = fit.b_life_interval(0.10, 0.95).unwrap();
414 assert!(ci.lower < b10 && b10 < ci.upper);
415 let want = fit.scale * (-(0.9_f64).ln()).powf(1.0 / fit.shape);
417 assert!((b10 - want).abs() < 1e-9 * want);
418 assert!(fit.shape_interval(1.0).is_none() && fit.b_life_interval(0.0, 0.9).is_none());
419 }
420
421 fn censored_loglik(f: &[f64], s: &[f64], beta: f64, eta: f64) -> f64 {
425 let r = f.len() as f64;
426 r * beta.ln() - r * beta * eta.ln() + (beta - 1.0) * f.iter().map(|t| t.ln()).sum::<f64>()
427 - f.iter().chain(s).map(|t| (t / eta).powf(beta)).sum::<f64>()
428 }
429
430 const FAILURES: [f64; 6] = [55.0, 187.0, 216.0, 240.0, 244.0, 335.0];
431 const SUSPENSIONS: [f64; 4] = [361.0, 373.0, 375.0, 386.0];
432
433 #[test]
436 fn censored_fit_maximises_the_censored_likelihood() {
437 let fit = weibull_mle_censored(&FAILURES, &SUSPENSIONS).expect("converges");
438 let best = censored_loglik(&FAILURES, &SUSPENSIONS, fit.shape, fit.scale);
439 assert!((fit.log_likelihood - best).abs() < 1e-9);
440 for (db, de) in [
441 (0.01, 0.0),
442 (-0.01, 0.0),
443 (0.0, 1.0),
444 (0.0, -1.0),
445 (0.01, 1.0),
446 ] {
447 let other = censored_loglik(&FAILURES, &SUSPENSIONS, fit.shape + db, fit.scale + de);
448 assert!(other < best, "({db}, {de}): {other} >= {best}");
449 }
450 }
451
452 #[test]
454 fn no_suspensions_is_the_uncensored_fit() {
455 let a = weibull_mle_censored(&FAILURES, &[]).unwrap();
456 let b = weibull_mle(&FAILURES).unwrap();
457 assert_eq!(
458 (a.shape, a.scale, a.log_likelihood),
459 (b.shape, b.scale, b.log_likelihood)
460 );
461 }
462
463 #[test]
465 fn suspensions_lengthen_the_scale() {
466 let without = weibull_mle(&FAILURES).unwrap().scale;
467 let with = weibull_mle_censored(&FAILURES, &SUSPENSIONS).unwrap().scale;
468 let later: Vec<f64> = SUSPENSIONS.iter().map(|t| t * 2.0).collect();
469 let longer = weibull_mle_censored(&FAILURES, &later).unwrap().scale;
470 assert!(without < with && with < longer, "{without} {with} {longer}");
471 }
472
473 #[test]
477 fn exponential_special_case_is_time_on_test_over_failures() {
478 let ttt: f64 = FAILURES.iter().chain(&SUSPENSIONS).sum();
479 let eta = ttt / FAILURES.len() as f64;
480 let score = -(FAILURES.len() as f64) / eta + ttt / (eta * eta);
482 assert!(score.abs() < 1e-12);
483 let h = 1e-6;
484 let numeric = (censored_loglik(&FAILURES, &SUSPENSIONS, 1.0, eta + h)
485 - censored_loglik(&FAILURES, &SUSPENSIONS, 1.0, eta - h))
486 / (2.0 * h);
487 assert!(numeric.abs() < 1e-6, "{numeric}");
488 }
489
490 #[test]
491 fn censored_input_is_checked() {
492 assert!(weibull_mle_censored(&[10.0], &[20.0, 30.0]).is_none()); assert!(weibull_mle_censored(&[10.0, 20.0], &[0.0]).is_none());
494 assert!(weibull_mle_censored(&[10.0, 20.0], &[f64::NAN]).is_none());
495 }
496
497 #[test]
498 fn test_mle_uniform_spacing() {
499 let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
501 let result = weibull_mle(&data).expect("MLE should converge");
502
503 assert!(
505 result.shape > 1.5 && result.shape < 5.0,
506 "shape = {}, expected in [1.5, 5.0]",
507 result.shape
508 );
509 assert!(
511 result.scale > 40.0 && result.scale < 100.0,
512 "scale = {}, expected in [40, 100]",
513 result.scale
514 );
515 assert!(result.log_likelihood.is_finite());
516 }
517
518 #[test]
519 fn test_mle_near_exponential() {
520 let data = [5.0, 10.0, 15.0, 25.0, 35.0, 50.0, 75.0, 100.0];
522 let result = weibull_mle(&data).expect("MLE should converge");
523
524 assert!(
525 result.shape > 0.5 && result.shape < 2.0,
526 "shape = {}, expected near 1.0",
527 result.shape
528 );
529 }
530
531 #[test]
532 fn test_mle_converges_with_iterations() {
533 let data = [10.0, 20.0, 30.0, 40.0, 50.0];
534 let result = weibull_mle(&data).expect("MLE should converge");
535 assert!(result.iterations > 0);
536 assert!(result.iterations <= MAX_ITER);
537 }
538
539 #[test]
540 fn test_mle_insufficient_data() {
541 assert!(weibull_mle(&[]).is_none());
542 assert!(weibull_mle(&[10.0]).is_none());
543 }
544
545 #[test]
546 fn test_mle_invalid_data() {
547 assert!(weibull_mle(&[0.0, 10.0, 20.0]).is_none());
549 assert!(weibull_mle(&[-5.0, 10.0, 20.0]).is_none());
550 assert!(weibull_mle(&[f64::NAN, 10.0, 20.0]).is_none());
552 assert!(weibull_mle(&[f64::INFINITY, 10.0, 20.0]).is_none());
553 }
554
555 #[test]
556 fn test_mle_log_likelihood_is_negative() {
557 let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
558 let result = weibull_mle(&data).expect("MLE should converge");
559 assert!(
561 result.log_likelihood < 0.0,
562 "log_likelihood = {}, expected < 0",
563 result.log_likelihood
564 );
565 }
566
567 #[test]
568 fn test_mle_known_weibull_data() {
569 let data: Vec<f64> = (1..=10)
573 .map(|i| {
574 let f = (i as f64 - 0.5) / 10.0;
575 50.0 * (-(1.0 - f).ln()).powf(0.5) })
577 .collect();
578
579 let result = weibull_mle(&data).expect("MLE should converge");
580
581 assert!(
582 (result.shape - 2.0).abs() < 0.5,
583 "shape = {}, expected near 2.0",
584 result.shape
585 );
586 assert!(
587 (result.scale - 50.0).abs() < 15.0,
588 "scale = {}, expected near 50.0",
589 result.scale
590 );
591 }
592
593 #[test]
594 fn test_mle_identical_values() {
595 let data = [10.0, 10.0, 10.0, 10.0, 10.0];
597 if let Some(result) = weibull_mle(&data) {
599 assert!(result.shape.is_finite() && result.shape > 0.0);
600 assert!(result.scale.is_finite() && result.scale > 0.0);
601 }
602 }
603
604 #[test]
605 fn test_mle_lawless_ball_bearing() {
606 let data = vec![
610 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,
611 68.64, 68.88, 84.12, 93.12, 98.64, 105.12, 105.84, 127.92, 128.04, 173.4_f64,
612 ];
613 let result = weibull_mle(&data).expect("MLE should converge on Lawless §4.2 data");
614 assert!(
615 (result.shape - 2.102).abs() < 0.01,
616 "shape β = {:.6}, expected 2.102 ± 0.01",
617 result.shape
618 );
619 assert!(
620 (result.scale - 81.88).abs() < 0.5,
621 "scale η = {:.6}, expected 81.88 ± 0.5",
622 result.scale
623 );
624 assert!(result.log_likelihood.is_finite());
625 assert!(
626 result.iterations <= 20,
627 "expected convergence in ≤ 20 iterations, got {}",
628 result.iterations
629 );
630 }
631}