prospicio_prob/
lognormal.rs1use prospicio_core::{Error, Result};
4use prospicio_math::special::{norm_cdf, norm_quantile};
5
6use crate::distribution::{Distribution, check_probability};
7use crate::severity::Severity;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct Lognormal {
25 meanlog: f64,
26 sdlog: f64,
27}
28
29impl Lognormal {
30 pub fn new(meanlog: f64, sdlog: f64) -> Result<Self> {
32 if !meanlog.is_finite() {
33 return Err(Error::InvalidParameter {
34 name: "meanlog",
35 value: meanlog,
36 reason: "must be finite",
37 });
38 }
39 if !sdlog.is_finite() || sdlog <= 0.0 {
40 return Err(Error::InvalidParameter {
41 name: "sdlog",
42 value: sdlog,
43 reason: "must be finite and positive",
44 });
45 }
46 Ok(Self { meanlog, sdlog })
47 }
48
49 pub fn from_mean_cv(mean: f64, cv: f64) -> Result<Self> {
52 if !mean.is_finite() || mean <= 0.0 {
53 return Err(Error::InvalidParameter {
54 name: "mean",
55 value: mean,
56 reason: "must be finite and positive",
57 });
58 }
59 if !cv.is_finite() || cv <= 0.0 {
60 return Err(Error::InvalidParameter {
61 name: "cv",
62 value: cv,
63 reason: "must be finite and positive",
64 });
65 }
66 let sigma2 = cv.mul_add(cv, 1.0).ln();
67 Self::new(mean.ln() - 0.5 * sigma2, sigma2.sqrt())
68 }
69
70 pub fn meanlog(&self) -> f64 {
72 self.meanlog
73 }
74
75 pub fn sdlog(&self) -> f64 {
77 self.sdlog
78 }
79}
80
81impl Distribution for Lognormal {
82 fn mean(&self) -> f64 {
83 (self.meanlog + 0.5 * self.sdlog * self.sdlog).exp()
84 }
85
86 fn variance(&self) -> f64 {
87 let s2 = self.sdlog * self.sdlog;
88 s2.exp_m1() * (2.0 * self.meanlog + s2).exp()
89 }
90
91 fn cdf(&self, x: f64) -> f64 {
92 if x <= 0.0 {
93 return 0.0;
94 }
95 norm_cdf((x.ln() - self.meanlog) / self.sdlog)
96 }
97
98 fn survival(&self, x: f64) -> f64 {
99 if x <= 0.0 {
100 return 1.0;
101 }
102 norm_cdf((self.meanlog - x.ln()) / self.sdlog)
103 }
104
105 fn quantile(&self, p: f64) -> Result<f64> {
106 check_probability(p)?;
107 Ok((self.meanlog + self.sdlog * norm_quantile(p)).exp())
108 }
109}
110
111impl Severity for Lognormal {
112 fn lev(&self, limit: f64) -> f64 {
114 if limit <= 0.0 {
115 return limit;
116 }
117 if limit == f64::INFINITY {
118 return self.mean();
119 }
120 let (mu, s) = (self.meanlog, self.sdlog);
121 let z = (limit.ln() - mu) / s;
122 self.mean() * norm_cdf(z - s) + limit * norm_cdf(-z)
123 }
124
125 fn stop_loss(&self, retention: f64) -> f64 {
129 if retention <= 0.0 {
130 return self.mean() - retention;
131 }
132 if retention == f64::INFINITY {
133 return 0.0;
134 }
135 let (mu, s) = (self.meanlog, self.sdlog);
136 let z = (retention.ln() - mu) / s;
137 (self.mean() * norm_cdf(s - z) - retention * norm_cdf(-z)).max(0.0)
138 }
139
140 fn layer_second_moment(&self, limit: f64, attachment: f64) -> f64 {
146 let (mu, s) = (self.meanlog, self.sdlog);
147 let a = attachment.max(0.0);
148 let b = a + limit;
149 let z = |x: f64| {
150 if x <= 0.0 {
151 f64::NEG_INFINITY
152 } else {
153 (x.ln() - mu) / s
154 }
155 };
156 let (za, zb) = (z(a), z(b));
157 let band = |lo: f64, hi: f64| {
159 if lo > 0.0 {
160 norm_cdf(-lo) - norm_cdf(-hi)
161 } else {
162 norm_cdf(hi) - norm_cdf(lo)
163 }
164 };
165 let m0 = band(za, zb);
166 let m1 = self.mean() * band(za - s, zb - s);
167 let m2 = (2.0 * mu + 2.0 * s * s).exp() * band(za - 2.0 * s, zb - 2.0 * s);
168 let inside = m2 - 2.0 * a * m1 + a * a * m0;
169 let above = if b == f64::INFINITY {
170 0.0
171 } else {
172 limit * limit * norm_cdf(-zb)
173 };
174 (inside + above).max(0.0)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use prospicio_core::StreamRng;
182
183 #[test]
184 fn severity_identities_and_edges() {
185 let d = Lognormal::new(7.0, 0.5).unwrap();
186 for limit in [100.0, 1_000.0, 5_000.0] {
187 let sum = d.lev(limit) + d.stop_loss(limit);
188 assert!((sum - d.mean()).abs() < 1e-12 * d.mean());
189 }
190 assert_eq!(d.lev(0.0), 0.0);
191 assert_eq!(d.lev(-5.0), -5.0);
192 assert_eq!(d.lev(f64::INFINITY), d.mean());
193 assert_eq!(d.stop_loss(f64::INFINITY), 0.0);
194 assert_eq!(d.stop_loss(0.0), d.mean());
195 assert_eq!(d.layer(f64::INFINITY, 0.0), d.mean());
196 let stacked = d.layer(1_000.0, 0.0) + d.layer(1_000.0, 1_000.0);
198 assert!((stacked - d.layer(2_000.0, 0.0)).abs() < 1e-9);
199 }
200
201 #[test]
202 fn stop_loss_keeps_precision_in_the_tail() {
203 let d = Lognormal::new(0.0, 1.0).unwrap();
205 let sl = d.stop_loss(1135.0);
206 let exact = 1.796211496502316e-10;
208 assert!((sl / exact - 1.0).abs() < 1e-12, "{sl}");
209 let naive = d.mean() - d.lev(1135.0);
212 assert!((naive / exact - 1.0).abs() > 1e-8);
213 }
214
215 #[test]
216 fn rejects_bad_parameters() {
217 assert!(Lognormal::new(0.0, 0.0).is_err());
218 assert!(Lognormal::new(f64::NAN, 1.0).is_err());
219 assert!(Lognormal::from_mean_cv(-1.0, 0.5).is_err());
220 }
221
222 #[test]
223 fn quantile_edges() {
224 let d = Lognormal::new(0.0, 1.0).unwrap();
225 assert_eq!(d.quantile(0.0), Ok(0.0));
226 assert_eq!(d.quantile(1.0), Ok(f64::INFINITY));
227 assert_eq!(d.quantile(1.1), Err(Error::InvalidProbability(1.1)));
228 assert_eq!(d.cdf(-1.0), 0.0);
229 }
230
231 const PINNED_SAMPLE: [f64; 3] = [1.0007760893701914, 1.6293872534754683, 1.0763869265482304];
234
235 #[test]
236 fn sample_is_pinned() {
237 let d = Lognormal::new(0.0, 1.0).unwrap();
238 assert_eq!(d.sample(&mut StreamRng::new(42, 3), 3), PINNED_SAMPLE);
239 }
240
241 #[test]
242 fn sample_is_reproducible_and_centred() {
243 let d = Lognormal::from_mean_cv(100.0, 0.3).unwrap();
244 let a = d.sample(&mut StreamRng::new(1, 0), 200_000);
245 let b = d.sample(&mut StreamRng::new(1, 0), 200_000);
246 assert_eq!(a, b);
247 let mean = a.iter().sum::<f64>() / a.len() as f64;
248 assert!((mean - 100.0).abs() < 0.35, "mean {mean}");
250 }
251}