u_analytics/weibull/reliability.rs
1//! Reliability analysis from fitted Weibull parameters.
2//!
3//! Provides reliability function, hazard rate, MTBF, B-life, and other
4//! common reliability engineering metrics.
5
6use u_numflow::special::gamma;
7
8/// Reliability analysis results from a fitted Weibull distribution.
9///
10/// Computes reliability engineering metrics (reliability function, hazard
11/// rate, MTBF, B-life) from Weibull shape (beta) and scale (eta) parameters.
12///
13/// # Mathematical Background
14///
15/// Given a Weibull distribution with shape beta > 0 and scale eta > 0:
16/// - Reliability: R(t) = exp(-(t/eta)^beta)
17/// - Hazard rate: lambda(t) = (beta/eta) * (t/eta)^(beta-1)
18/// - MTBF: eta * Gamma(1 + 1/beta)
19///
20/// # Examples
21///
22/// ```
23/// use u_analytics::weibull::ReliabilityAnalysis;
24/// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
25/// assert!((ra.reliability(0.0) - 1.0).abs() < 1e-10);
26/// assert!(ra.hazard_rate(50.0) > 0.0);
27/// assert!(ra.mtbf() > 0.0);
28/// let b10 = ra.b_life(0.10).unwrap();
29/// assert!(b10 > 0.0 && b10 < 100.0);
30/// ```
31///
32/// # Reference
33/// Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, Wiley.
34#[derive(Debug, Clone)]
35pub struct ReliabilityAnalysis {
36 /// Shape parameter (beta).
37 shape: f64,
38 /// Scale parameter (eta).
39 scale: f64,
40}
41
42impl ReliabilityAnalysis {
43 /// Creates a new reliability analysis from Weibull parameters.
44 ///
45 /// # Arguments
46 /// * `shape` - Shape parameter beta (must be positive and finite)
47 /// * `scale` - Scale parameter eta (must be positive and finite)
48 ///
49 /// # Returns
50 /// `None` if either parameter is non-positive or non-finite.
51 ///
52 /// # Examples
53 ///
54 /// ```
55 /// use u_analytics::weibull::ReliabilityAnalysis;
56 /// let ra = ReliabilityAnalysis::new(2.0, 100.0);
57 /// assert!(ra.is_some());
58 ///
59 /// // Invalid parameters return None
60 /// assert!(ReliabilityAnalysis::new(-1.0, 100.0).is_none());
61 /// assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
62 /// ```
63 pub fn new(shape: f64, scale: f64) -> Option<Self> {
64 if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
65 return None;
66 }
67 Some(Self { shape, scale })
68 }
69
70 /// Creates a reliability analysis from an MLE fitting result.
71 ///
72 /// # Panics
73 /// This function does not panic. MLE results always have valid parameters.
74 pub fn from_mle(result: &super::mle::WeibullMleResult) -> Self {
75 Self {
76 shape: result.shape,
77 scale: result.scale,
78 }
79 }
80
81 /// Creates a reliability analysis from an MRR fitting result.
82 ///
83 /// # Panics
84 /// This function does not panic. MRR results always have valid parameters.
85 pub fn from_mrr(result: &super::mrr::WeibullMrrResult) -> Self {
86 Self {
87 shape: result.shape,
88 scale: result.scale,
89 }
90 }
91
92 /// Returns the shape parameter (beta).
93 pub fn shape(&self) -> f64 {
94 self.shape
95 }
96
97 /// Returns the scale parameter (eta).
98 pub fn scale(&self) -> f64 {
99 self.scale
100 }
101
102 /// Reliability (survival) function at time t.
103 ///
104 /// ```text
105 /// R(t) = exp(-(t/eta)^beta)
106 /// ```
107 ///
108 /// For t < 0, returns 1.0 (no failure before time 0).
109 ///
110 /// # Examples
111 ///
112 /// ```
113 /// use u_analytics::weibull::ReliabilityAnalysis;
114 /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
115 ///
116 /// // R(0) = 1.0 (full reliability at time zero)
117 /// assert!((ra.reliability(0.0) - 1.0).abs() < 1e-10);
118 ///
119 /// // R(eta) = exp(-1) ≈ 0.368 for any shape parameter
120 /// let expected = (-1.0_f64).exp();
121 /// assert!((ra.reliability(100.0) - expected).abs() < 1e-10);
122 /// ```
123 ///
124 /// # Reference
125 /// Weibull (1951), *Journal of Applied Mechanics* 18(3), pp. 293-297.
126 pub fn reliability(&self, t: f64) -> f64 {
127 if t <= 0.0 {
128 return 1.0;
129 }
130 let z = t / self.scale;
131 (-z.powf(self.shape)).exp()
132 }
133
134 /// Failure rate (hazard function) at time t.
135 ///
136 /// ```text
137 /// lambda(t) = (beta/eta) * (t/eta)^(beta-1)
138 /// ```
139 ///
140 /// - beta < 1: Decreasing failure rate (infant mortality)
141 /// - beta = 1: Constant failure rate (random/exponential failures)
142 /// - beta > 1: Increasing failure rate (wear-out)
143 ///
144 /// For t <= 0, returns 0.0.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// use u_analytics::weibull::ReliabilityAnalysis;
150 /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
151 ///
152 /// // Hazard rate is positive for t > 0
153 /// assert!(ra.hazard_rate(50.0) > 0.0);
154 ///
155 /// // With beta > 1, hazard rate increases over time (wear-out)
156 /// assert!(ra.hazard_rate(50.0) < ra.hazard_rate(80.0));
157 /// ```
158 ///
159 /// # Reference
160 /// Meeker & Escobar (1998), *Statistical Methods for Reliability Data*, Ch. 4.
161 pub fn hazard_rate(&self, t: f64) -> f64 {
162 if t <= 0.0 {
163 return 0.0;
164 }
165 let z = t / self.scale;
166 (self.shape / self.scale) * z.powf(self.shape - 1.0)
167 }
168
169 /// Mean Time Between Failures (MTBF).
170 ///
171 /// ```text
172 /// MTBF = eta * Gamma(1 + 1/beta)
173 /// ```
174 ///
175 /// # Examples
176 ///
177 /// ```
178 /// use u_analytics::weibull::ReliabilityAnalysis;
179 /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
180 /// let mtbf = ra.mtbf();
181 /// assert!(mtbf > 0.0);
182 ///
183 /// // For beta=1 (exponential), MTBF = eta
184 /// let exp_ra = ReliabilityAnalysis::new(1.0, 50.0).unwrap();
185 /// assert!((exp_ra.mtbf() - 50.0).abs() < 1e-8);
186 /// ```
187 ///
188 /// # Reference
189 /// Johnson, Kotz & Balakrishnan (1994), *Continuous Univariate Distributions*,
190 /// Vol. 1, Chapter 21.
191 pub fn mtbf(&self) -> f64 {
192 self.scale * gamma(1.0 + 1.0 / self.shape)
193 }
194
195 /// Time at which reliability drops to a given level.
196 ///
197 /// Solves R(t) = p for t:
198 ///
199 /// ```text
200 /// t = eta * (-ln(p))^(1/beta)
201 /// ```
202 ///
203 /// # Arguments
204 /// * `p` - Desired reliability level, must be in (0, 1).
205 ///
206 /// # Returns
207 /// `None` if `p` is outside (0, 1).
208 ///
209 /// # Reference
210 /// Abernethy (2006), *The New Weibull Handbook*, 5th ed.
211 pub fn time_to_reliability(&self, p: f64) -> Option<f64> {
212 if p <= 0.0 || p >= 1.0 {
213 return None;
214 }
215 Some(self.scale * (-p.ln()).powf(1.0 / self.shape))
216 }
217
218 /// B-life: time at which a given fraction of the population has failed.
219 ///
220 /// B10 life (10% failed) = `b_life(0.10)`, which is equivalent to
221 /// `time_to_reliability(0.90)`.
222 ///
223 /// # Arguments
224 /// * `fraction_failed` - Fraction of population that has failed, must be in (0, 1).
225 ///
226 /// # Returns
227 /// `None` if `fraction_failed` is outside (0, 1).
228 ///
229 /// # Examples
230 ///
231 /// ```
232 /// use u_analytics::weibull::ReliabilityAnalysis;
233 /// let ra = ReliabilityAnalysis::new(2.0, 100.0).unwrap();
234 ///
235 /// // B10: time when 10% of the population has failed
236 /// let b10 = ra.b_life(0.10).unwrap();
237 /// assert!(b10 > 0.0 && b10 < 100.0);
238 ///
239 /// // B-lives increase with failure fraction: B5 < B10 < B50
240 /// let b5 = ra.b_life(0.05).unwrap();
241 /// let b50 = ra.b_life(0.50).unwrap();
242 /// assert!(b5 < b10 && b10 < b50);
243 /// ```
244 ///
245 /// # Reference
246 /// Abernethy (2006), *The New Weibull Handbook*, 5th ed., Chapter 2.
247 pub fn b_life(&self, fraction_failed: f64) -> Option<f64> {
248 if fraction_failed <= 0.0 || fraction_failed >= 1.0 {
249 return None;
250 }
251 self.time_to_reliability(1.0 - fraction_failed)
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::weibull::{weibull_mle, weibull_mrr};
259
260 #[test]
261 fn test_new_valid() {
262 assert!(ReliabilityAnalysis::new(2.0, 50.0).is_some());
263 }
264
265 #[test]
266 fn test_new_invalid() {
267 assert!(ReliabilityAnalysis::new(0.0, 50.0).is_none());
268 assert!(ReliabilityAnalysis::new(-1.0, 50.0).is_none());
269 assert!(ReliabilityAnalysis::new(2.0, 0.0).is_none());
270 assert!(ReliabilityAnalysis::new(2.0, -1.0).is_none());
271 assert!(ReliabilityAnalysis::new(f64::NAN, 50.0).is_none());
272 assert!(ReliabilityAnalysis::new(2.0, f64::INFINITY).is_none());
273 }
274
275 #[test]
276 fn test_reliability_at_zero() {
277 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
278 assert!((ra.reliability(0.0) - 1.0).abs() < 1e-15);
279 }
280
281 #[test]
282 fn test_reliability_decreasing() {
283 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
284 let mut prev = 1.0;
285 for i in 1..=100 {
286 let t = i as f64;
287 let r = ra.reliability(t);
288 assert!(
289 r <= prev + 1e-15,
290 "reliability should be non-increasing: R({}) = {} > R({}) = {}",
291 t,
292 r,
293 t - 1.0,
294 prev
295 );
296 prev = r;
297 }
298 }
299
300 #[test]
301 fn test_reliability_at_scale() {
302 // R(eta) = exp(-1) for any beta
303 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
304 let r_at_scale = ra.reliability(50.0);
305 let expected = (-1.0_f64).exp();
306 assert!(
307 (r_at_scale - expected).abs() < 1e-10,
308 "R(eta) = {}, expected exp(-1) = {}",
309 r_at_scale,
310 expected
311 );
312 }
313
314 #[test]
315 fn test_reliability_negative_time() {
316 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
317 assert!((ra.reliability(-10.0) - 1.0).abs() < 1e-15);
318 }
319
320 #[test]
321 fn test_hazard_rate_exponential() {
322 // beta=1 => constant hazard rate = 1/eta
323 let ra = ReliabilityAnalysis::new(1.0, 20.0).expect("valid parameters");
324 for t in [5.0, 10.0, 20.0, 50.0] {
325 assert!(
326 (ra.hazard_rate(t) - 1.0 / 20.0).abs() < 1e-10,
327 "hazard at t={} should be 1/eta=0.05",
328 t
329 );
330 }
331 }
332
333 #[test]
334 fn test_hazard_rate_increasing() {
335 // beta > 1 => increasing hazard rate (wear-out)
336 let ra = ReliabilityAnalysis::new(3.0, 50.0).expect("valid parameters");
337 let h1 = ra.hazard_rate(10.0);
338 let h2 = ra.hazard_rate(20.0);
339 let h3 = ra.hazard_rate(30.0);
340 assert!(
341 h1 < h2,
342 "hazard should increase: h(10)={} >= h(20)={}",
343 h1,
344 h2
345 );
346 assert!(
347 h2 < h3,
348 "hazard should increase: h(20)={} >= h(30)={}",
349 h2,
350 h3
351 );
352 }
353
354 #[test]
355 fn test_hazard_rate_zero_time() {
356 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
357 assert!((ra.hazard_rate(0.0)).abs() < 1e-15);
358 assert!((ra.hazard_rate(-5.0)).abs() < 1e-15);
359 }
360
361 #[test]
362 fn test_mtbf_exponential() {
363 // beta=1 => MTBF = eta * Gamma(2) = eta * 1 = eta
364 let ra = ReliabilityAnalysis::new(1.0, 100.0).expect("valid parameters");
365 assert!(
366 (ra.mtbf() - 100.0).abs() < 1e-8,
367 "MTBF = {}, expected 100.0 for exponential",
368 ra.mtbf()
369 );
370 }
371
372 #[test]
373 fn test_mtbf_rayleigh() {
374 // beta=2, eta=1 => MTBF = Gamma(1.5) = sqrt(pi)/2
375 let ra = ReliabilityAnalysis::new(2.0, 1.0).expect("valid parameters");
376 let expected = std::f64::consts::PI.sqrt() / 2.0;
377 assert!(
378 (ra.mtbf() - expected).abs() < 1e-10,
379 "MTBF = {}, expected sqrt(pi)/2 = {}",
380 ra.mtbf(),
381 expected
382 );
383 }
384
385 #[test]
386 fn test_time_to_reliability_median() {
387 // Median life: R(t) = 0.5 => t = eta * (ln(2))^(1/beta)
388 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
389 let t_median = ra.time_to_reliability(0.5).expect("p=0.5 is valid");
390 let expected = 50.0 * (2.0_f64.ln()).powf(0.5);
391 assert!(
392 (t_median - expected).abs() < 1e-10,
393 "median life = {}, expected {}",
394 t_median,
395 expected
396 );
397 }
398
399 #[test]
400 fn test_time_to_reliability_invalid() {
401 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
402 assert!(ra.time_to_reliability(0.0).is_none());
403 assert!(ra.time_to_reliability(1.0).is_none());
404 assert!(ra.time_to_reliability(-0.1).is_none());
405 assert!(ra.time_to_reliability(1.5).is_none());
406 }
407
408 #[test]
409 fn test_time_to_reliability_roundtrip() {
410 let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
411 for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
412 let t = ra.time_to_reliability(p).expect("valid p");
413 let r = ra.reliability(t);
414 assert!(
415 (r - p).abs() < 1e-10,
416 "roundtrip: time_to_reliability({}) = {}, reliability({}) = {}",
417 p,
418 t,
419 t,
420 r
421 );
422 }
423 }
424
425 #[test]
426 fn test_b_life_b10() {
427 // B10 = time when 10% have failed = time_to_reliability(0.90)
428 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
429 let b10 = ra.b_life(0.10).expect("valid fraction");
430 let t_r90 = ra.time_to_reliability(0.90).expect("valid p");
431 assert!(
432 (b10 - t_r90).abs() < 1e-10,
433 "B10 = {}, time_to_reliability(0.90) = {}",
434 b10,
435 t_r90
436 );
437 }
438
439 #[test]
440 fn test_b_life_invalid() {
441 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
442 assert!(ra.b_life(0.0).is_none());
443 assert!(ra.b_life(1.0).is_none());
444 assert!(ra.b_life(-0.1).is_none());
445 }
446
447 #[test]
448 fn test_from_mle() {
449 let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
450 let mle = weibull_mle(&data).expect("MLE should converge");
451 let ra = ReliabilityAnalysis::from_mle(&mle);
452
453 assert!((ra.shape() - mle.shape).abs() < 1e-15);
454 assert!((ra.scale() - mle.scale).abs() < 1e-15);
455 assert!(ra.mtbf() > 0.0);
456 }
457
458 #[test]
459 fn test_from_mrr() {
460 let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
461 let mrr = weibull_mrr(&data).expect("MRR should succeed");
462 let ra = ReliabilityAnalysis::from_mrr(&mrr);
463
464 assert!((ra.shape() - mrr.shape).abs() < 1e-15);
465 assert!((ra.scale() - mrr.scale).abs() < 1e-15);
466 assert!(ra.mtbf() > 0.0);
467 }
468
469 #[test]
470 fn test_b_life_ordering() {
471 // B5 < B10 < B50 (more failures => more time)
472 let ra = ReliabilityAnalysis::new(2.0, 50.0).expect("valid parameters");
473 let b5 = ra.b_life(0.05).expect("valid");
474 let b10 = ra.b_life(0.10).expect("valid");
475 let b50 = ra.b_life(0.50).expect("valid");
476 assert!(b5 < b10, "B5={} should < B10={}", b5, b10);
477 assert!(b10 < b50, "B10={} should < B50={}", b10, b50);
478 }
479
480 #[test]
481 fn test_hazard_pdf_reliability_relation() {
482 // h(t) = f(t) / R(t) where f(t) is the Weibull PDF
483 // f(t) = (beta/eta) * (t/eta)^(beta-1) * exp(-(t/eta)^beta)
484 let ra = ReliabilityAnalysis::new(2.5, 50.0).expect("valid parameters");
485 for t in [5.0, 20.0, 50.0, 80.0] {
486 let z = t / ra.scale();
487 let pdf =
488 (ra.shape() / ra.scale()) * z.powf(ra.shape() - 1.0) * (-z.powf(ra.shape())).exp();
489 let h = ra.hazard_rate(t);
490 let r = ra.reliability(t);
491 assert!(
492 (h * r - pdf).abs() < 1e-10,
493 "h(t)*R(t) = {} should equal f(t) = {} at t={}",
494 h * r,
495 pdf,
496 t
497 );
498 }
499 }
500
501 #[test]
502 fn test_accessors() {
503 let ra = ReliabilityAnalysis::new(2.5, 100.0).expect("valid parameters");
504 assert!((ra.shape() - 2.5).abs() < 1e-15);
505 assert!((ra.scale() - 100.0).abs() < 1e-15);
506 }
507}