prospicio_prob/
weibull.rs1use prospicio_core::{Error, Result};
4use prospicio_math::special::{gamma_inc, ln_gamma};
5
6use crate::distribution::{Distribution, check_probability};
7use crate::severity::Severity;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct Weibull {
28 shape: f64,
29 scale: f64,
30}
31
32impl Weibull {
33 pub fn new(shape: f64, scale: f64) -> Result<Self> {
35 for (name, v) in [("shape", shape), ("scale", scale)] {
36 if !(v.is_finite() && v > 0.0) {
37 return Err(Error::InvalidParameter {
38 name,
39 value: v,
40 reason: "must be finite and positive",
41 });
42 }
43 }
44 Ok(Self { shape, scale })
45 }
46
47 pub fn shape(&self) -> f64 {
49 self.shape
50 }
51
52 pub fn scale(&self) -> f64 {
54 self.scale
55 }
56
57 fn raw_moment(&self, j: f64) -> f64 {
59 self.scale.powf(j) * ln_gamma(1.0 + j / self.shape).exp()
60 }
61
62 fn tail_moment(&self, j: i32, u: f64) -> f64 {
65 if u <= 0.0 {
66 return self.raw_moment(f64::from(j)) - u.powi(j);
67 }
68 if u == f64::INFINITY {
69 return 0.0;
70 }
71 let z = (u / self.scale).powf(self.shape);
72 let jf = f64::from(j);
73 let q = gamma_inc(1.0 + jf / self.shape, z).1;
74 (self.raw_moment(jf) * q - u.powi(j) * (-z).exp()).max(0.0)
75 }
76}
77
78impl Distribution for Weibull {
79 fn mean(&self) -> f64 {
80 self.raw_moment(1.0)
81 }
82
83 fn variance(&self) -> f64 {
84 let m = self.mean();
85 self.raw_moment(2.0) - m * m
86 }
87
88 fn cdf(&self, x: f64) -> f64 {
89 if x <= 0.0 {
90 return 0.0;
91 }
92 -(-(x / self.scale).powf(self.shape)).exp_m1()
93 }
94
95 fn survival(&self, x: f64) -> f64 {
96 if x <= 0.0 {
97 return 1.0;
98 }
99 (-(x / self.scale).powf(self.shape)).exp()
100 }
101
102 fn quantile(&self, p: f64) -> Result<f64> {
104 check_probability(p)?;
105 Ok(self.scale * (-(-p).ln_1p()).powf(1.0 / self.shape))
106 }
107}
108
109impl Severity for Weibull {
110 fn lev(&self, limit: f64) -> f64 {
112 if limit <= 0.0 {
113 return limit;
114 }
115 if limit == f64::INFINITY {
116 return self.mean();
117 }
118 let z = (limit / self.scale).powf(self.shape);
119 self.mean() * gamma_inc(1.0 + 1.0 / self.shape, z).0 + limit * (-z).exp()
120 }
121
122 fn stop_loss(&self, retention: f64) -> f64 {
124 if retention <= 0.0 {
125 return self.mean() - retention;
126 }
127 self.tail_moment(1, retention)
128 }
129
130 fn layer(&self, limit: f64, attachment: f64) -> f64 {
132 let a = attachment.max(0.0);
133 if a <= self.mean() {
134 self.lev(a + limit) - self.lev(a)
135 } else {
136 self.stop_loss(a) - self.stop_loss(a + limit)
137 }
138 }
139
140 fn layer_second_moment(&self, limit: f64, attachment: f64) -> f64 {
142 let a = attachment.max(0.0);
143 let b = a + limit;
144 let t = |j: i32, u: f64| self.tail_moment(j, u);
145 (t(2, a) - t(2, b) - 2.0 * a * (t(1, a) - t(1, b))).max(0.0)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn identities() {
155 let d = Weibull::new(0.7, 1000.0).unwrap();
156 for u in [10.0, 1000.0, 20_000.0] {
157 assert!((d.lev(u) + d.stop_loss(u) - d.mean()).abs() < 1e-10 * d.mean());
158 let x = d.quantile(d.cdf(u)).unwrap();
159 assert!((x / u - 1.0).abs() < 1e-12);
160 }
161 let m2 = d.layer_second_moment(f64::INFINITY, 0.0);
162 assert!((m2 / (d.variance() + d.mean().powi(2)) - 1.0).abs() < 1e-12);
163 let stacked = d.layer(500.0, 0.0) + d.layer(500.0, 500.0);
164 assert!((stacked - d.layer(1000.0, 0.0)).abs() < 1e-9);
165 assert!(Weibull::new(0.0, 1.0).is_err());
166 }
167}