1use serde::{Deserialize, Serialize};
49use statrs::distribution::{
50 Beta as BetaDist, ContinuousCDF, Gamma as GammaDist, LogNormal as LogNormalDist,
51 Normal as NormalDist,
52};
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58#[non_exhaustive]
59#[serde(tag = "kind")]
60pub enum Distribution {
61 Uniform { lo: f64, hi: f64 },
63
64 Normal { mu: f64, sigma: f64 },
66
67 LogNormal { mu_log: f64, sigma_log: f64 },
70
71 Triangular { lo: f64, mode: f64, hi: f64 },
74
75 Beta {
78 alpha: f64,
79 beta: f64,
80 lo: f64,
81 hi: f64,
82 },
83
84 Gamma { shape: f64, scale: f64 },
89
90 Weibull { shape: f64, scale: f64 },
93
94 Exponential { lambda: f64 },
97
98 Bernoulli { p: f64 },
101
102 DiscreteUniform { lo: i64, hi: i64 },
104}
105
106impl Distribution {
116 #[must_use]
127 pub fn quantile(&self, u: f64) -> f64 {
128 let u = u.clamp(0.0, 1.0);
129 match *self {
130 Self::Uniform { lo, hi } => uniform_quantile(lo, hi, u),
131 Self::Normal { mu, sigma } => normal_quantile(mu, sigma, u),
132 Self::LogNormal { mu_log, sigma_log } => lognormal_quantile(mu_log, sigma_log, u),
133 Self::Triangular { lo, mode, hi } => triangular_quantile(lo, mode, hi, u),
134 Self::Beta {
135 alpha,
136 beta,
137 lo,
138 hi,
139 } => beta_quantile(alpha, beta, lo, hi, u),
140 Self::Gamma { shape, scale } => gamma_quantile(shape, scale, u),
141 Self::Weibull { shape, scale } => weibull_quantile(shape, scale, u),
142 Self::Exponential { lambda } => exponential_quantile(lambda, u),
143 Self::Bernoulli { p } => bernoulli_quantile(p, u),
144 Self::DiscreteUniform { lo, hi } => discrete_uniform_quantile(lo, hi, u),
145 }
146 }
147
148 #[must_use]
153 pub fn support(&self) -> (f64, f64) {
154 match *self {
155 Self::Uniform { lo, hi }
159 | Self::Triangular { lo, hi, .. }
160 | Self::Beta { lo, hi, .. } => (lo, hi),
161 Self::Normal { .. } => (f64::NEG_INFINITY, f64::INFINITY),
162 Self::LogNormal { .. }
163 | Self::Gamma { .. }
164 | Self::Exponential { .. }
165 | Self::Weibull { .. } => (0.0, f64::INFINITY),
166 Self::Bernoulli { .. } => (0.0, 1.0),
167 #[allow(clippy::cast_precision_loss)]
168 Self::DiscreteUniform { lo, hi } => (lo as f64, hi as f64),
169 }
170 }
171}
172
173fn uniform_quantile(lo: f64, hi: f64, u: f64) -> f64 {
176 lo + u * (hi - lo)
177}
178
179fn triangular_quantile(lo: f64, mode: f64, hi: f64, u: f64) -> f64 {
180 assert!(lo < hi, "Triangular: lo must be < hi");
181 assert!(
182 lo <= mode && mode <= hi,
183 "Triangular: mode must be in [lo, hi]"
184 );
185 let f_mode = (mode - lo) / (hi - lo);
186 if u <= f_mode {
187 lo + (u * (hi - lo) * (mode - lo)).sqrt()
188 } else {
189 hi - ((1.0 - u) * (hi - lo) * (hi - mode)).sqrt()
190 }
191}
192
193fn weibull_quantile(shape: f64, scale: f64, u: f64) -> f64 {
194 assert!(shape > 0.0, "Weibull: shape must be > 0");
195 assert!(scale > 0.0, "Weibull: scale must be > 0");
196 if u >= 1.0 {
197 return f64::INFINITY;
198 }
199 scale * (-(1.0 - u).ln()).powf(1.0 / shape)
200}
201
202fn exponential_quantile(lambda: f64, u: f64) -> f64 {
203 assert!(lambda > 0.0, "Exponential: lambda must be > 0");
204 if u >= 1.0 {
205 return f64::INFINITY;
206 }
207 -((1.0 - u).ln()) / lambda
208}
209
210fn bernoulli_quantile(p: f64, u: f64) -> f64 {
211 assert!((0.0..=1.0).contains(&p), "Bernoulli: p must be in [0, 1]");
212 if u <= 1.0 - p {
218 0.0
219 } else {
220 1.0
221 }
222}
223
224fn discrete_uniform_quantile(lo: i64, hi: i64, u: f64) -> f64 {
225 assert!(lo <= hi, "DiscreteUniform: lo must be <= hi");
226 let n = hi - lo + 1;
227 #[allow(clippy::cast_precision_loss)]
228 let scaled = u * (n as f64);
229 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
232 let idx = (scaled.floor() as i64).min(n - 1);
233 #[allow(clippy::cast_precision_loss)]
234 let result = (lo + idx) as f64;
235 result
236}
237
238#[allow(clippy::expect_used)]
250fn normal_quantile(mu: f64, sigma: f64, u: f64) -> f64 {
251 assert!(sigma > 0.0, "Normal: sigma must be > 0");
252 let dist = NormalDist::new(mu, sigma).expect("Normal::new param check");
253 dist.inverse_cdf(u)
254}
255
256#[allow(clippy::expect_used)]
257fn lognormal_quantile(mu_log: f64, sigma_log: f64, u: f64) -> f64 {
258 assert!(sigma_log > 0.0, "LogNormal: sigma_log must be > 0");
259 let dist = LogNormalDist::new(mu_log, sigma_log).expect("LogNormal::new param check");
260 dist.inverse_cdf(u)
261}
262
263#[allow(clippy::expect_used)]
264fn beta_quantile(alpha: f64, beta: f64, lo: f64, hi: f64, u: f64) -> f64 {
265 assert!(alpha > 0.0, "Beta: alpha must be > 0");
266 assert!(beta > 0.0, "Beta: beta must be > 0");
267 assert!(lo < hi, "Beta: lo must be < hi");
268 let dist = BetaDist::new(alpha, beta).expect("Beta::new param check");
269 let v = dist.inverse_cdf(u);
270 lo + (hi - lo) * v
271}
272
273#[allow(clippy::expect_used)]
274fn gamma_quantile(shape: f64, scale: f64, u: f64) -> f64 {
275 assert!(shape > 0.0, "Gamma: shape must be > 0");
276 assert!(scale > 0.0, "Gamma: scale must be > 0");
277 let dist = GammaDist::new(shape, 1.0 / scale).expect("Gamma::new param check");
279 dist.inverse_cdf(u)
280}
281
282#[cfg(test)]
283#[allow(
284 clippy::float_cmp,
285 clippy::approx_constant,
286 clippy::cast_precision_loss
287)]
288mod tests {
289 use super::*;
290
291 fn assert_close(got: f64, want: f64, tol: f64, ctx: &str) {
293 assert!(
294 (got - want).abs() <= tol,
295 "{ctx}: got {got}, want {want}, |Δ|={}, tol={tol}",
296 (got - want).abs()
297 );
298 }
299
300 fn assert_monotone_non_decreasing(d: &Distribution) {
301 let us = [
304 0.0, 0.001, 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 0.999,
305 1.0,
306 ];
307 let mut prev = f64::NEG_INFINITY;
308 for &u in &us {
309 let q = d.quantile(u);
310 assert!(
311 q >= prev || (q.is_nan() && prev.is_nan()),
312 "monotonicity violated for {d:?}: q({u}) = {q} < prev {prev}"
313 );
314 prev = q;
315 }
316 }
317
318 #[test]
321 fn uniform_zero_one_quantile_is_u() {
322 let d = Distribution::Uniform { lo: 0.0, hi: 1.0 };
323 for u in [0.0, 0.25, 0.5, 0.75, 1.0] {
324 assert_eq!(d.quantile(u), u);
325 }
326 }
327
328 #[test]
329 fn uniform_general_quantile_linearly_maps() {
330 let d = Distribution::Uniform { lo: 10.0, hi: 30.0 };
331 assert_eq!(d.quantile(0.0), 10.0);
332 assert_eq!(d.quantile(0.5), 20.0);
333 assert_eq!(d.quantile(1.0), 30.0);
334 }
335
336 #[test]
337 fn uniform_negative_range() {
338 let d = Distribution::Uniform { lo: -5.0, hi: 5.0 };
339 assert_eq!(d.quantile(0.5), 0.0);
340 assert_eq!(d.quantile(0.0), -5.0);
341 assert_eq!(d.quantile(1.0), 5.0);
342 }
343
344 #[test]
345 fn uniform_support_matches_params() {
346 let d = Distribution::Uniform { lo: 2.5, hi: 7.5 };
347 assert_eq!(d.support(), (2.5, 7.5));
348 }
349
350 #[test]
351 fn uniform_monotone() {
352 assert_monotone_non_decreasing(&Distribution::Uniform { lo: 0.0, hi: 1.0 });
353 assert_monotone_non_decreasing(&Distribution::Uniform {
354 lo: -10.0,
355 hi: 100.0,
356 });
357 }
358
359 #[test]
360 fn uniform_saturates_out_of_range_u() {
361 let d = Distribution::Uniform { lo: 0.0, hi: 1.0 };
362 assert_eq!(d.quantile(-0.5), 0.0);
363 assert_eq!(d.quantile(1.5), 1.0);
364 }
365
366 #[test]
369 fn normal_quantile_at_half_is_mean() {
370 let d = Distribution::Normal {
371 mu: 5.0,
372 sigma: 2.0,
373 };
374 assert_close(d.quantile(0.5), 5.0, 1e-12, "Normal median");
375 }
376
377 #[test]
378 fn normal_quantile_one_sigma_above_mean() {
379 let d = Distribution::Normal {
381 mu: 0.0,
382 sigma: 1.0,
383 };
384 assert_close(d.quantile(0.841_344_746_068_543), 1.0, 1e-9, "+1σ");
385 }
386
387 #[test]
388 fn normal_quantile_symmetric_about_mean() {
389 let d = Distribution::Normal {
390 mu: 7.0,
391 sigma: 3.0,
392 };
393 for u in [0.1, 0.2, 0.3, 0.4] {
394 let q_lo = d.quantile(u);
395 let q_hi = d.quantile(1.0 - u);
396 assert_close(q_lo + q_hi, 2.0 * 7.0, 1e-9, "Normal symmetry");
398 }
399 }
400
401 #[test]
402 fn normal_support_is_unbounded() {
403 let d = Distribution::Normal {
404 mu: 0.0,
405 sigma: 1.0,
406 };
407 let (lo, hi) = d.support();
408 assert_eq!(lo, f64::NEG_INFINITY);
409 assert_eq!(hi, f64::INFINITY);
410 }
411
412 #[test]
413 fn normal_monotone() {
414 assert_monotone_non_decreasing(&Distribution::Normal {
415 mu: 0.0,
416 sigma: 1.0,
417 });
418 }
419
420 #[test]
421 #[should_panic(expected = "sigma must be > 0")]
422 fn normal_zero_sigma_panics() {
423 let d = Distribution::Normal {
424 mu: 0.0,
425 sigma: 0.0,
426 };
427 let _ = d.quantile(0.5);
428 }
429
430 #[test]
433 fn lognormal_quantile_at_half_is_exp_mu_log() {
434 let d = Distribution::LogNormal {
436 mu_log: 1.0,
437 sigma_log: 0.5,
438 };
439 assert_close(d.quantile(0.5), 1.0_f64.exp(), 1e-9, "LogNormal median");
440 }
441
442 #[test]
443 fn lognormal_support_is_zero_to_infinity() {
444 let d = Distribution::LogNormal {
445 mu_log: 0.0,
446 sigma_log: 1.0,
447 };
448 let (lo, hi) = d.support();
449 assert_eq!(lo, 0.0);
450 assert_eq!(hi, f64::INFINITY);
451 }
452
453 #[test]
454 fn lognormal_monotone() {
455 assert_monotone_non_decreasing(&Distribution::LogNormal {
456 mu_log: 0.0,
457 sigma_log: 1.0,
458 });
459 }
460
461 #[test]
464 fn triangular_quantile_at_zero_is_lo() {
465 let d = Distribution::Triangular {
466 lo: 0.0,
467 mode: 0.5,
468 hi: 1.0,
469 };
470 assert_eq!(d.quantile(0.0), 0.0);
471 }
472
473 #[test]
474 fn triangular_quantile_at_one_is_hi() {
475 let d = Distribution::Triangular {
476 lo: 0.0,
477 mode: 0.5,
478 hi: 1.0,
479 };
480 assert_eq!(d.quantile(1.0), 1.0);
481 }
482
483 #[test]
484 fn triangular_at_f_mode_is_mode() {
485 let d = Distribution::Triangular {
487 lo: 0.0,
488 mode: 0.5,
489 hi: 1.0,
490 };
491 assert_close(d.quantile(0.5), 0.5, 1e-12, "Triangular at F(mode)");
492 }
493
494 #[test]
495 fn triangular_asymmetric_mode() {
496 let d = Distribution::Triangular {
498 lo: 0.0,
499 mode: 0.25,
500 hi: 1.0,
501 };
502 assert_close(d.quantile(0.25), 0.25, 1e-12, "asymmetric mode");
503 }
504
505 #[test]
506 fn triangular_support_matches_params() {
507 let d = Distribution::Triangular {
508 lo: -2.0,
509 mode: 0.0,
510 hi: 5.0,
511 };
512 assert_eq!(d.support(), (-2.0, 5.0));
513 }
514
515 #[test]
516 fn triangular_monotone_symmetric() {
517 assert_monotone_non_decreasing(&Distribution::Triangular {
518 lo: 0.0,
519 mode: 0.5,
520 hi: 1.0,
521 });
522 }
523
524 #[test]
525 fn triangular_monotone_asymmetric() {
526 assert_monotone_non_decreasing(&Distribution::Triangular {
527 lo: -10.0,
528 mode: -3.0,
529 hi: 7.0,
530 });
531 }
532
533 #[test]
536 fn beta_quantile_at_half_for_alpha_eq_beta_is_midpoint() {
537 let d = Distribution::Beta {
539 alpha: 2.0,
540 beta: 2.0,
541 lo: 0.0,
542 hi: 1.0,
543 };
544 assert_close(d.quantile(0.5), 0.5, 1e-9, "symmetric Beta median");
545 }
546
547 #[test]
548 fn beta_affine_to_general_range() {
549 let d = Distribution::Beta {
551 alpha: 2.0,
552 beta: 2.0,
553 lo: 10.0,
554 hi: 30.0,
555 };
556 assert_close(d.quantile(0.5), 20.0, 1e-8, "Beta affine median");
557 }
558
559 #[test]
560 fn beta_quantile_at_zero_is_lo() {
561 let d = Distribution::Beta {
562 alpha: 2.0,
563 beta: 5.0,
564 lo: 1.0,
565 hi: 7.0,
566 };
567 assert_close(d.quantile(0.0), 1.0, 1e-12, "Beta lo edge");
568 }
569
570 #[test]
571 fn beta_quantile_at_one_is_hi() {
572 let d = Distribution::Beta {
573 alpha: 2.0,
574 beta: 5.0,
575 lo: 1.0,
576 hi: 7.0,
577 };
578 assert_close(d.quantile(1.0), 7.0, 1e-12, "Beta hi edge");
579 }
580
581 #[test]
582 fn beta_uniform_special_case() {
583 let d = Distribution::Beta {
585 alpha: 1.0,
586 beta: 1.0,
587 lo: 0.0,
588 hi: 1.0,
589 };
590 for u in [0.1, 0.3, 0.5, 0.7, 0.9] {
591 assert_close(d.quantile(u), u, 1e-9, "Beta(1,1) ≡ Uniform");
592 }
593 }
594
595 #[test]
596 fn beta_monotone() {
597 assert_monotone_non_decreasing(&Distribution::Beta {
598 alpha: 2.0,
599 beta: 5.0,
600 lo: 0.0,
601 hi: 1.0,
602 });
603 }
604
605 #[test]
608 fn gamma_shape_one_collapses_to_exponential() {
609 let d_g = Distribution::Gamma {
611 shape: 1.0,
612 scale: 2.0,
613 };
614 let d_e = Distribution::Exponential { lambda: 0.5 };
615 for u in [0.1, 0.3, 0.5, 0.7, 0.9] {
616 assert_close(
617 d_g.quantile(u),
618 d_e.quantile(u),
619 1e-7,
620 "Gamma(1) ≡ Exponential",
621 );
622 }
623 }
624
625 #[test]
626 fn gamma_quantile_at_zero_is_zero() {
627 let d = Distribution::Gamma {
628 shape: 2.0,
629 scale: 3.0,
630 };
631 assert_close(d.quantile(0.0), 0.0, 1e-12, "Gamma lo edge");
632 }
633
634 #[test]
635 fn gamma_support() {
636 let d = Distribution::Gamma {
637 shape: 2.0,
638 scale: 3.0,
639 };
640 assert_eq!(d.support(), (0.0, f64::INFINITY));
641 }
642
643 #[test]
644 fn gamma_monotone() {
645 assert_monotone_non_decreasing(&Distribution::Gamma {
646 shape: 2.0,
647 scale: 3.0,
648 });
649 }
650
651 #[test]
654 fn weibull_shape_one_collapses_to_exponential() {
655 let d_w = Distribution::Weibull {
657 shape: 1.0,
658 scale: 4.0,
659 };
660 let d_e = Distribution::Exponential { lambda: 0.25 };
661 for u in [0.1, 0.3, 0.5, 0.7, 0.9] {
662 assert_close(
663 d_w.quantile(u),
664 d_e.quantile(u),
665 1e-12,
666 "Weibull(1) ≡ Exponential",
667 );
668 }
669 }
670
671 #[test]
672 fn weibull_quantile_at_zero_is_zero() {
673 let d = Distribution::Weibull {
674 shape: 2.0,
675 scale: 1.0,
676 };
677 assert_close(d.quantile(0.0), 0.0, 1e-12, "Weibull lo edge");
678 }
679
680 #[test]
681 fn weibull_quantile_at_one_is_infinity() {
682 let d = Distribution::Weibull {
683 shape: 2.0,
684 scale: 1.0,
685 };
686 assert_eq!(d.quantile(1.0), f64::INFINITY);
687 }
688
689 #[test]
690 fn weibull_monotone() {
691 assert_monotone_non_decreasing(&Distribution::Weibull {
692 shape: 2.0,
693 scale: 1.0,
694 });
695 }
696
697 #[test]
700 fn exponential_quantile_at_zero_is_zero() {
701 let d = Distribution::Exponential { lambda: 1.0 };
702 assert_eq!(d.quantile(0.0), 0.0);
703 }
704
705 #[test]
706 fn exponential_quantile_at_one_is_infinity() {
707 let d = Distribution::Exponential { lambda: 1.0 };
708 assert_eq!(d.quantile(1.0), f64::INFINITY);
709 }
710
711 #[test]
712 fn exponential_quantile_known_point() {
713 let lambda = 2.0_f64;
715 let d = Distribution::Exponential { lambda };
716 let u = 1.0 - (-1.0_f64).exp();
717 assert_close(d.quantile(u), 1.0 / lambda, 1e-12, "Exponential @1/λ");
718 }
719
720 #[test]
721 fn exponential_monotone() {
722 assert_monotone_non_decreasing(&Distribution::Exponential { lambda: 1.0 });
723 }
724
725 #[test]
728 fn bernoulli_zero_p_is_always_zero() {
729 let d = Distribution::Bernoulli { p: 0.0 };
730 for u in [0.0, 0.25, 0.5, 0.75, 1.0] {
731 assert_eq!(d.quantile(u), 0.0);
732 }
733 }
734
735 #[test]
736 fn bernoulli_one_p_returns_one_above_zero() {
737 let d = Distribution::Bernoulli { p: 1.0 };
742 assert_eq!(d.quantile(0.0), 0.0); for u in [0.000_001, 0.25, 0.5, 0.75, 1.0] {
744 assert_eq!(d.quantile(u), 1.0);
745 }
746 }
747
748 #[test]
749 fn bernoulli_threshold_is_inclusive_at_one_minus_p() {
750 let d = Distribution::Bernoulli { p: 0.3 };
754 assert_eq!(d.quantile(0.0), 0.0);
756 assert_eq!(d.quantile(0.5), 0.0);
757 assert_eq!(d.quantile(0.69), 0.0);
758 assert_eq!(d.quantile(0.7), 0.0); assert_eq!(d.quantile(0.700_000_000_001), 1.0);
761 assert_eq!(d.quantile(0.99), 1.0);
762 assert_eq!(d.quantile(1.0), 1.0);
763 }
764
765 #[test]
766 fn bernoulli_monotone() {
767 assert_monotone_non_decreasing(&Distribution::Bernoulli { p: 0.4 });
768 }
769
770 #[test]
773 fn discrete_uniform_singleton() {
774 let d = Distribution::DiscreteUniform { lo: 5, hi: 5 };
775 for u in [0.0, 0.5, 1.0] {
776 assert_eq!(d.quantile(u), 5.0);
777 }
778 }
779
780 #[test]
781 fn discrete_uniform_two_values() {
782 let d = Distribution::DiscreteUniform { lo: 0, hi: 1 };
783 assert_eq!(d.quantile(0.0), 0.0);
785 assert_eq!(d.quantile(0.49), 0.0);
786 assert_eq!(d.quantile(0.5), 1.0);
787 assert_eq!(d.quantile(0.99), 1.0);
788 assert_eq!(d.quantile(1.0), 1.0);
789 }
790
791 #[test]
792 fn discrete_uniform_six_values() {
793 let d = Distribution::DiscreteUniform { lo: 1, hi: 6 };
795 assert_eq!(d.quantile(0.0), 1.0);
796 assert_eq!(d.quantile(1.0 / 6.0 + 1e-9), 2.0); assert_eq!(d.quantile(0.5), 4.0); assert_eq!(d.quantile(1.0), 6.0); }
800
801 #[test]
802 fn discrete_uniform_negative_range() {
803 let d = Distribution::DiscreteUniform { lo: -3, hi: 3 };
804 assert_eq!(d.quantile(0.0), -3.0);
805 assert_eq!(d.quantile(1.0), 3.0);
806 let mid = d.quantile(0.5);
807 assert_eq!(mid, 0.0);
809 }
810
811 #[test]
812 fn discrete_uniform_monotone() {
813 assert_monotone_non_decreasing(&Distribution::DiscreteUniform { lo: 1, hi: 10 });
814 }
815
816 #[test]
819 fn distribution_serde_round_trip_for_all_variants() {
820 let cases = vec![
821 Distribution::Uniform { lo: 1.0, hi: 5.0 },
822 Distribution::Normal {
823 mu: 0.0,
824 sigma: 2.0,
825 },
826 Distribution::LogNormal {
827 mu_log: 1.0,
828 sigma_log: 0.5,
829 },
830 Distribution::Triangular {
831 lo: 0.0,
832 mode: 0.3,
833 hi: 1.0,
834 },
835 Distribution::Beta {
836 alpha: 2.0,
837 beta: 5.0,
838 lo: 0.0,
839 hi: 1.0,
840 },
841 Distribution::Gamma {
842 shape: 2.0,
843 scale: 1.0,
844 },
845 Distribution::Weibull {
846 shape: 1.5,
847 scale: 2.0,
848 },
849 Distribution::Exponential { lambda: 0.7 },
850 Distribution::Bernoulli { p: 0.3 },
851 Distribution::DiscreteUniform { lo: 1, hi: 6 },
852 ];
853 for d in cases {
854 let json = serde_json::to_string(&d).expect("serialize");
855 let back: Distribution = serde_json::from_str(&json).expect("deserialize");
856 assert_eq!(back, d, "round-trip {d:?} → {json} → {back:?}");
857 }
858 }
859
860 #[test]
861 fn quantile_at_zero_returns_lower_support_for_finite_distributions() {
862 let cases = vec![
863 (Distribution::Uniform { lo: 2.0, hi: 5.0 }, 2.0),
864 (
865 Distribution::Triangular {
866 lo: -1.0,
867 mode: 0.0,
868 hi: 1.0,
869 },
870 -1.0,
871 ),
872 (
873 Distribution::Beta {
874 alpha: 2.0,
875 beta: 3.0,
876 lo: 0.5,
877 hi: 1.5,
878 },
879 0.5,
880 ),
881 (Distribution::Bernoulli { p: 0.4 }, 0.0),
882 (Distribution::DiscreteUniform { lo: -2, hi: 2 }, -2.0),
883 ];
884 for (d, lo) in cases {
885 assert_close(d.quantile(0.0), lo, 1e-9, "lo edge");
886 }
887 }
888
889 #[test]
890 fn quantile_at_one_returns_upper_support_for_finite_distributions() {
891 let cases = vec![
892 (Distribution::Uniform { lo: 2.0, hi: 5.0 }, 5.0),
893 (
894 Distribution::Triangular {
895 lo: -1.0,
896 mode: 0.0,
897 hi: 1.0,
898 },
899 1.0,
900 ),
901 (
902 Distribution::Beta {
903 alpha: 2.0,
904 beta: 3.0,
905 lo: 0.5,
906 hi: 1.5,
907 },
908 1.5,
909 ),
910 (Distribution::Bernoulli { p: 0.4 }, 1.0),
911 (Distribution::DiscreteUniform { lo: -2, hi: 2 }, 2.0),
912 ];
913 for (d, hi) in cases {
914 assert_close(d.quantile(1.0), hi, 1e-9, "hi edge");
915 }
916 }
917}