1use u_numflow::special::gamma;
7
8#[derive(Debug, Clone)]
37pub struct ReliabilityAnalysis {
38 shape: f64,
40 scale: f64,
42}
43
44impl ReliabilityAnalysis {
45 pub fn new(shape: f64, scale: f64) -> Option<Self> {
66 if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
67 return None;
68 }
69 Some(Self { shape, scale })
70 }
71
72 pub fn from_mle(result: &super::mle::WeibullMleResult) -> Self {
77 Self {
78 shape: result.shape,
79 scale: result.scale,
80 }
81 }
82
83 pub fn from_mrr(result: &super::mrr::WeibullMrrResult) -> Self {
88 Self {
89 shape: result.shape,
90 scale: result.scale,
91 }
92 }
93
94 pub fn shape(&self) -> f64 {
96 self.shape
97 }
98
99 pub fn scale(&self) -> f64 {
101 self.scale
102 }
103
104 pub fn reliability(&self, t: f64) -> f64 {
129 if t <= 0.0 {
130 return 1.0;
131 }
132 let z = t / self.scale;
133 (-z.powf(self.shape)).exp()
134 }
135
136 pub fn hazard_rate(&self, t: f64) -> f64 {
164 if t <= 0.0 {
165 return 0.0;
166 }
167 let z = t / self.scale;
168 (self.shape / self.scale) * z.powf(self.shape - 1.0)
169 }
170
171 pub fn mtbf(&self) -> f64 {
194 self.scale * gamma(1.0 + 1.0 / self.shape)
195 }
196
197 pub fn time_to_reliability(&self, p: f64) -> Option<f64> {
214 if p <= 0.0 || p >= 1.0 {
215 return None;
216 }
217 Some(self.scale * (-p.ln()).powf(1.0 / self.shape))
218 }
219
220 pub fn b_life(&self, fraction_failed: f64) -> Option<f64> {
250 if fraction_failed <= 0.0 || fraction_failed >= 1.0 {
251 return None;
252 }
253 self.time_to_reliability(1.0 - fraction_failed)
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::weibull::{weibull_mle, weibull_mrr};
261
262 #[test]
263 fn test_new_valid() {
264 assert!(ReliabilityAnalysis::new(2.0, 50.0).is_some());
265 }
266
267 #[test]
268 fn test_new_invalid() {
269 assert!(ReliabilityAnalysis::new(0.0, 50.0).is_none());
270 assert!(ReliabilityAnalysis::new(-1.0, 50.0).is_none());
271 assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
272 assert!(ReliabilityAnalysis::new(2.0, -1.0).is_none());
273 assert!(ReliabilityAnalysis::new(f64::NAN, 50.0).is_none());
274 assert!(ReliabilityAnalysis::new(2.0, f64::INFINITY).is_none());
275 }
276
277 #[test]
278 fn test_reliability_at_zero() {
279 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
280 assert!((ra.reliability(0.0) - 1.0).abs() < 1e-15);
281 }
282
283 #[test]
284 fn test_reliability_decreasing() {
285 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
286 let mut prev = 1.0;
287 for i in 1..=100 {
288 let t = i as f64;
289 let r = ra.reliability(t);
290 assert!(
291 r <= prev + 1e-15,
292 "reliability should be non-increasing: R({}) = {} > R({}) = {}",
293 t,
294 r,
295 t - 1.0,
296 prev
297 );
298 prev = r;
299 }
300 }
301
302 #[test]
303 fn test_reliability_at_scale() {
304 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
306 let r_at_scale = ra.reliability(50.0);
307 let expected = (-1.0_f64).exp();
308 assert!(
309 (r_at_scale - expected).abs() < 1e-10,
310 "R(eta) = {}, expected exp(-1) = {}",
311 r_at_scale,
312 expected
313 );
314 }
315
316 #[test]
317 fn test_reliability_negative_time() {
318 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
319 assert!((ra.reliability(-10.0) - 1.0).abs() < 1e-15);
320 }
321
322 #[test]
323 fn test_hazard_rate_exponential() {
324 let ra = ReliabilityAnalysis::new(1.0, 20.0).expect("valid parameters");
326 for t in [5.0, 10.0, 20.0, 50.0] {
327 assert!(
328 (ra.hazard_rate(t) - 1.0 / 20.0).abs() < 1e-10,
329 "hazard at t={} should be 1/eta=0.05",
330 t
331 );
332 }
333 }
334
335 #[test]
336 fn test_hazard_rate_increasing() {
337 let ra = ReliabilityAnalysis::new(3.0, 50.0).expect("valid parameters");
339 let h1 = ra.hazard_rate(10.0);
340 let h2 = ra.hazard_rate(20.0);
341 let h3 = ra.hazard_rate(30.0);
342 assert!(
343 h1 < h2,
344 "hazard should increase: h(10)={} >= h(20)={}",
345 h1,
346 h2
347 );
348 assert!(
349 h2 < h3,
350 "hazard should increase: h(20)={} >= h(30)={}",
351 h2,
352 h3
353 );
354 }
355
356 #[test]
357 fn test_hazard_rate_zero_time() {
358 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
359 assert!((ra.hazard_rate(0.0)).abs() < 1e-15);
360 assert!((ra.hazard_rate(-5.0)).abs() < 1e-15);
361 }
362
363 #[test]
364 fn test_mtbf_exponential() {
365 let ra = ReliabilityAnalysis::new(1.0, 100.0).expect("valid parameters");
367 assert!(
368 (ra.mtbf() - 100.0).abs() < 1e-8,
369 "MTBF = {}, expected 100.0 for exponential",
370 ra.mtbf()
371 );
372 }
373
374 #[test]
375 fn test_mtbf_rayleigh() {
376 let ra = ReliabilityAnalysis::new(2.0, 1.0).expect("valid parameters");
378 let expected = std::f64::consts::PI.sqrt() / 2.0;
379 assert!(
380 (ra.mtbf() - expected).abs() < 1e-10,
381 "MTBF = {}, expected sqrt(pi)/2 = {}",
382 ra.mtbf(),
383 expected
384 );
385 }
386
387 #[test]
388 fn test_time_to_reliability_median() {
389 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
391 let t_median = ra.time_to_reliability(0.5).expect("p=0.5 is valid");
392 let expected = 50.0 * (2.0_f64.ln()).powf(0.5);
393 assert!(
394 (t_median - expected).abs() < 1e-10,
395 "median life = {}, expected {}",
396 t_median,
397 expected
398 );
399 }
400
401 #[test]
402 fn test_time_to_reliability_invalid() {
403 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
404 assert!(ra.time_to_reliability(0.0).is_none());
405 assert!(ra.time_to_reliability(1.0).is_none());
406 assert!(ra.time_to_reliability(-0.1).is_none());
407 assert!(ra.time_to_reliability(1.5).is_none());
408 }
409
410 #[test]
411 fn test_time_to_reliability_roundtrip() {
412 let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
413 for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
414 let t = ra.time_to_reliability(p).expect("valid p");
415 let r = ra.reliability(t);
416 assert!(
417 (r - p).abs() < 1e-10,
418 "roundtrip: time_to_reliability({}) = {}, reliability({}) = {}",
419 p,
420 t,
421 t,
422 r
423 );
424 }
425 }
426
427 #[test]
428 fn test_b_life_b10() {
429 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
431 let b10 = ra.b_life(0.10).expect("valid fraction");
432 let t_r90 = ra.time_to_reliability(0.90).expect("valid p");
433 assert!(
434 (b10 - t_r90).abs() < 1e-10,
435 "B10 = {}, time_to_reliability(0.90) = {}",
436 b10,
437 t_r90
438 );
439 }
440
441 #[test]
442 fn test_b_life_invalid() {
443 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
444 assert!(ra.b_life(0.0).is_none());
445 assert!(ra.b_life(1.0).is_none());
446 assert!(ra.b_life(-0.1).is_none());
447 }
448
449 #[test]
450 fn test_from_mle() {
451 let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
452 let mle = weibull_mle(&data).expect("MLE should converge");
453 let ra = ReliabilityAnalysis::from_mle(&mle);
454
455 assert!((ra.shape() - mle.shape).abs() < 1e-15);
456 assert!((ra.scale() - mle.scale).abs() < 1e-15);
457 assert!(ra.mtbf() > 0.0);
458 }
459
460 #[test]
461 fn test_from_mrr() {
462 let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
463 let mrr = weibull_mrr(&data).expect("MRR should succeed");
464 let ra = ReliabilityAnalysis::from_mrr(&mrr);
465
466 assert!((ra.shape() - mrr.shape).abs() < 1e-15);
467 assert!((ra.scale() - mrr.scale).abs() < 1e-15);
468 assert!(ra.mtbf() > 0.0);
469 }
470
471 #[test]
472 fn test_b_life_ordering() {
473 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
475 let b5 = ra.b_life(0.05).expect("valid");
476 let b10 = ra.b_life(0.10).expect("valid");
477 let b50 = ra.b_life(0.50).expect("valid");
478 assert!(b5 < b10, "B5={} should < B10={}", b5, b10);
479 assert!(b10 < b50, "B10={} should < B50={}", b10, b50);
480 }
481
482 #[test]
483 fn test_hazard_pdf_reliability_relation() {
484 let ra = ReliabilityAnalysis::new(2.5, 50.0).expect("valid parameters");
487 for t in [5.0, 20.0, 50.0, 80.0] {
488 let z = t / ra.scale();
489 let pdf =
490 (ra.shape() / ra.scale()) * z.powf(ra.shape() - 1.0) * (-z.powf(ra.shape())).exp();
491 let h = ra.hazard_rate(t);
492 let r = ra.reliability(t);
493 assert!(
494 (h * r - pdf).abs() < 1e-10,
495 "h(t)*R(t) = {} should equal f(t) = {} at t={}",
496 h * r,
497 pdf,
498 t
499 );
500 }
501 }
502
503 #[test]
504 fn test_accessors() {
505 let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
506 assert!((ra.shape() - 2.5).abs() < 1e-15);
507 assert!((ra.scale() - 100.0).abs() < 1e-15);
508 }
509
510 #[test]
515 fn test_reference_reliability_beta2_eta100() {
516 let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
517
518 assert!(
520 (ra.reliability(0.0) - 1.0).abs() < 1e-15,
521 "R(0) = {}, expected 1.0",
522 ra.reliability(0.0)
523 );
524
525 let r100 = ra.reliability(100.0);
527 let expected_r100 = (-1.0_f64).exp(); assert!(
529 (r100 - expected_r100).abs() < 1e-5,
530 "R(100) = {:.6}, expected {:.6}",
531 r100,
532 expected_r100
533 );
534
535 let r50 = ra.reliability(50.0);
537 let expected_r50 = (-0.25_f64).exp(); assert!(
539 (r50 - expected_r50).abs() < 1e-5,
540 "R(50) = {:.6}, expected {:.6}",
541 r50,
542 expected_r50
543 );
544 }
545
546 #[test]
547 fn test_reference_cdf_beta2_eta100() {
548 let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
549
550 let f100 = 1.0 - ra.reliability(100.0);
552 let expected_f100 = 1.0 - (-1.0_f64).exp(); assert!(
554 (f100 - expected_f100).abs() < 1e-5,
555 "F(100) = {:.6}, expected {:.6}",
556 f100,
557 expected_f100
558 );
559 }
560
561 #[test]
562 fn test_reference_hazard_rate_beta2_eta100() {
563 let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
564
565 let h100 = ra.hazard_rate(100.0);
567 assert!(
568 (h100 - 0.02).abs() < 1e-6,
569 "h(100) = {:.8}, expected 0.02",
570 h100
571 );
572
573 let h50 = ra.hazard_rate(50.0);
575 assert!(
576 (h50 - 0.01).abs() < 1e-6,
577 "h(50) = {:.8}, expected 0.01",
578 h50
579 );
580 }
581
582 #[test]
583 fn test_reference_mttf_beta2_eta100() {
584 let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
585
586 let mttf = ra.mtbf();
588 let expected_mttf = 100.0 * std::f64::consts::PI.sqrt() / 2.0; assert!(
590 (mttf - expected_mttf).abs() < 0.001,
591 "MTTF = {:.6}, expected {:.6}",
592 mttf,
593 expected_mttf
594 );
595 }
596
597 #[test]
598 fn test_reference_b10_beta2_eta100() {
599 let ra = ReliabilityAnalysis::new(2.0, 100.0).expect("valid parameters");
600
601 let b10 = ra.b_life(0.10).expect("valid fraction");
603 let expected_b10 = 100.0 * (-0.9_f64.ln()).powf(0.5); assert!(
605 (b10 - expected_b10).abs() < 0.01,
606 "B10 = {:.5}, expected {:.5}",
607 b10,
608 expected_b10
609 );
610 }
611}