1use std::f64::consts::{LN_2, PI};
5
6use prospicio_core::Result;
7use prospicio_math::integrate::gauss_legendre;
8use prospicio_math::special::{norm_cdf, norm_pdf};
9
10use crate::distribution::{Distribution, check_probability};
11use crate::pareto::{invalid, power_integral, raw_integral};
12use crate::piecewise_pareto::PiecewisePareto;
13use crate::severity::Severity;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct LogAffinePareto {
42 t: f64,
43 alpha0: f64,
44 gamma: f64,
45}
46
47impl LogAffinePareto {
48 pub fn new(t: f64, alpha0: f64, gamma: f64) -> Result<Self> {
51 if !t.is_finite() || t <= 0.0 {
52 return Err(invalid("t", t, "must be finite and positive"));
53 }
54 if !alpha0.is_finite() || alpha0 <= 0.0 {
55 return Err(invalid("alpha0", alpha0, "must be finite and positive"));
56 }
57 if !gamma.is_finite() || gamma < 0.0 {
58 return Err(invalid("gamma", gamma, "must be finite and non-negative"));
59 }
60 Ok(Self { t, alpha0, gamma })
61 }
62
63 pub fn from_delta(t: f64, alpha0: f64, delta: f64) -> Result<Self> {
66 if !alpha0.is_finite() || alpha0 <= 0.0 {
67 return Err(invalid("alpha0", alpha0, "must be finite and positive"));
68 }
69 Self::new(t, alpha0, delta / (alpha0 * LN_2))
70 }
71
72 pub fn t(&self) -> f64 {
73 self.t
74 }
75
76 pub fn alpha0(&self) -> f64 {
78 self.alpha0
79 }
80
81 pub fn gamma(&self) -> f64 {
82 self.gamma
83 }
84
85 pub fn delta(&self) -> f64 {
87 self.alpha0 * self.gamma * LN_2
88 }
89
90 pub fn local_alpha(&self, x: f64) -> f64 {
92 if x < self.t {
93 return 0.0;
94 }
95 self.alpha0 * (1.0 + self.gamma * (x / self.t).ln())
96 }
97
98 fn integral(&self, k: i32, a: f64, b: f64) -> f64 {
100 if a >= b {
101 return 0.0;
102 }
103 let t = self.t;
104 let below = if a < t {
105 power_integral(k, a, b.min(t))
106 } else {
107 0.0
108 };
109 let lo = a.max(t);
110 if lo >= b {
111 return below;
112 }
113 let c = self.alpha0 * self.gamma;
114 if c == 0.0 {
115 return below + raw_integral(k, t, self.alpha0, lo, b);
116 }
117 let beta = f64::from(k) + 1.0 - self.alpha0;
119 let (l1, l2) = ((lo / t).ln(), (b / t).ln());
120 below + t.powi(k + 1) * gaussian_integral(beta, c, l1, l2)
121 }
122}
123
124fn gaussian_integral(beta: f64, c: f64, l1: f64, l2: f64) -> f64 {
131 let sigma = 1.0 / c.sqrt();
132 let mu = beta / c;
133 let h = |l: f64| beta * l - 0.5 * c * l * l;
134 let tail = |l: f64| -> f64 {
136 if l.is_infinite() {
137 return 0.0;
138 }
139 sigma * h(l).exp() * mills((l - mu).abs() / sigma)
140 };
141 let (z1, z2) = ((l1 - mu) / sigma, (l2 - mu) / sigma);
142 if z1 >= 0.0 {
143 tail(l1) - tail(l2)
145 } else if z2 <= 0.0 {
146 tail(l2) - tail(l1)
148 } else {
149 sigma * (2.0 * PI).sqrt() * (0.5 * beta * mu).exp() - tail(l1) - tail(l2)
151 }
152}
153
154fn mills(z: f64) -> f64 {
157 if z < 26.0 {
158 return norm_cdf(-z) / norm_pdf(z);
159 }
160 let mut r = 0.0;
162 for n in (1..=60).rev() {
163 r = f64::from(n) / (z + r);
164 }
165 1.0 / (z + r)
166}
167
168impl Distribution for LogAffinePareto {
169 fn mean(&self) -> f64 {
170 self.integral(0, 0.0, f64::INFINITY)
171 }
172
173 fn variance(&self) -> f64 {
174 let m = self.mean();
175 if m == f64::INFINITY {
176 return f64::INFINITY;
177 }
178 2.0 * self.integral(1, 0.0, f64::INFINITY) - m * m
179 }
180
181 fn cdf(&self, x: f64) -> f64 {
182 1.0 - self.survival(x)
183 }
184
185 fn survival(&self, x: f64) -> f64 {
186 if x < self.t {
187 return 1.0;
188 }
189 let l = (x / self.t).ln();
190 (-self.alpha0 * l * (1.0 + 0.5 * self.gamma * l)).exp()
191 }
192
193 fn quantile(&self, p: f64) -> Result<f64> {
197 check_probability(p)?;
198 if p == 1.0 {
199 return Ok(f64::INFINITY);
200 }
201 let log_s = (-p).ln_1p();
202 let a = self.alpha0;
203 let l = -2.0 * log_s / (a + (a * a - 2.0 * a * self.gamma * log_s).sqrt());
204 Ok(self.t * l.exp())
205 }
206}
207
208impl Severity for LogAffinePareto {
209 fn lev(&self, limit: f64) -> f64 {
210 if limit <= 0.0 {
211 return limit;
212 }
213 self.integral(0, 0.0, limit)
214 }
215
216 fn stop_loss(&self, retention: f64) -> f64 {
217 if retention <= 0.0 {
218 return self.mean() - retention;
219 }
220 self.integral(0, retention, f64::INFINITY)
221 }
222
223 fn layer(&self, limit: f64, attachment: f64) -> f64 {
224 let a = attachment.max(0.0);
225 self.integral(0, a, a + limit)
226 }
227
228 fn layer_second_moment(&self, limit: f64, attachment: f64) -> f64 {
230 let a = attachment.max(0.0);
231 let b = a + limit;
232 2.0 * (self.integral(1, a, b) - a * self.integral(0, a, b))
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq)]
238pub struct LocalParetoConversion {
239 pub rel_tolerance: f64,
242 pub stop_survival: f64,
244 pub stop_at: f64,
246}
247
248impl Default for LocalParetoConversion {
249 fn default() -> Self {
251 Self {
252 rel_tolerance: 1e-4,
253 stop_survival: 1e-9,
254 stop_at: f64::INFINITY,
255 }
256 }
257}
258
259#[derive(Debug, Clone, PartialEq)]
263pub struct LocalParetoApproximation {
264 pub severity: PiecewisePareto,
265 pub max_relative_error: f64,
266 pub approximated_to: f64,
269}
270
271pub fn local_pareto_to_piecewise(
295 t: f64,
296 alpha: impl Fn(f64) -> f64,
297 options: LocalParetoConversion,
298) -> Result<LocalParetoApproximation> {
299 if !t.is_finite() || t <= 0.0 {
300 return Err(invalid("t", t, "must be finite and positive"));
301 }
302 let tol = options.rel_tolerance;
303 if !(tol > 0.0 && tol < 1.0) {
304 return Err(invalid("rel_tolerance", tol, "must be in (0, 1)"));
305 }
306 if !(options.stop_survival > 0.0 && options.stop_survival < 1.0) {
307 return Err(invalid(
308 "stop_survival",
309 options.stop_survival,
310 "must be in (0, 1)",
311 ));
312 }
313 if options.stop_at.is_nan() || options.stop_at <= t {
314 return Err(invalid("stop_at", options.stop_at, "must be above t"));
315 }
316 let a = |v: f64| -> Result<f64> {
317 let x = t * v.exp();
318 let al = alpha(x);
319 if !al.is_finite() || al < 0.0 {
320 return Err(invalid("alpha", al, "must be finite and non-negative"));
321 }
322 Ok(al)
323 };
324 let integrate = |lo: f64, hi: f64| -> Result<f64> { gauss_legendre(&a, lo, hi) };
325 let l_stop = (options.stop_at / t).ln();
326 let (mut thresholds, mut alphas) = (vec![t], Vec::new());
327 let (mut l0, mut a0) = (0.0f64, 0.0f64);
328 let mut h = 0.1f64;
329 let mut max_err = 0.0f64;
330 let piece = |l0: f64, h: f64| -> Result<(f64, f64)> {
332 const CHECKS: usize = 16;
333 let mut cum = Vec::with_capacity(CHECKS);
334 let mut acc = 0.0;
335 for j in 0..CHECKS {
336 let (lo, hi) = (
337 h * j as f64 / CHECKS as f64,
338 h * (j + 1) as f64 / CHECKS as f64,
339 );
340 acc += integrate(l0 + lo, l0 + hi)?;
341 cum.push(acc);
342 }
343 let rise = acc;
344 let slope = rise / h;
345 let gap = cum[..CHECKS - 1]
346 .iter()
347 .enumerate()
348 .map(|(j, &c)| (c - slope * h * (j + 1) as f64 / CHECKS as f64).abs())
349 .fold(0.0, f64::max);
350 Ok((rise, gap.exp_m1()))
351 };
352 for _ in 0..100_000 {
353 if l0 >= l_stop || -a0 < options.stop_survival.ln() {
354 break;
355 }
356 let mut ok = piece(l0, h)?;
358 if ok.1 <= tol {
359 loop {
360 let next = piece(l0, 2.0 * h)?;
361 if next.1 > tol || h > 1e3 {
362 break;
363 }
364 h *= 2.0;
365 ok = next;
366 }
367 } else {
368 while ok.1 > tol {
369 h *= 0.5;
370 if h < 1e-12 {
371 return Err(invalid(
372 "alpha",
373 t * l0.exp(),
374 "varies too fast to approximate here",
375 ));
376 }
377 ok = piece(l0, h)?;
378 }
379 }
380 if l0 + h > l_stop {
382 h = l_stop - l0;
383 ok = piece(l0, h)?;
384 }
385 alphas.push(ok.0 / h);
386 max_err = max_err.max(ok.1);
387 l0 += h;
388 a0 += ok.0;
389 thresholds.push(t * l0.exp());
390 }
391 let x_end = t * l0.exp();
392 let tail = a(l0)?;
393 if tail <= 0.0 {
394 return Err(invalid(
395 "alpha",
396 tail,
397 "must be positive where the conversion stops",
398 ));
399 }
400 alphas.push(tail);
401 Ok(LocalParetoApproximation {
402 severity: PiecewisePareto::new(thresholds, alphas)?,
403 max_relative_error: max_err,
404 approximated_to: x_end,
405 })
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use crate::Pareto;
412
413 fn close(a: f64, b: f64, rel: f64) -> bool {
414 (a - b).abs() <= rel * b.abs().max(1e-300)
415 }
416
417 #[test]
418 fn gamma_zero_is_the_pareto() {
419 let d = LogAffinePareto::new(1000.0, 2.5, 0.0).unwrap();
420 let p = Pareto::new(1000.0, 2.5).unwrap();
421 for (l, a) in [
422 (4000.0, 1000.0),
423 (1e5, 5e4),
424 (f64::INFINITY, 2000.0),
425 (500.0, 0.0),
426 ] {
427 assert!(close(d.layer(l, a), p.layer(l, a), 1e-14));
428 }
429 assert!(close(d.variance(), p.variance(), 1e-13));
430 assert!(close(
431 d.quantile(0.9).unwrap(),
432 p.quantile(0.9).unwrap(),
433 1e-14
434 ));
435 }
436
437 #[test]
438 fn layers_match_quadrature() {
439 let simpson = |d: &LogAffinePareto, k: i32, a: f64, b: f64| -> f64 {
441 let (l1, l2) = ((a / d.t).ln(), (b / d.t).ln());
442 let n = 20_000;
443 let h = (l2 - l1) / f64::from(n);
444 let f = |l: f64| {
445 let x = d.t * l.exp();
446 x.powi(k) * d.survival(x) * x
447 };
448 let mut s = f(l1) + f(l2);
449 for i in 1..n {
450 s += f(l1 + h * f64::from(i)) * if i % 2 == 1 { 4.0 } else { 2.0 };
451 }
452 s * h / 3.0
453 };
454 for (alpha0, gamma) in [(0.5, 0.3), (1.0, 1.0), (2.0, 0.05), (0.2, 2.0), (3.0, 0.5)] {
455 let d = LogAffinePareto::new(1000.0, alpha0, gamma).unwrap();
456 for (a, b) in [(1000.0, 2000.0), (1500.0, 9000.0), (5e4, 2e5), (2e3, 2.1e3)] {
457 let want = simpson(&d, 0, a, b);
458 assert!(
459 close(d.layer(b - a, a), want, 1e-10),
460 "{alpha0} {gamma} {a} {b}"
461 );
462 let m2 = 2.0 * (simpson(&d, 1, a, b) - a * want);
463 assert!(
464 close(d.layer_second_moment(b - a, a), m2, 1e-9),
465 "{alpha0} {gamma} {a} {b}"
466 );
467 }
468 }
469 }
470
471 #[test]
472 fn identities_and_round_trip() {
473 let d = LogAffinePareto::from_delta(1000.0, 0.8, 0.6).unwrap();
474 assert!(close(d.delta(), 0.6, 1e-15));
475 for x in [500.0, 2000.0, 1e5] {
476 assert!(close(d.lev(x) + d.stop_loss(x), d.mean(), 1e-12));
477 }
478 let m2 = d.layer_second_moment(f64::INFINITY, 0.0);
479 assert!(close(m2 - d.mean() * d.mean(), d.variance(), 1e-12));
480 for p in [0.0, 0.3, 0.9, 0.999_999] {
481 let x = d.quantile(p).unwrap();
482 assert!(close(d.cdf(x), p, 1e-12) || p == 0.0);
483 }
484 assert!(
486 LogAffinePareto::new(1.0, 0.3, 0.1)
487 .unwrap()
488 .variance()
489 .is_finite()
490 );
491 let far = d.layer(1e8, 1e8);
494 assert!(far > 0.0 && far < 1e8 * d.survival(1e8), "{far}");
495 }
496
497 #[test]
498 fn mills_ratio_is_continuous() {
499 let below = mills(26.0 - 1e-12);
501 let above = mills(26.0);
502 assert!(close(below, above, 1e-12), "{below} {above}");
503 assert!(close(mills(0.0), (PI / 2.0).sqrt(), 1e-15));
504 }
505
506 #[test]
507 fn rejects_bad_parameters() {
508 assert!(LogAffinePareto::new(0.0, 1.0, 0.0).is_err());
509 assert!(LogAffinePareto::new(1.0, 0.0, 0.0).is_err());
510 assert!(LogAffinePareto::new(1.0, 1.0, -0.1).is_err());
511 assert!(LogAffinePareto::from_delta(1.0, 1.0, -0.1).is_err());
512 }
513
514 #[test]
515 fn conversion_reproduces_the_log_affine_survival() {
516 let exact = LogAffinePareto::new(1000.0, 1.2, 0.6).unwrap();
517 for tol in [1e-3, 1e-5] {
518 let opts = LocalParetoConversion {
519 rel_tolerance: tol,
520 ..LocalParetoConversion::default()
521 };
522 let approx = local_pareto_to_piecewise(1000.0, |x| exact.local_alpha(x), opts).unwrap();
523 assert!(approx.max_relative_error <= tol);
524 let end = approx.approximated_to;
525 assert!(exact.survival(end) < 1.0001e-9);
526 let mut x = 1000.0;
527 while x < end {
528 let r = approx.severity.survival(x) / exact.survival(x) - 1.0;
529 assert!(r.abs() <= 1.01 * tol, "{tol} {x} {r}");
530 x *= 1.07;
531 }
532 for &th in approx.severity.thresholds() {
534 if th < end {
535 let r = approx.severity.survival(th) / exact.survival(th) - 1.0;
536 assert!(r.abs() < 1e-12, "{th} {r}");
537 }
538 }
539 }
540 }
541
542 #[test]
543 fn conversion_of_constant_and_wavy_alphas() {
544 let approx = local_pareto_to_piecewise(
546 10.0,
547 |_| 2.5,
548 LocalParetoConversion {
549 stop_at: 1e4,
550 ..LocalParetoConversion::default()
551 },
552 )
553 .unwrap();
554 assert!(
555 approx
556 .severity
557 .alphas()
558 .iter()
559 .all(|&a| (a - 2.5).abs() < 1e-12)
560 );
561 assert!(approx.max_relative_error < 1e-12);
562 let alpha = |x: f64| 1.5 + 0.8 * (x / 100.0).ln().sin();
564 let opts = LocalParetoConversion {
565 rel_tolerance: 1e-6,
566 stop_survival: 1e-6,
567 ..LocalParetoConversion::default()
568 };
569 let approx = local_pareto_to_piecewise(100.0, alpha, opts).unwrap();
570 for x in [150.0, 1000.0, 1e4, 3e4] {
571 if x > approx.approximated_to {
572 continue;
573 }
574 let l = (x / 100.0f64).ln();
576 let want = (-(1.5 * l + 0.8 * (1.0 - l.cos()))).exp();
577 let r = approx.severity.survival(x) / want - 1.0;
578 assert!(r.abs() <= 1.01e-6, "{x} {r}");
579 }
580 assert!(
581 local_pareto_to_piecewise(1.0, |_| -1.0, LocalParetoConversion::default()).is_err()
582 );
583 assert!(local_pareto_to_piecewise(0.0, |_| 1.0, LocalParetoConversion::default()).is_err());
584 }
585}