1use prospicio_core::{Error, Result};
10use prospicio_math::roots::bisect;
11
12use crate::distribution::{Distribution, check_probability};
13use crate::severity::Severity;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct Gpd {
35 xi: f64,
36 beta: f64,
37 location: f64,
38}
39
40impl Gpd {
41 pub fn new(xi: f64, beta: f64) -> Result<Self> {
42 if !xi.is_finite() {
43 return Err(invalid("xi", xi, "must be finite"));
44 }
45 if !beta.is_finite() || beta <= 0.0 {
46 return Err(invalid("beta", beta, "must be finite and positive"));
47 }
48 Ok(Self {
49 xi,
50 beta,
51 location: 0.0,
52 })
53 }
54
55 pub fn shifted(self, location: f64) -> Result<Self> {
57 if !location.is_finite() {
58 return Err(invalid("location", location, "must be finite"));
59 }
60 Ok(Self { location, ..self })
61 }
62
63 pub fn riegel(t: f64, alpha_ini: f64, alpha_tail: f64) -> Result<Self> {
84 if !t.is_finite() || t <= 0.0 {
85 return Err(invalid("t", t, "must be finite and positive"));
86 }
87 if !alpha_ini.is_finite() || alpha_ini <= 0.0 {
88 return Err(invalid(
89 "alpha_ini",
90 alpha_ini,
91 "must be finite and positive",
92 ));
93 }
94 if !alpha_tail.is_finite() || alpha_tail <= 0.0 {
95 return Err(invalid(
96 "alpha_tail",
97 alpha_tail,
98 "must be finite and positive",
99 ));
100 }
101 Self::new(1.0 / alpha_tail, t / alpha_ini)?.shifted(t)
102 }
103
104 pub fn xi(&self) -> f64 {
107 self.xi
108 }
109
110 pub fn beta(&self) -> f64 {
112 self.beta
113 }
114
115 pub fn location(&self) -> f64 {
117 self.location
118 }
119
120 fn survival_at(&self, x: f64) -> f64 {
122 self.excess_survival(x - self.location)
123 }
124
125 fn excess_survival(&self, x: f64) -> f64 {
127 if x <= 0.0 {
128 return 1.0;
129 }
130 let z = self.xi * x / self.beta;
131 if self.xi == 0.0 {
132 return (-x / self.beta).exp();
133 }
134 if z <= -1.0 {
135 return 0.0;
136 }
137 (-z.ln_1p() / self.xi).exp()
138 }
139
140 pub fn fit(exceedances: &[f64]) -> Result<Self> {
161 let y = exceedances;
162 let n = y.len();
163 if n < 3 {
164 return Err(invalid("exceedances", n as f64, "needs at least 3 values"));
165 }
166 if let Some(&bad) = y.iter().find(|v| !v.is_finite() || **v < 0.0) {
167 return Err(invalid(
168 "exceedances",
169 bad,
170 "must be finite and non-negative",
171 ));
172 }
173 let max = y.iter().copied().fold(0.0, f64::max);
174 let mean = y.iter().sum::<f64>() / n as f64;
175 if max == 0.0 || y.iter().all(|&v| v == y[0]) {
176 return Err(invalid("exceedances", max, "must not all be equal"));
177 }
178
179 let profile = |theta: f64| -> f64 {
182 if theta == 0.0 {
183 return -mean.ln() - 1.0;
184 }
185 let xi = y.iter().map(|&v| (theta * v).ln_1p()).sum::<f64>() / n as f64;
186 if !xi.is_finite() || xi <= -1.0 || xi == 0.0 {
187 return f64::NEG_INFINITY;
188 }
189 let beta = xi / theta;
190 if beta <= 0.0 {
191 return f64::NEG_INFINITY;
192 }
193 -beta.ln() - (1.0 + xi)
194 };
195
196 let decade = |j: i32| 10f64.powf(f64::from(j) / 20.0);
201 let mut thetas: Vec<f64> = Vec::with_capacity(800);
202 thetas.extend((1..=240).rev().map(|j| -(1.0 - decade(-j)) / max));
203 thetas.extend((-160..=-1).rev().map(|j| -decade(j) / max));
204 thetas.extend((-160..=240).map(|j| decade(j) / max));
205 let (mut best_k, mut best) = (0, f64::NEG_INFINITY);
206 for (k, &theta) in thetas.iter().enumerate() {
207 let v = profile(theta);
208 if v > best {
209 best = v;
210 best_k = k;
211 }
212 }
213 if best <= profile(0.0) {
214 return Self::new(0.0, mean);
215 }
216
217 let score = |theta: f64| -> f64 {
222 let (mut xi, mut dxi) = (0.0, 0.0);
223 for &v in y {
224 xi += (theta * v).ln_1p();
225 dxi += v / (1.0 + theta * v);
226 }
227 let (xi, dxi) = (xi / n as f64, dxi / n as f64);
228 -dxi / xi + 1.0 / theta - dxi
229 };
230 let theta_at = |k: usize| thetas[k];
231 let mut a = theta_at(best_k.saturating_sub(1));
232 let mut b = theta_at((best_k + 1).min(thetas.len() - 1));
233 if a < 0.0 && b > 0.0 {
236 if theta_at(best_k) > 0.0 {
237 a = theta_at(best_k) * 1e-3;
238 } else {
239 b = theta_at(best_k) * 1e-3;
240 }
241 }
242 let theta = if score(a) > 0.0 && score(b) < 0.0 {
243 bisect(a, b, |t| score(t) > 0.0)
244 } else {
245 theta_at(best_k)
247 };
248 let xi = y.iter().map(|&v| (theta * v).ln_1p()).sum::<f64>() / n as f64;
249 Self::new(xi, xi / theta)
250 }
251}
252
253impl Distribution for Gpd {
254 fn mean(&self) -> f64 {
256 if self.xi >= 1.0 {
257 f64::INFINITY
258 } else {
259 self.location + self.beta / (1.0 - self.xi)
260 }
261 }
262
263 fn variance(&self) -> f64 {
265 if self.xi >= 0.5 {
266 f64::INFINITY
267 } else {
268 self.beta * self.beta / ((1.0 - self.xi).powi(2) * (1.0 - 2.0 * self.xi))
269 }
270 }
271
272 fn cdf(&self, x: f64) -> f64 {
273 1.0 - self.survival_at(x)
274 }
275
276 fn survival(&self, x: f64) -> f64 {
277 self.survival_at(x)
278 }
279
280 fn quantile(&self, p: f64) -> Result<f64> {
281 check_probability(p)?;
282 let e = -(-p).ln_1p();
284 if self.xi == 0.0 {
285 return Ok(self.location + self.beta * e);
286 }
287 Ok(self.location + self.beta * (self.xi * e).exp_m1() / self.xi)
288 }
289}
290
291impl Gpd {
292 fn excess_end(&self) -> f64 {
294 if self.xi < 0.0 {
295 -self.beta / self.xi
296 } else {
297 f64::INFINITY
298 }
299 }
300
301 fn excess_moments(&self, m: f64, b: f64) -> (f64, f64) {
306 let z0 = m - self.location;
307 let s0 = self.excess_survival(z0);
308 if s0 == 0.0 || m >= b {
309 return (0.0, 0.0);
310 }
311 let beta = self.beta + self.xi * z0;
312 let c = (b - m).min(self.excess_end() - z0);
313 let (k0, k1) = gpd_partial_moments(self.xi, beta, c);
314 (s0 * k0, s0 * k1)
315 }
316}
317
318impl Severity for Gpd {
319 fn lev(&self, limit: f64) -> f64 {
320 if limit <= 0.0 {
321 return limit;
322 }
323 self.layer(limit, 0.0)
324 }
325
326 fn stop_loss(&self, retention: f64) -> f64 {
327 if retention <= 0.0 {
328 return self.mean() - retention;
329 }
330 self.layer(f64::INFINITY, retention)
331 }
332
333 fn layer(&self, limit: f64, attachment: f64) -> f64 {
335 let a = attachment.max(0.0);
336 let b = a + limit;
337 let below = (b.min(self.location) - a).max(0.0);
338 below + self.excess_moments(a.max(self.location), b).0
339 }
340
341 fn layer_second_moment(&self, limit: f64, attachment: f64) -> f64 {
343 let a = attachment.max(0.0);
344 let b = a + limit;
345 let below = (b.min(self.location) - a).max(0.0);
346 let m = a.max(self.location);
347 let (k0, k1) = self.excess_moments(m, b);
348 below * below + 2.0 * (k1 + (m - a) * k0)
349 }
350}
351
352fn gpd_partial_moments(xi: f64, beta: f64, c: f64) -> (f64, f64) {
365 let full = c == f64::INFINITY || (xi < 0.0 && c >= -beta / xi);
366 if full {
367 let k0 = if xi < 1.0 {
368 beta / (1.0 - xi)
369 } else {
370 f64::INFINITY
371 };
372 let k1 = if xi < 0.5 {
373 beta * beta / ((1.0 - xi) * (1.0 - 2.0 * xi))
374 } else {
375 f64::INFINITY
376 };
377 return (k0, k1);
378 }
379 if c <= 0.0 {
380 return (0.0, 0.0);
381 }
382 let r = c / beta;
383 let l = (xi * r).ln_1p();
384 let k0 = if xi == 0.0 {
385 beta * -(-r).exp_m1()
386 } else {
387 beta / xi * l * exprel((1.0 - 1.0 / xi) * l)
388 };
389 let k1 = if r * xi.abs().max(1.0) < 0.05 {
390 let mut coef = 1.0;
393 let mut sum = 0.0;
394 for n in 0..14 {
395 sum += coef / (f64::from(n) + 2.0);
396 coef *= -(1.0 + f64::from(n) * xi) * r / (f64::from(n) + 1.0);
397 }
398 c * c * sum
399 } else if xi.abs() < 0.25 {
400 let a = if xi == 0.0 {
401 (-r).exp()
402 } else {
403 ((1.0 - 1.0 / xi) * l).exp()
404 };
405 beta * beta * (1.0 - a * (1.0 + (1.0 - xi) * r)) / ((1.0 - xi) * (1.0 - 2.0 * xi))
406 } else {
407 let q = 1.0 - 1.0 / xi;
408 (beta / xi).powi(2) * l * (exprel((q + 1.0) * l) - exprel(q * l))
409 };
410 (k0, k1)
411}
412
413fn exprel(z: f64) -> f64 {
415 if z == 0.0 { 1.0 } else { z.exp_m1() / z }
416}
417
418#[derive(Debug, Clone, PartialEq)]
436pub struct PotTail {
437 threshold: f64,
438 p_exceed: f64,
439 gpd: Gpd,
440}
441
442impl PotTail {
443 pub fn fit(draws: &impl crate::Empirical, level: f64) -> Result<Self> {
446 check_probability(level)?;
447 let sorted = draws.sorted();
448 let threshold = crate::risk::var_sorted(sorted, level)?;
449 let above: Vec<f64> = sorted
450 .iter()
451 .filter(|&&x| x > threshold)
452 .map(|&x| x - threshold)
453 .collect();
454 let gpd = Gpd::fit(&above)?;
455 Ok(Self {
456 threshold,
457 p_exceed: above.len() as f64 / sorted.len() as f64,
458 gpd,
459 })
460 }
461
462 pub fn new(threshold: f64, p_exceed: f64, gpd: Gpd) -> Result<Self> {
465 if gpd.location != 0.0 {
466 return Err(invalid(
467 "location",
468 gpd.location,
469 "the exceedance GPD must have location 0",
470 ));
471 }
472 if !threshold.is_finite() {
473 return Err(invalid("threshold", threshold, "must be finite"));
474 }
475 if !(p_exceed > 0.0 && p_exceed <= 1.0) {
476 return Err(invalid("p_exceed", p_exceed, "must be in (0, 1]"));
477 }
478 Ok(Self {
479 threshold,
480 p_exceed,
481 gpd,
482 })
483 }
484
485 pub fn threshold(&self) -> f64 {
486 self.threshold
487 }
488
489 pub fn p_exceed(&self) -> f64 {
491 self.p_exceed
492 }
493
494 pub fn gpd(&self) -> &Gpd {
495 &self.gpd
496 }
497
498 pub fn survival(&self, x: f64) -> f64 {
500 self.p_exceed * self.gpd.survival(x - self.threshold)
501 }
502
503 pub fn var(&self, p: f64) -> Result<f64> {
506 self.check_level(p)?;
507 let q = (1.0 - (1.0 - p) / self.p_exceed).max(0.0);
510 Ok(self.threshold + self.gpd.quantile(q)?)
511 }
512
513 pub fn tvar(&self, p: f64) -> Result<f64> {
516 let var = self.var(p)?;
517 let (xi, beta) = (self.gpd.xi, self.gpd.beta);
518 if xi >= 1.0 {
519 return Ok(f64::INFINITY);
520 }
521 Ok(var + (beta + xi * (var - self.threshold)) / (1.0 - xi))
522 }
523
524 fn check_level(&self, p: f64) -> Result<()> {
525 check_probability(p)?;
526 if p < 1.0 - self.p_exceed {
527 return Err(invalid(
528 "p",
529 p,
530 "is below the tail: must be at least 1 - p_exceed",
531 ));
532 }
533 Ok(())
534 }
535}
536
537fn invalid(name: &'static str, value: f64, reason: &'static str) -> Error {
538 Error::InvalidParameter {
539 name,
540 value,
541 reason,
542 }
543}
544
545pub fn mean_excess(draws: &[f64], thresholds: &[f64]) -> Vec<(f64, f64, usize)> {
561 let mut sorted = draws.to_vec();
562 sorted.sort_by(f64::total_cmp);
563 let mut suffix = vec![0.0; sorted.len() + 1];
565 for i in (0..sorted.len()).rev() {
566 suffix[i] = suffix[i + 1] + sorted[i];
567 }
568 thresholds
569 .iter()
570 .map(|&u| {
571 let first = sorted.partition_point(|&x| x <= u);
572 let n = sorted.len() - first;
573 let e = if n == 0 {
574 f64::NAN
575 } else {
576 suffix[first] / n as f64 - u
577 };
578 (u, e, n)
579 })
580 .collect()
581}
582
583pub fn hill(draws: &[f64], ks: &[usize]) -> Result<Vec<f64>> {
602 let mut sorted = draws.to_vec();
603 sorted.sort_by(|a, b| b.total_cmp(a)); ks.iter()
605 .map(|&k| {
606 if k == 0 || k >= sorted.len() {
607 return Err(Error::InvalidParameter {
608 name: "k",
609 value: k as f64,
610 reason: "must be at least 1 and below the number of draws",
611 });
612 }
613 if sorted[k].is_nan() || sorted[k] <= 0.0 {
614 return Err(Error::InvalidParameter {
615 name: "draws",
616 value: sorted[k],
617 reason: "the k + 1 largest must be positive",
618 });
619 }
620 let threshold = sorted[k].ln();
621 Ok(sorted[..k].iter().map(|x| x.ln() - threshold).sum::<f64>() / k as f64)
622 })
623 .collect()
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use prospicio_core::StreamRng;
630
631 #[test]
632 fn gpd_closed_forms() {
633 let g = Gpd::new(0.25, 4.0).unwrap();
634 assert!((g.mean() - 4.0 / 0.75).abs() < 1e-12);
635 assert!((g.variance() - 16.0 / (0.5625 * 0.5)).abs() < 1e-12);
636 let e = Gpd::new(0.0, 2.0).unwrap();
638 assert!((e.cdf(2.0) - (1.0 - (-1f64).exp())).abs() < 1e-15);
639 assert!((e.quantile(0.5).unwrap() - 2.0 * 2f64.ln()).abs() < 1e-15);
640 let b = Gpd::new(-0.5, 1.0).unwrap();
642 assert_eq!(b.survival(2.0), 0.0);
643 assert!((b.quantile(1.0).unwrap() - 2.0).abs() < 1e-15);
644 assert_eq!(Gpd::new(1.5, 1.0).unwrap().mean(), f64::INFINITY);
645 for p in [1e-12, 0.1, 0.5, 0.99, 1.0 - 1e-12] {
646 let x = g.quantile(p).unwrap();
647 assert!((g.cdf(x) - p).abs() < 1e-12 * p.max(1e-3), "p = {p}");
648 }
649 assert!(Gpd::new(0.1, 0.0).is_err());
650 }
651
652 #[test]
653 fn fit_recovers_parameters() {
654 for (xi, beta) in [(0.5, 1.0), (0.0, 3.0), (-0.3, 2.0), (1.2, 0.5)] {
655 let truth = Gpd::new(xi, beta).unwrap();
656 let y = truth.sample(&mut StreamRng::new(11, 0), 50_000);
657 let fit = Gpd::fit(&y).unwrap();
658 assert!((fit.xi() - xi).abs() < 0.03, "xi {xi}: {fit:?}");
659 assert!(
660 (fit.beta() / beta - 1.0).abs() < 0.04,
661 "beta {beta}: {fit:?}"
662 );
663 }
664 }
665
666 #[test]
667 fn fit_rejects_degenerate_input() {
668 assert!(Gpd::fit(&[1.0, 2.0]).is_err());
669 assert!(Gpd::fit(&[1.0, 1.0, 1.0]).is_err());
670 assert!(Gpd::fit(&[1.0, -1.0, 2.0]).is_err());
671 }
672
673 #[test]
674 fn pot_tail_is_exact_for_a_gpd_tail() {
675 let tail = PotTail::new(10.0, 0.05, Gpd::new(0.25, 2.0).unwrap()).unwrap();
677 assert!((tail.var(0.95).unwrap() - 10.0).abs() < 1e-12);
679 assert!((tail.tvar(0.95).unwrap() - (10.0 + 2.0 / 0.75)).abs() < 1e-12);
681 for p in [0.96, 0.99, 0.9999] {
683 let v = tail.var(p).unwrap();
684 assert!((tail.survival(v) - (1.0 - p)).abs() < 1e-14);
685 }
686 assert!(tail.var(0.9).is_err());
687 }
688
689 fn close(a: f64, b: f64, rel: f64) -> bool {
690 (a - b).abs() <= rel * b.abs().max(1e-300)
691 }
692
693 fn quad(g: &Gpd, k: i32, a: f64, b: f64) -> f64 {
696 let u = g.location();
697 if a < u && u < b {
698 return quad_smooth(g, k, a, u) + quad_smooth(g, k, u, b);
699 }
700 quad_smooth(g, k, a, b)
701 }
702
703 fn quad_smooth(g: &Gpd, k: i32, a: f64, b: f64) -> f64 {
704 const X: [f64; 5] = [
706 0.0,
707 -0.538_469_310_105_683_1,
708 0.538_469_310_105_683_1,
709 -0.906_179_845_938_664,
710 0.906_179_845_938_664,
711 ];
712 const W: [f64; 5] = [
713 0.568_888_888_888_888_9,
714 0.478_628_670_499_366_5,
715 0.478_628_670_499_366_5,
716 0.236_926_885_056_189_1,
717 0.236_926_885_056_189_1,
718 ];
719 let n = 4000;
720 let (la, lb) = (a.ln(), b.ln());
721 let h = (lb - la) / f64::from(n);
722 let mut sum = 0.0;
723 for i in 0..n {
724 let mid = la + (f64::from(i) + 0.5) * h;
725 for (x, w) in X.iter().zip(W) {
726 let v = (mid + 0.5 * h * x).exp();
727 sum += w * 0.5 * h * v * v.powi(k) * g.survival(v);
728 }
729 }
730 sum
731 }
732
733 #[test]
734 fn riegel_matches_its_definition() {
735 let g = Gpd::riegel(1000.0, 2.0, 1.5).unwrap();
736 assert!(close(g.xi(), 1.0 / 1.5, 1e-15));
737 assert!(close(g.beta(), 500.0, 1e-15));
738 assert_eq!(g.location(), 1000.0);
739 for x in [1000.0, 1500.0, 1e4, 1e7] {
740 let want = (1.0f64 + (2.0 / 1.5) * (x / 1000.0 - 1.0)).powf(-1.5);
741 assert!(close(g.survival(x), want, 1e-14));
742 }
743 assert_eq!(g.survival(999.0), 1.0);
744 assert!(close(g.cdf(g.quantile(0.9).unwrap()), 0.9, 1e-14));
745 assert!(close(g.mean(), 1000.0 + 500.0 / (1.0 - 1.0 / 1.5), 1e-15));
746 let p = crate::Pareto::new(1000.0, 2.5).unwrap();
748 let r = Gpd::riegel(1000.0, 2.5, 2.5).unwrap();
749 for (l, a) in [(4000.0f64, 1000.0), (1e5, 5e4), (f64::INFINITY, 2000.0)] {
750 assert!(close(r.layer(l, a), p.layer(l, a), 1e-13));
751 assert!(close(
752 r.layer_second_moment(l.min(1e9), a),
753 p.layer_second_moment(l.min(1e9), a),
754 1e-12
755 ));
756 }
757 assert!(Gpd::riegel(0.0, 1.0, 1.0).is_err());
758 assert!(Gpd::riegel(1.0, 0.0, 1.0).is_err());
759 assert!(Gpd::riegel(1.0, 1.0, -1.0).is_err());
760 assert!(PotTail::new(0.0, 0.1, r).is_err());
761 }
762
763 #[test]
764 fn layer_moments_match_quadrature() {
765 for (xi, beta, u) in [
766 (0.0, 100.0, 0.0),
767 (1e-9, 100.0, 0.0),
768 (0.1, 100.0, 50.0),
769 (-0.2, 100.0, 0.0),
770 (-0.6, 100.0, 10.0),
771 (0.25, 100.0, 0.0),
772 (0.5, 100.0, 0.0),
773 (0.8, 100.0, 0.0),
774 (1.0, 100.0, 0.0),
775 (1.7, 100.0, 20.0),
776 ] {
777 let g = Gpd::new(xi, beta).unwrap().shifted(u).unwrap();
778 for (l, a) in [
779 (1.0f64, 30.0f64),
780 (10.0, 30.0),
781 (200.0, 30.0),
782 (500.0, 100.0),
783 (2e4, 1e3),
784 ] {
785 let b = (a + l).min(u + g.excess_end());
786 if a >= b {
787 assert_eq!(g.layer(l, a), 0.0);
788 continue;
789 }
790 let m1 = quad(&g, 0, a, b);
791 let m2 = 2.0 * (quad(&g, 1, a, b) - a * m1);
792 assert!(close(g.layer(l, a), m1, 1e-12), "{xi} {l} xs {a}");
793 assert!(
794 close(g.layer_second_moment(l, a), m2, 1e-11),
795 "{xi} {l} xs {a}: {} {m2}",
796 g.layer_second_moment(l, a)
797 );
798 }
799 }
800 }
801
802 #[test]
803 fn partial_moments_are_continuous_across_branches() {
804 for xi in [-0.3, -0.25, -0.2, 0.0, 0.2, 0.25, 0.3, 2.0] {
806 let below =
807 gpd_partial_moments(xi, 1.0, 0.05 / f64::max(1.0, f64::abs(xi)) * (1.0 - 1e-9));
808 let above =
809 gpd_partial_moments(xi, 1.0, 0.05 / f64::max(1.0, f64::abs(xi)) * (1.0 + 1e-9));
810 assert!(close(below.1, above.1, 1e-8), "{xi} {below:?} {above:?}");
811 }
812 for c in [0.1, 1.0, 3.0] {
813 let lo = gpd_partial_moments(0.25 * (1.0 - 1e-12), 1.0, c);
814 let hi = gpd_partial_moments(0.25, 1.0, c);
815 assert!(close(lo.1, hi.1, 1e-11), "{c}");
816 }
817 let (k0, k1) = gpd_partial_moments(0.0, 1.0, 2.0);
819 assert!(close(k0, 1.0 - (-2.0f64).exp(), 1e-15));
820 assert!(close(k1, 1.0 - 3.0 * (-2.0f64).exp(), 1e-15));
821 }
822
823 #[test]
824 fn severity_identities() {
825 for g in [
826 Gpd::riegel(1000.0, 1.2, 2.5).unwrap(),
827 Gpd::new(-0.3, 50.0).unwrap(),
828 Gpd::new(0.0, 50.0).unwrap().shifted(10.0).unwrap(),
829 ] {
830 for d in [5.0, 100.0, 1500.0, 1e4] {
831 assert!(
832 close(g.lev(d) + g.stop_loss(d), g.mean(), 1e-12),
833 "{g:?} {d}"
834 );
835 }
836 let m2 = g.layer_second_moment(f64::INFINITY, 0.0);
837 let loc = g.location();
838 let raw2 = g.variance() + g.mean() * g.mean();
839 assert!(close(m2, raw2, 1e-12), "{g:?} {m2} {raw2} {loc}");
840 }
841 let heavy = Gpd::riegel(1.0, 1.0, 0.9).unwrap();
842 assert_eq!(heavy.stop_loss(10.0), f64::INFINITY);
843 assert!(heavy.layer(10.0, 10.0).is_finite());
844 }
845}