Skip to main content

probl_engine/
continuous.rs

1//! Continuous distributions (docs/semantics.md, section 13): densities,
2//! CDFs, quantiles and moments, and drawing from them.
3//!
4//! Drawing uses its own generator and `libm` rather than the platform's math
5//! library, so that a seed gives the same numbers everywhere (section 14).
6
7use crate::error::{Fault, OpError, OpResult};
8use std::f64::consts::{PI, SQRT_2};
9use std::fmt;
10
11/// The 95% quantile of the standard normal: an estimate's 90% interval is
12/// this many standard deviations on each side of its centre.
13pub const Z95: f64 = 1.644853626951472;
14
15/// A continuous distribution.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum Family {
18    Normal {
19        mean: f64,
20        sd: f64,
21    },
22    Lognormal {
23        mu: f64,
24        sigma: f64,
25    },
26    Uniform {
27        lo: f64,
28        hi: f64,
29    },
30    Beta {
31        a: f64,
32        b: f64,
33    },
34    Gamma {
35        shape: f64,
36        scale: f64,
37    },
38    Exponential {
39        rate: f64,
40    },
41    Triangular {
42        lo: f64,
43        mode: f64,
44        hi: f64,
45    },
46    /// A beta distribution stretched over `lo..hi` with its mode at `mode`.
47    Pert {
48        lo: f64,
49        mode: f64,
50        hi: f64,
51    },
52}
53
54fn check(ok: bool, message: impl FnOnce() -> String) -> OpResult<()> {
55    if ok {
56        Ok(())
57    } else {
58        Err(OpError::fault(Fault::DomainError, message()))
59    }
60}
61
62fn finite(values: &[f64], what: &str) -> OpResult<()> {
63    check(values.iter().all(|x| x.is_finite()), || {
64        format!("{what} needs finite numbers")
65    })
66}
67
68/// a/(a+b) for positive parameters, even when their sum overflows.
69fn ratio(a: f64, b: f64) -> f64 {
70    if a >= b {
71        1.0 / (1.0 + b / a)
72    } else {
73        let r = a / b;
74        r / (1.0 + r)
75    }
76}
77
78fn interval_fraction(lo: f64, hi: f64, x: f64) -> f64 {
79    if x <= lo {
80        return 0.0;
81    }
82    if x >= hi {
83        return 1.0;
84    }
85    let width = hi - lo;
86    if width.is_finite() {
87        (x - lo) / width
88    } else {
89        (x / 2.0 - lo / 2.0) / (hi / 2.0 - lo / 2.0)
90    }
91}
92
93fn inverse_width(lo: f64, hi: f64) -> f64 {
94    let width = hi - lo;
95    if width.is_finite() {
96        1.0 / width
97    } else {
98        0.5 / (hi / 2.0 - lo / 2.0)
99    }
100}
101
102fn standardized(x: f64, mean: f64, sd: f64) -> f64 {
103    if !x.is_finite() {
104        return x;
105    }
106    let delta = x - mean;
107    if delta.is_finite() {
108        delta / sd
109    } else {
110        x / sd - mean / sd
111    }
112}
113
114impl Family {
115    pub fn normal(mean: f64, sd: f64) -> OpResult<Family> {
116        finite(&[mean, sd], "normal")?;
117        check(sd > 0.0, || "normal needs a standard deviation above 0".into())?;
118        Ok(Family::Normal { mean, sd })
119    }
120
121    pub fn lognormal(mu: f64, sigma: f64) -> OpResult<Family> {
122        finite(&[mu, sigma], "lognormal")?;
123        check(sigma > 0.0, || "lognormal needs a sigma above 0".into())?;
124        Ok(Family::Lognormal { mu, sigma })
125    }
126
127    pub fn uniform(lo: f64, hi: f64) -> OpResult<Family> {
128        finite(&[lo, hi], "uniform")?;
129        check(lo < hi, || "uniform needs its lower end below its upper end".into())?;
130        Ok(Family::Uniform { lo, hi })
131    }
132
133    pub fn beta(a: f64, b: f64) -> OpResult<Family> {
134        finite(&[a, b], "beta")?;
135        check(a > 0.0 && b > 0.0, || "beta needs two parameters above 0".into())?;
136        Ok(Family::Beta { a, b })
137    }
138
139    pub fn gamma(shape: f64, scale: f64) -> OpResult<Family> {
140        finite(&[shape, scale], "gamma")?;
141        check(shape > 0.0 && scale > 0.0, || {
142            "gamma needs a shape and a scale above 0".into()
143        })?;
144        Ok(Family::Gamma { shape, scale })
145    }
146
147    pub fn exponential(rate: f64) -> OpResult<Family> {
148        finite(&[rate], "exponential")?;
149        check(rate > 0.0, || "exponential needs a rate above 0".into())?;
150        Ok(Family::Exponential { rate })
151    }
152
153    pub fn triangular(lo: f64, mode: f64, hi: f64) -> OpResult<Family> {
154        finite(&[lo, mode, hi], "triangular")?;
155        check(lo < hi && lo <= mode && mode <= hi, || {
156            "triangular needs lo < hi, with the mode between them".into()
157        })?;
158        Ok(Family::Triangular { lo, mode, hi })
159    }
160
161    pub fn pert(lo: f64, mode: f64, hi: f64) -> OpResult<Family> {
162        finite(&[lo, mode, hi], "pert")?;
163        check(lo < hi && lo <= mode && mode <= hi, || {
164            "pert needs lo < hi, with the mode between them".into()
165        })?;
166        Ok(Family::Pert { lo, mode, hi })
167    }
168
169    /// `a to b`: the lognormal whose 5% and 95% quantiles are `a` and `b`.
170    pub fn estimate(a: f64, b: f64) -> OpResult<Family> {
171        finite(&[a, b], "`to`")?;
172        if a <= 0.0 || b <= 0.0 {
173            return Err(
174                OpError::fault(Fault::DomainError, "`a to b` needs two positive numbers")
175                    .help("for a quantity that can be zero or negative, use `normal_range(lo, hi)`"),
176            );
177        }
178        check(a < b, || "`a to b` needs a below b".into())?;
179        let (la, lb) = (libm::log(a), libm::log(b));
180        Family::lognormal((la + lb) / 2.0, (lb - la) / (2.0 * Z95))
181    }
182
183    /// `normal_range(lo, hi)`: the normal whose 5% and 95% quantiles are
184    /// `lo` and `hi`.
185    pub fn normal_range(lo: f64, hi: f64) -> OpResult<Family> {
186        finite(&[lo, hi], "normal_range")?;
187        check(lo < hi, || {
188            "normal_range needs its lower end below its upper end".into()
189        })?;
190        Family::normal(
191            crate::stats::midpoint(lo, hi),
192            crate::stats::scaled_difference(hi, lo, 1.0 / (2.0 * Z95)),
193        )
194    }
195
196    /// The parameters of the beta distribution behind a PERT.
197    fn pert_shape(lo: f64, mode: f64, hi: f64) -> (f64, f64) {
198        let p = interval_fraction(lo, hi, mode);
199        (1.0 + 4.0 * p, 1.0 + 4.0 * (1.0 - p))
200    }
201
202    /// The smallest and largest possible values.
203    pub fn support(&self) -> (f64, f64) {
204        match *self {
205            Family::Normal { .. } => (f64::NEG_INFINITY, f64::INFINITY),
206            Family::Lognormal { .. } | Family::Gamma { .. } | Family::Exponential { .. } => (0.0, f64::INFINITY),
207            Family::Uniform { lo, hi } | Family::Triangular { lo, hi, .. } | Family::Pert { lo, hi, .. } => (lo, hi),
208            Family::Beta { .. } => (0.0, 1.0),
209        }
210    }
211
212    pub fn mean(&self) -> f64 {
213        match *self {
214            Family::Normal { mean, .. } => mean,
215            Family::Lognormal { mu, sigma } => crate::math::exp(mu + sigma * sigma / 2.0),
216            Family::Uniform { lo, hi } => crate::stats::midpoint(lo, hi),
217            Family::Beta { a, b } => ratio(a, b),
218            Family::Gamma { shape, scale } => shape * scale,
219            Family::Exponential { rate } => 1.0 / rate,
220            Family::Triangular { lo, mode, hi } => crate::stats::lerp(crate::stats::midpoint(lo, hi), mode, 1.0 / 3.0),
221            Family::Pert { lo, mode, hi } => {
222                let (a, b) = Family::pert_shape(lo, mode, hi);
223                crate::stats::lerp(lo, hi, ratio(a, b))
224            }
225        }
226    }
227
228    pub fn variance(&self) -> f64 {
229        let sd = self.sd();
230        sd * sd
231    }
232
233    pub fn sd(&self) -> f64 {
234        match *self {
235            Family::Normal { sd, .. } => sd,
236            Family::Lognormal { mu, sigma } => {
237                let s2 = sigma * sigma;
238                let log_sd = if s2 == 0.0 {
239                    mu + libm::log(sigma)
240                } else {
241                    mu + s2 + 0.5 * libm::log(-libm::expm1(-s2))
242                };
243                crate::math::exp(log_sd)
244            }
245            Family::Uniform { lo, hi } => crate::stats::scaled_difference(hi, lo, 1.0 / libm::sqrt(12.0)),
246            Family::Beta { a, b } => {
247                let max = a.max(b);
248                let inv = if max < 1.0 {
249                    1.0 / (1.0 + a + b)
250                } else {
251                    (1.0 / max) / (1.0 + a.min(b) / max + 1.0 / max)
252                };
253                libm::sqrt(ratio(a, b)) * libm::sqrt(ratio(b, a)) * libm::sqrt(inv)
254            }
255            Family::Gamma { shape, scale } => libm::sqrt(shape) * scale,
256            Family::Exponential { rate } => 1.0 / rate,
257            Family::Triangular { lo, mode, hi } => {
258                let d = |a, b| crate::stats::scaled_difference(a, b, 1.0 / 6.0);
259                libm::hypot(libm::hypot(d(mode, lo), d(hi, mode)), d(hi, lo))
260            }
261            Family::Pert { lo, mode, hi } => {
262                let (a, b) = Family::pert_shape(lo, mode, hi);
263                crate::stats::scaled_difference(hi, lo, Family::Beta { a, b }.sd())
264            }
265        }
266    }
267
268    /// Conditional moments on a nonempty interval, using incomplete moments.
269    /// Uniform and normal use centered formulas to avoid subtracting large
270    /// location parameters when computing the variance.
271    pub fn interval_moments(&self, lo: f64, hi: f64) -> (f64, f64) {
272        let mass = self.cdf(hi) - self.cdf(lo);
273        let raw = |first: f64, second: f64| {
274            let mean = first / mass;
275            (mean, (second / mass - mean * mean).max(0.0))
276        };
277        match *self {
278            Family::Uniform { .. } => (crate::stats::midpoint(lo, hi), Family::Uniform { lo, hi }.variance()),
279            Family::Normal { mean, sd } => {
280                let (a, b) = ((lo - mean) / sd, (hi - mean) / sd);
281                let (pa, pb) = (std_normal_pdf(a), std_normal_pdf(b));
282                let shift = (pa - pb) / mass;
283                let edge = |z: f64, p: f64| if z.is_finite() { z * p } else { 0.0 };
284                (
285                    mean + sd * shift,
286                    sd * sd * (1.0 + (edge(a, pa) - edge(b, pb)) / mass - shift * shift).max(0.0),
287                )
288            }
289            Family::Beta { a, b } => {
290                let moment = |n: f64| beta_cdf(a + n, b, hi) - beta_cdf(a + n, b, lo);
291                raw(
292                    a / (a + b) * moment(1.0),
293                    a * (a + 1.0) / ((a + b) * (a + b + 1.0)) * moment(2.0),
294                )
295            }
296            Family::Gamma { shape, scale } => {
297                let moment = |n: f64| gamma_cdf(shape + n, hi / scale) - gamma_cdf(shape + n, lo / scale);
298                raw(
299                    shape * scale * moment(1.0),
300                    shape * (shape + 1.0) * scale * scale * moment(2.0),
301                )
302            }
303            Family::Exponential { rate } => Family::Gamma {
304                shape: 1.0,
305                scale: 1.0 / rate,
306            }
307            .interval_moments(lo, hi),
308            Family::Lognormal { mu, sigma } => {
309                let moment = |n: f64| {
310                    let cdf = |x| std_normal_cdf((libm::log(x) - mu - n * sigma * sigma) / sigma);
311                    crate::math::exp(n * mu + n * n * sigma * sigma / 2.0) * (cdf(hi) - cdf(lo))
312                };
313                raw(moment(1.0), moment(2.0))
314            }
315            Family::Pert { lo: a, mode, hi: b } => {
316                let (alpha, beta) = Self::pert_shape(a, mode, b);
317                let (m, v) =
318                    Family::Beta { a: alpha, b: beta }.interval_moments((lo - a) / (b - a), (hi - a) / (b - a));
319                (a + (b - a) * m, (b - a).powi(2) * v)
320            }
321            Family::Triangular { lo: a, mode, hi: b } => {
322                let width = b - a;
323                let (l, h, m) = ((lo - a) / width, (hi - a) / width, (mode - a) / width);
324                let moment = |n: i32| {
325                    let integral = |l: f64, h: f64, k: i32| (h.powi(k + 1) - l.powi(k + 1)) / (k + 1) as f64;
326                    let left = if l < m {
327                        2.0 / m * integral(l, h.min(m), n + 1)
328                    } else {
329                        0.0
330                    };
331                    let right = if h > m {
332                        2.0 / (1.0 - m) * (integral(l.max(m), h, n) - integral(l.max(m), h, n + 1))
333                    } else {
334                        0.0
335                    };
336                    left + right
337                };
338                let (m, v) = raw(moment(1), moment(2));
339                (a + width * m, width * width * v)
340            }
341        }
342    }
343
344    pub fn pdf(&self, x: f64) -> f64 {
345        match *self {
346            Family::Normal { mean, sd } => std_normal_pdf(standardized(x, mean, sd)) / sd,
347            Family::Lognormal { mu, sigma } => {
348                if x <= 0.0 {
349                    0.0
350                } else {
351                    std_normal_pdf((libm::log(x) - mu) / sigma) / (sigma * x)
352                }
353            }
354            Family::Uniform { lo, hi } => {
355                if (lo..=hi).contains(&x) {
356                    inverse_width(lo, hi)
357                } else {
358                    0.0
359                }
360            }
361            Family::Beta { a, b } => beta_pdf(a, b, x),
362            Family::Gamma { shape, scale } => {
363                if x < 0.0 {
364                    return 0.0;
365                }
366                if x == 0.0 {
367                    return if shape < 1.0 {
368                        f64::INFINITY
369                    } else if shape == 1.0 {
370                        1.0 / scale
371                    } else {
372                        0.0
373                    };
374                }
375                let y = x / scale;
376                crate::math::exp((shape - 1.0) * libm::log(y) - y - libm::lgamma(shape)) / scale
377            }
378            Family::Exponential { rate } => {
379                if x < 0.0 {
380                    0.0
381                } else {
382                    rate * crate::math::exp(-rate * x)
383                }
384            }
385            Family::Triangular { lo, mode, hi } => {
386                if x < lo || x > hi {
387                    0.0
388                } else if x < mode {
389                    2.0 * interval_fraction(lo, mode, x) * inverse_width(lo, hi)
390                } else if x > mode {
391                    2.0 * (1.0 - interval_fraction(mode, hi, x)) * inverse_width(lo, hi)
392                } else {
393                    2.0 * inverse_width(lo, hi)
394                }
395            }
396            Family::Pert { lo, mode, hi } => {
397                let (a, b) = Family::pert_shape(lo, mode, hi);
398                if x < lo || x > hi {
399                    0.0
400                } else {
401                    beta_pdf(a, b, interval_fraction(lo, hi, x)) * inverse_width(lo, hi)
402                }
403            }
404        }
405    }
406
407    /// ln f(x), which doesn't underflow far into a tail: −∞ outside the
408    /// support.
409    pub fn ln_pdf(&self, x: f64) -> f64 {
410        match *self {
411            Family::Normal { mean, sd } => {
412                let z = standardized(x, mean, sd);
413                -0.5 * z * z - libm::log(sd) - LN_SQRT_2PI
414            }
415            Family::Lognormal { mu, sigma } if x > 0.0 => {
416                let z = (libm::log(x) - mu) / sigma;
417                -0.5 * z * z - libm::log(sigma) - libm::log(x) - LN_SQRT_2PI
418            }
419            Family::Beta { a, b } if x > 0.0 && x < 1.0 => {
420                (a - 1.0) * libm::log(x) + (b - 1.0) * libm::log1p(-x) - ln_beta(a, b)
421            }
422            Family::Gamma { shape, scale } if x > 0.0 => {
423                let y = x / scale;
424                (shape - 1.0) * libm::log(y) - y - libm::lgamma(shape) - libm::log(scale)
425            }
426            Family::Exponential { rate } if x >= 0.0 => libm::log(rate) - rate * x,
427            Family::Pert { lo, mode, hi } if x > lo && x < hi => {
428                let (a, b) = Family::pert_shape(lo, mode, hi);
429                Family::Beta { a, b }.ln_pdf(interval_fraction(lo, hi, x)) + libm::log(inverse_width(lo, hi))
430            }
431            // Ends, bounded densities and outside the support.
432            _ => libm::log(self.pdf(x)),
433        }
434    }
435
436    /// P(X ≤ x).
437    pub fn cdf(&self, x: f64) -> f64 {
438        if x.is_nan() {
439            return f64::NAN;
440        }
441        match *self {
442            Family::Normal { mean, sd } => std_normal_cdf(standardized(x, mean, sd)),
443            Family::Lognormal { mu, sigma } => {
444                if x <= 0.0 {
445                    0.0
446                } else {
447                    std_normal_cdf((libm::log(x) - mu) / sigma)
448                }
449            }
450            Family::Uniform { lo, hi } => interval_fraction(lo, hi, x).clamp(0.0, 1.0),
451            Family::Beta { a, b } => beta_cdf(a, b, x),
452            Family::Gamma { shape, scale } => gamma_cdf(shape, x / scale),
453            Family::Exponential { rate } => {
454                if x <= 0.0 {
455                    0.0
456                } else {
457                    -libm::expm1(-rate * x)
458                }
459            }
460            Family::Triangular { lo, mode, hi } => {
461                if x <= lo {
462                    0.0
463                } else if x >= hi {
464                    1.0
465                } else if x <= mode {
466                    interval_fraction(lo, hi, x) * interval_fraction(lo, mode, x)
467                } else {
468                    1.0 - (1.0 - interval_fraction(lo, hi, x)) * (1.0 - interval_fraction(mode, hi, x))
469                }
470            }
471            Family::Pert { lo, mode, hi } => {
472                let (a, b) = Family::pert_shape(lo, mode, hi);
473                beta_cdf(a, b, interval_fraction(lo, hi, x))
474            }
475        }
476    }
477
478    /// The value below which a share `p` of the distribution lies.
479    pub fn quantile(&self, p: f64) -> f64 {
480        let (lo, hi) = self.support();
481        if p <= 0.0 {
482            return lo;
483        }
484        if p >= 1.0 {
485            return hi;
486        }
487        match *self {
488            Family::Normal { mean, sd } => mean + sd * std_normal_quantile(p),
489            Family::Lognormal { mu, sigma } => crate::math::exp(mu + sigma * std_normal_quantile(p)),
490            Family::Uniform { lo, hi } => crate::stats::lerp(lo, hi, p),
491            Family::Exponential { rate } => -libm::log1p(-p) / rate,
492            Family::Triangular { lo, mode, hi } => {
493                let split = interval_fraction(lo, hi, mode);
494                if p <= split {
495                    crate::stats::lerp(lo, hi, libm::sqrt(p * split))
496                } else {
497                    crate::stats::lerp(hi, lo, libm::sqrt((1.0 - p) * (1.0 - split)))
498                }
499            }
500            Family::Beta { .. } | Family::Pert { .. } => invert(|x| self.cdf(x), p, lo, hi),
501            Family::Gamma { .. } => {
502                let mut top = self.mean() + 10.0 * libm::sqrt(self.variance());
503                while self.cdf(top) < p && top < f64::MAX / 4.0 {
504                    top *= 2.0;
505                }
506                invert(|x| self.cdf(x), p, 0.0, top)
507            }
508        }
509    }
510
511    /// One draw.
512    pub fn sample(&self, rng: &mut Rng) -> f64 {
513        match *self {
514            Family::Normal { mean, sd } => mean + sd * rng.normal(),
515            Family::Lognormal { mu, sigma } => crate::math::exp(mu + sigma * rng.normal()),
516            Family::Uniform { lo, hi } => crate::stats::lerp(lo, hi, rng.uniform()),
517            Family::Beta { a, b } => rng.beta(a, b),
518            Family::Gamma { shape, scale } => scale * rng.gamma(shape),
519            Family::Exponential { rate } => -libm::log(rng.open()) / rate,
520            Family::Triangular { .. } => self.quantile(rng.uniform()),
521            Family::Pert { lo, mode, hi } => {
522                let (a, b) = Family::pert_shape(lo, mode, hi);
523                crate::stats::lerp(lo, hi, rng.beta(a, b))
524            }
525        }
526    }
527
528    pub fn name(&self) -> &'static str {
529        match self {
530            Family::Normal { .. } => "normal",
531            Family::Lognormal { .. } => "lognormal",
532            Family::Uniform { .. } => "uniform",
533            Family::Beta { .. } => "beta",
534            Family::Gamma { .. } => "gamma",
535            Family::Exponential { .. } => "exponential",
536            Family::Triangular { .. } => "triangular",
537            Family::Pert { .. } => "pert",
538        }
539    }
540
541    /// The parameters, for hashing, ordering and display.
542    pub fn params(&self) -> Vec<f64> {
543        match *self {
544            Family::Normal { mean, sd } => vec![mean, sd],
545            Family::Lognormal { mu, sigma } => vec![mu, sigma],
546            Family::Uniform { lo, hi } => vec![lo, hi],
547            Family::Beta { a, b } => vec![a, b],
548            Family::Gamma { shape, scale } => vec![shape, scale],
549            Family::Exponential { rate } => vec![rate],
550            Family::Triangular { lo, mode, hi } | Family::Pert { lo, mode, hi } => vec![lo, mode, hi],
551        }
552    }
553}
554
555impl fmt::Display for Family {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        let params: Vec<String> = self
558            .params()
559            .iter()
560            .map(|x| {
561                format!("{:.4}", x)
562                    .trim_end_matches('0')
563                    .trim_end_matches('.')
564                    .to_string()
565            })
566            .collect();
567        write!(f, "{}({})", self.name(), params.join(", "))
568    }
569}
570
571// ── Special functions ────────────────────────────────────────────────────
572
573fn std_normal_pdf(z: f64) -> f64 {
574    crate::math::exp(-z * z / 2.0) / libm::sqrt(2.0 * PI)
575}
576
577pub fn std_normal_cdf(z: f64) -> f64 {
578    0.5 * libm::erfc(-z / SQRT_2)
579}
580
581/// Φ⁻¹(p): a rational approximation (Abramowitz and Stegun 26.2.23, error
582/// below 4.5e-4), polished by Newton's method on the exact CDF.
583pub fn std_normal_quantile(p: f64) -> f64 {
584    if p == 0.5 {
585        return 0.0;
586    }
587    if p <= 0.0 {
588        return f64::NEG_INFINITY;
589    }
590    if p >= 1.0 {
591        return f64::INFINITY;
592    }
593    let tail = |q: f64| {
594        let t = libm::sqrt(-2.0 * libm::log(q));
595        t - (2.515517 + 0.802853 * t + 0.010328 * t * t)
596            / (1.0 + 1.432788 * t + 0.189269 * t * t + 0.001308 * t * t * t)
597    };
598    let mut z = if p < 0.5 { -tail(p) } else { tail(1.0 - p) };
599    for _ in 0..6 {
600        let density = std_normal_pdf(z);
601        if density == 0.0 {
602            break;
603        }
604        let step = (std_normal_cdf(z) - p) / density;
605        z -= step;
606        if step.abs() < 1e-15 * z.abs().max(1.0) {
607            break;
608        }
609    }
610    z
611}
612
613fn beta_pdf(a: f64, b: f64, x: f64) -> f64 {
614    if !(0.0..=1.0).contains(&x) {
615        return 0.0;
616    }
617    if x == 0.0 || x == 1.0 {
618        // At an end, the density is infinite, 1/B(a, b), or zero, as the
619        // parameter for that end is below, at or above 1.
620        let edge = if x == 0.0 { a } else { b };
621        return if edge < 1.0 {
622            f64::INFINITY
623        } else if edge == 1.0 {
624            crate::math::exp(libm::lgamma(a + b) - libm::lgamma(a) - libm::lgamma(b))
625        } else {
626            0.0
627        };
628    }
629    crate::math::exp(
630        (a - 1.0) * libm::log(x) + (b - 1.0) * libm::log1p(-x) + libm::lgamma(a + b)
631            - libm::lgamma(a)
632            - libm::lgamma(b),
633    )
634}
635
636/// ln √(2π).
637const LN_SQRT_2PI: f64 = 0.918_938_533_204_672_7;
638
639/// ln B(a, b), the logarithm of the beta function. For large arguments,
640/// the log gammas it's made of are huge and nearly cancel, so it's computed
641/// from their Stirling series instead, as R's `lbeta` is.
642pub fn ln_beta(a: f64, b: f64) -> f64 {
643    let (p, q) = if a < b { (a, b) } else { (b, a) };
644    let share = p / (p + q);
645    if p >= 10.0 {
646        let rest = stirling_rest(p) + stirling_rest(q) - stirling_rest(p + q);
647        -0.5 * libm::log(q) + LN_SQRT_2PI + rest + (p - 0.5) * libm::log(share) + q * libm::log1p(-share)
648    } else if q >= 10.0 {
649        let rest = stirling_rest(q) - stirling_rest(p + q);
650        libm::lgamma(p) + rest + p - p * libm::log(p + q) + (q - 0.5) * libm::log1p(-share)
651    } else {
652        libm::lgamma(p) + libm::lgamma(q) - libm::lgamma(p + q)
653    }
654}
655
656/// ln Γ(x) minus Stirling's approximation (x − ½) ln x − x + ln √(2π), for
657/// x ≥ 10: the first seven terms of its series, which are exact to 10⁻¹⁶
658/// there.
659fn stirling_rest(x: f64) -> f64 {
660    let r = 1.0 / (x * x);
661    let series = 1.0 / 12.0
662        + r * (-1.0 / 360.0
663            + r * (1.0 / 1260.0 + r * (-1.0 / 1680.0 + r * (1.0 / 1188.0 + r * (-691.0 / 360_360.0 + r / 156.0)))));
664    series / x
665}
666
667/// The regularized incomplete beta function I_x(a, b), by its continued
668/// fraction (Numerical Recipes, section 6.4).
669pub fn beta_cdf(a: f64, b: f64, x: f64) -> f64 {
670    if x <= 0.0 {
671        return 0.0;
672    }
673    if x >= 1.0 {
674        return 1.0;
675    }
676    let front = crate::math::exp(
677        libm::lgamma(a + b) - libm::lgamma(a) - libm::lgamma(b) + a * libm::log(x) + b * libm::log1p(-x),
678    );
679    if x < (a + 1.0) / (a + b + 2.0) {
680        front * beta_fraction(a, b, x) / a
681    } else {
682        1.0 - front * beta_fraction(b, a, 1.0 - x) / b
683    }
684}
685
686fn beta_fraction(a: f64, b: f64, x: f64) -> f64 {
687    const TINY: f64 = 1e-300;
688    let (qab, qap, qam) = (a + b, a + 1.0, a - 1.0);
689    let mut c = 1.0;
690    let mut d = 1.0 - qab * x / qap;
691    if d.abs() < TINY {
692        d = TINY;
693    }
694    d = 1.0 / d;
695    let mut h = d;
696    for m in 1..=1000 {
697        let m = m as f64;
698        let m2 = 2.0 * m;
699        let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
700        d = 1.0 + aa * d;
701        if d.abs() < TINY {
702            d = TINY;
703        }
704        c = 1.0 + aa / c;
705        if c.abs() < TINY {
706            c = TINY;
707        }
708        d = 1.0 / d;
709        h *= d * c;
710        let aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
711        d = 1.0 + aa * d;
712        if d.abs() < TINY {
713            d = TINY;
714        }
715        c = 1.0 + aa / c;
716        if c.abs() < TINY {
717            c = TINY;
718        }
719        d = 1.0 / d;
720        let delta = d * c;
721        h *= delta;
722        if (delta - 1.0).abs() < 1e-16 {
723            break;
724        }
725    }
726    h
727}
728
729/// The regularized lower incomplete gamma function P(a, x): its series
730/// below a + 1, its continued fraction above (Numerical Recipes, 6.2).
731pub fn gamma_cdf(a: f64, x: f64) -> f64 {
732    if x == f64::INFINITY {
733        return 1.0;
734    }
735    if x <= 0.0 {
736        return 0.0;
737    }
738    let front = crate::math::exp(-x + a * libm::log(x) - libm::lgamma(a));
739    if x < a + 1.0 {
740        let (mut sum, mut term, mut n) = (1.0 / a, 1.0 / a, a);
741        for _ in 0..10_000 {
742            n += 1.0;
743            term *= x / n;
744            sum += term;
745            if term.abs() < sum.abs() * 1e-17 {
746                break;
747            }
748        }
749        (sum * front).min(1.0)
750    } else {
751        const TINY: f64 = 1e-300;
752        let mut b = x + 1.0 - a;
753        let mut c = 1.0 / TINY;
754        let mut d = 1.0 / b;
755        let mut h = d;
756        for i in 1..10_000 {
757            let i = i as f64;
758            let an = -i * (i - a);
759            b += 2.0;
760            d = an * d + b;
761            if d.abs() < TINY {
762                d = TINY;
763            }
764            c = b + an / c;
765            if c.abs() < TINY {
766                c = TINY;
767            }
768            d = 1.0 / d;
769            let delta = d * c;
770            h *= delta;
771            if (delta - 1.0).abs() < 1e-16 {
772                break;
773            }
774        }
775        (1.0 - front * h).max(0.0)
776    }
777}
778
779/// The x in `lo..hi` where a non-decreasing `f` reaches `p` (the smallest
780/// such x), by bisection.
781pub fn invert(f: impl Fn(f64) -> f64, p: f64, mut lo: f64, mut hi: f64) -> f64 {
782    if !lo.is_finite() || !hi.is_finite() {
783        return f64::NAN;
784    }
785    for _ in 0..300 {
786        let mid = crate::stats::midpoint(lo, hi);
787        if mid <= lo || mid >= hi {
788            break;
789        }
790        let cumulative = f(mid);
791        if !cumulative.is_finite() {
792            return f64::NAN;
793        }
794        if cumulative >= p {
795            hi = mid;
796        } else {
797            lo = mid;
798        }
799    }
800    hi
801}
802
803// ── Random numbers ───────────────────────────────────────────────────────
804
805/// xoshiro256++, seeded through SplitMix64.
806#[derive(Clone, Debug)]
807pub struct Rng {
808    s: [u64; 4],
809}
810
811/// SplitMix64's step and output function.
812const GOLDEN: u64 = 0x9E37_79B9_7F4A_7C15;
813
814pub(crate) fn mix(z: u64) -> u64 {
815    let z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
816    let z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
817    z ^ (z >> 31)
818}
819
820impl Rng {
821    pub fn new(seed: u64) -> Rng {
822        let mut z = seed;
823        let mut next = || {
824            z = z.wrapping_add(GOLDEN);
825            mix(z)
826        };
827        Rng {
828            s: [next(), next(), next(), next()],
829        }
830    }
831
832    /// The random numbers of the batch numbered `index`, for a run with this
833    /// seed. Every batch has a stream of its own, so batches can run in any
834    /// order and on any number of threads, and still get the same numbers
835    /// (docs/semantics.md, section 14).
836    pub fn stream(seed: u64, index: u64) -> Rng {
837        // The index-th output of SplitMix64 from the seed: different for
838        // every index.
839        Rng::new(mix(seed.wrapping_add(index.wrapping_add(1).wrapping_mul(GOLDEN))))
840    }
841
842    pub fn next_u64(&mut self) -> u64 {
843        let s = &mut self.s;
844        let result = s[0].wrapping_add(s[3]).rotate_left(23).wrapping_add(s[0]);
845        let t = s[1] << 17;
846        s[2] ^= s[0];
847        s[3] ^= s[1];
848        s[1] ^= s[2];
849        s[0] ^= s[3];
850        s[2] ^= t;
851        s[3] = s[3].rotate_left(45);
852        result
853    }
854
855    /// Uniform in [0, 1).
856    pub fn uniform(&mut self) -> f64 {
857        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
858    }
859
860    /// Uniform in (0, 1]: safe to take the logarithm of.
861    pub fn open(&mut self) -> f64 {
862        1.0 - self.uniform()
863    }
864
865    /// A standard normal draw (Box–Muller).
866    pub fn normal(&mut self) -> f64 {
867        let (u, v) = (self.open(), self.uniform());
868        libm::sqrt(-2.0 * libm::log(u)) * libm::cos(2.0 * PI * v)
869    }
870
871    /// A gamma draw with scale 1 (Marsaglia and Tsang, 2000).
872    pub fn gamma(&mut self, shape: f64) -> f64 {
873        if shape < 1.0 {
874            let u = self.open();
875            return self.gamma(shape + 1.0) * libm::pow(u, 1.0 / shape);
876        }
877        let d = shape - 1.0 / 3.0;
878        let c = 1.0 / libm::sqrt(9.0 * d);
879        loop {
880            let x = self.normal();
881            let v = 1.0 + c * x;
882            if v <= 0.0 {
883                continue;
884            }
885            let v = v * v * v;
886            let u = self.open();
887            if u < 1.0 - 0.0331 * x.powi(4) || libm::log(u) < 0.5 * x * x + d * (1.0 - v + libm::log(v)) {
888                return d * v;
889            }
890        }
891    }
892
893    pub fn beta(&mut self, a: f64, b: f64) -> f64 {
894        for _ in 0..16 {
895            let x = self.gamma(a);
896            let y = self.gamma(b);
897            if x + y > 0.0 {
898                return if x == 0.0 {
899                    0.0
900                } else if y == 0.0 {
901                    1.0
902                } else {
903                    ratio(x, y)
904                };
905            }
906        }
907        // Both draws round to zero only for tiny parameters, where the
908        // distribution is nearly all at 0 and 1.
909        if self.uniform() < ratio(a, b) { 1.0 } else { 0.0 }
910    }
911
912    /// An index chosen with probability proportional to `weights`, or `None`
913    /// if they're all zero.
914    pub fn choose(&mut self, weights: impl Iterator<Item = f64> + Clone) -> Option<usize> {
915        let total: f64 = weights.clone().sum();
916        if total <= 0.0 {
917            return None;
918        }
919        let target = self.uniform() * total;
920        let mut acc = 0.0;
921        let mut last = None;
922        for (i, w) in weights.enumerate() {
923            if w <= 0.0 {
924                continue;
925            }
926            acc += w;
927            last = Some(i);
928            if target < acc {
929                return Some(i);
930            }
931        }
932        last
933    }
934}
935
936// ── Mixtures ─────────────────────────────────────────────────────────────
937
938/// A part of a mixture: a number, or a continuous distribution.
939#[derive(Clone, Debug)]
940pub enum Part {
941    Point(f64),
942    Continuous(Family),
943    Analytic(crate::analytic::Analytic),
944}
945
946/// Moments, CDF and quantiles of a mixture of numbers and continuous
947/// distributions, each with its probability.
948#[derive(Clone, Debug)]
949pub struct Mixture {
950    pub parts: Vec<(Part, f64)>,
951}
952
953impl Mixture {
954    fn total(&self) -> f64 {
955        crate::stats::sum(self.parts.iter().map(|(_, p)| *p))
956    }
957
958    pub fn mean(&self) -> f64 {
959        crate::stats::weighted_mean(self.parts.iter().map(|(part, p)| {
960            (
961                match part {
962                    Part::Point(x) => *x,
963                    Part::Continuous(f) => f.mean(),
964                    // NaN where an outcome has no mean from formulas.
965                    Part::Analytic(a) => a.moments().map_or(f64::NAN, |m| m.0),
966                },
967                *p,
968            )
969        }))
970    }
971
972    pub fn variance(&self) -> f64 {
973        let sd = self.sd();
974        sd * sd
975    }
976
977    pub fn sd(&self) -> f64 {
978        crate::stats::weighted_sd(self.parts.iter().map(|(part, p)| {
979            let (m, sd) = match part {
980                Part::Point(x) => (*x, 0.0),
981                Part::Continuous(f) => (f.mean(), f.sd()),
982                Part::Analytic(a) => (a.moments().map_or(f64::NAN, |m| m.0), a.sd().unwrap_or(f64::NAN)),
983            };
984            (m, sd, *p)
985        }))
986    }
987
988    pub fn cdf(&self, x: f64) -> f64 {
989        let below = crate::stats::sum(self.parts.iter().map(|(part, p)| {
990            p * match part {
991                Part::Point(v) => {
992                    if *v <= x {
993                        1.0
994                    } else {
995                        0.0
996                    }
997                }
998                Part::Continuous(f) => f.cdf(x),
999                Part::Analytic(a) => a.cdf(x),
1000            }
1001        }));
1002        below / self.total()
1003    }
1004
1005    /// Distinguish an actual gap in the support at half the mass from a CDF
1006    /// that merely rounds to 0.5. This also handles disconnected analytic
1007    /// domains in report summaries without perturbing the requested quantile.
1008    pub fn median_bounds(&self) -> (f64, f64) {
1009        let mut intervals = Vec::new();
1010        for (part, weight) in &self.parts {
1011            if *weight <= 0.0 {
1012                continue;
1013            }
1014            match part {
1015                Part::Point(x) => intervals.push((*x, *x, *weight)),
1016                Part::Continuous(f) => {
1017                    let (lo, hi) = f.support();
1018                    intervals.push((lo, hi, *weight));
1019                }
1020                Part::Analytic(a) => {
1021                    let total = a.measure(&a.domain);
1022                    for (lo, hi, f) in a.segments() {
1023                        let (x, y) = f.ends(&a.family, lo, hi);
1024                        let share = a.measure(&crate::analytic::Domain(vec![(lo, hi)])) / total;
1025                        intervals.push((x.min(y), x.max(y), weight * share));
1026                    }
1027                }
1028            }
1029        }
1030        intervals.sort_by(|a, b| a.0.total_cmp(&b.0));
1031        let total = crate::stats::sum(intervals.iter().map(|x| x.2));
1032        let mut acc = crate::stats::Sum::default();
1033        let mut end = f64::NEG_INFINITY;
1034        for (lo, hi, weight) in intervals {
1035            if lo > end && acc.value() > 0.0 && crate::stats::half_split(acc.value(), total) {
1036                return (end, lo);
1037            }
1038            end = end.max(hi);
1039            acc.add(weight);
1040        }
1041        let x = self.quantile(0.5);
1042        (x, x)
1043    }
1044
1045    pub fn median(&self) -> f64 {
1046        let (lo, hi) = self.median_bounds();
1047        crate::stats::midpoint(lo, hi)
1048    }
1049
1050    pub fn quantile(&self, q: f64) -> f64 {
1051        let ends = |q: f64| {
1052            self.parts.iter().map(move |(part, _)| match part {
1053                Part::Point(x) => *x,
1054                Part::Continuous(f) => f.quantile(q),
1055                Part::Analytic(a) => a.quantile(q),
1056            })
1057        };
1058        if self.parts.len() == 1 {
1059            return match &self.parts[0].0 {
1060                Part::Point(x) => *x,
1061                Part::Continuous(f) => f.quantile(q),
1062                Part::Analytic(a) => a.quantile(q),
1063            };
1064        }
1065        if q <= 0.0 {
1066            return ends(0.0).fold(f64::INFINITY, f64::min);
1067        }
1068        if q >= 1.0 {
1069            return ends(1.0).fold(f64::NEG_INFINITY, f64::max);
1070        }
1071        let lo = ends(q.min(1e-12)).fold(f64::INFINITY, f64::min).max(-f64::MAX);
1072        let hi = ends(q.max(1.0 - 1e-12)).fold(f64::NEG_INFINITY, f64::max).min(f64::MAX);
1073        if lo >= hi {
1074            return lo;
1075        }
1076        if self.cdf(hi) < q {
1077            return f64::INFINITY;
1078        }
1079        invert(|x| self.cdf(x), q, (lo - 1e-9 * lo.abs().max(1.0)).max(-f64::MAX), hi)
1080    }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085    use super::*;
1086
1087    fn close(a: f64, b: f64, tol: f64) {
1088        assert!((a - b).abs() <= tol * (1.0 + b.abs()), "{a} vs {b}");
1089    }
1090
1091    #[test]
1092    fn normal_quantiles() {
1093        close(std_normal_cdf(Z95), 0.95, 1e-15);
1094        close(std_normal_quantile(0.95), Z95, 1e-14);
1095        close(std_normal_quantile(0.5), 0.0, 1e-15);
1096        close(std_normal_quantile(0.025), -1.9599639845400545, 1e-13);
1097        close(std_normal_quantile(1e-10), -6.361340902404056, 1e-10);
1098        close(std_normal_cdf(-1.959963984540054), 0.025, 1e-14);
1099    }
1100
1101    #[test]
1102    fn estimates_have_the_right_intervals() {
1103        let e = Family::estimate(3.0, 7.0).unwrap();
1104        close(e.quantile(0.05), 3.0, 1e-12);
1105        close(e.quantile(0.95), 7.0, 1e-12);
1106        let r = Family::normal_range(-0.08, 0.0).unwrap();
1107        close(r.quantile(0.05), -0.08, 1e-12);
1108        close(r.cdf(0.0), 0.95, 1e-12);
1109        assert!(Family::estimate(-1.0, 3.0).is_err());
1110        assert!(Family::estimate(3.0, 1.0).is_err());
1111    }
1112
1113    #[test]
1114    fn incomplete_functions() {
1115        // I_0.5(2, 3) = 11/16; P(1, x) = 1 − e^−x; P(1/2, x) = erf(√x).
1116        close(beta_cdf(2.0, 3.0, 0.5), 11.0 / 16.0, 1e-14);
1117        close(beta_cdf(1.0, 1.0, 0.3), 0.3, 1e-14);
1118        close(gamma_cdf(1.0, 2.0), 1.0 - (-2.0f64).exp(), 1e-14);
1119        close(gamma_cdf(0.5, 3.0), libm::erf(3.0f64.sqrt()), 1e-13);
1120        close(gamma_cdf(10.0, 30.0), 0.9999928782491372, 1e-13);
1121        let b = Family::beta(2.0, 40.0).unwrap();
1122        close(b.quantile(b.cdf(0.05)), 0.05, 1e-10);
1123        let g = Family::gamma(3.0, 2.0).unwrap();
1124        close(g.quantile(0.5), 5.348120627447122, 1e-10);
1125    }
1126
1127    #[test]
1128    fn densities_integrate_to_their_cdfs() {
1129        let families = [
1130            Family::normal(1.0, 2.0).unwrap(),
1131            Family::lognormal(0.5, 0.3).unwrap(),
1132            Family::uniform(-1.0, 3.0).unwrap(),
1133            Family::beta(2.5, 4.0).unwrap(),
1134            Family::gamma(2.0, 1.5).unwrap(),
1135            Family::exponential(0.7).unwrap(),
1136            Family::triangular(0.0, 1.0, 4.0).unwrap(),
1137            Family::pert(1.0, 2.0, 6.0).unwrap(),
1138        ];
1139        for f in families {
1140            let (a, b) = (f.quantile(0.1), f.quantile(0.8));
1141            let n = 20_000;
1142            let h = (b - a) / n as f64;
1143            let integral: f64 = (0..n).map(|i| f.pdf(a + (i as f64 + 0.5) * h) * h).sum();
1144            close(integral, 0.7, 1e-6);
1145            close(f.cdf(b) - f.cdf(a), 0.7, 1e-9);
1146        }
1147    }
1148
1149    #[test]
1150    fn log_densities_agree_and_reach_far_tails() {
1151        let families = [
1152            Family::normal(1.0, 2.0).unwrap(),
1153            Family::lognormal(0.5, 0.3).unwrap(),
1154            Family::uniform(-1.0, 3.0).unwrap(),
1155            Family::beta(2.5, 4.0).unwrap(),
1156            Family::gamma(2.0, 1.5).unwrap(),
1157            Family::exponential(0.7).unwrap(),
1158            Family::triangular(0.0, 1.0, 4.0).unwrap(),
1159            Family::pert(1.0, 2.0, 6.0).unwrap(),
1160        ];
1161        for f in families {
1162            for q in [0.01, 0.3, 0.5, 0.9, 0.999] {
1163                let x = f.quantile(q);
1164                close(f.ln_pdf(x), libm::log(f.pdf(x)), 1e-12);
1165            }
1166            if f.support().0 > -10.0 {
1167                assert_eq!(f.ln_pdf(-10.0), f64::NEG_INFINITY, "{f}");
1168            }
1169        }
1170        // Where the density underflows, its logarithm doesn't.
1171        let n = Family::normal(0.0, 1.0).unwrap();
1172        assert_eq!(n.pdf(50.0), 0.0);
1173        close(n.ln_pdf(50.0), -1250.0 - LN_SQRT_2PI, 1e-15);
1174        let b = Family::beta(2000.0, 2000.0).unwrap();
1175        assert!(b.ln_pdf(0.01).is_finite());
1176    }
1177
1178    /// Kolmogorov–Smirnov: the draws follow each distribution's CDF.
1179    #[test]
1180    fn draws_follow_the_distributions() {
1181        let families = [
1182            Family::normal(1.0, 2.0).unwrap(),
1183            Family::lognormal(0.5, 0.3).unwrap(),
1184            Family::uniform(-1.0, 3.0).unwrap(),
1185            Family::beta(2.0, 40.0).unwrap(),
1186            Family::beta(0.5, 0.5).unwrap(),
1187            Family::gamma(0.4, 1.5).unwrap(),
1188            Family::gamma(7.0, 0.5).unwrap(),
1189            Family::exponential(0.7).unwrap(),
1190            Family::triangular(0.0, 1.0, 4.0).unwrap(),
1191            Family::pert(1.0, 2.0, 6.0).unwrap(),
1192            Family::estimate(60.0, 150.0).unwrap(),
1193        ];
1194        let mut rng = Rng::new(42);
1195        let n = 20_000;
1196        for f in families {
1197            let mut xs: Vec<f64> = (0..n).map(|_| f.sample(&mut rng)).collect();
1198            xs.sort_by(f64::total_cmp);
1199            let d = xs
1200                .iter()
1201                .enumerate()
1202                .map(|(i, x)| {
1203                    let c = f.cdf(*x);
1204                    (c - i as f64 / n as f64)
1205                        .abs()
1206                        .max((c - (i + 1) as f64 / n as f64).abs())
1207                })
1208                .fold(0.0, f64::max);
1209            // The 0.001 critical value is 1.95 / √n.
1210            assert!(d < 1.95 / (n as f64).sqrt(), "{f}: D = {d}");
1211            let mean = xs.iter().sum::<f64>() / n as f64;
1212            close(mean, f.mean(), 6.0 * f.variance().sqrt() / (n as f64).sqrt());
1213        }
1214    }
1215
1216    #[test]
1217    fn batches_have_streams_of_their_own() {
1218        let firsts: Vec<u64> = (0..1000).map(|i| Rng::stream(11, i).next_u64()).collect();
1219        let mut distinct = firsts.clone();
1220        distinct.sort();
1221        distinct.dedup();
1222        assert_eq!(distinct.len(), firsts.len());
1223        assert_eq!(Rng::stream(11, 3).next_u64(), Rng::stream(11, 3).next_u64());
1224        assert_ne!(Rng::stream(11, 3).next_u64(), Rng::stream(12, 3).next_u64());
1225    }
1226
1227    #[test]
1228    fn seeds_repeat() {
1229        let (mut a, mut b) = (Rng::new(7), Rng::new(7));
1230        for _ in 0..100 {
1231            assert_eq!(a.next_u64(), b.next_u64());
1232        }
1233        assert_ne!(Rng::new(7).next_u64(), Rng::new(8).next_u64());
1234    }
1235
1236    #[test]
1237    fn mixtures() {
1238        let m = Mixture {
1239            parts: vec![
1240                (Part::Point(0.0), 0.5),
1241                (Part::Continuous(Family::uniform(1.0, 3.0).unwrap()), 0.5),
1242            ],
1243        };
1244        close(m.mean(), 1.0, 1e-12);
1245        close(m.cdf(2.0), 0.75, 1e-12);
1246        close(m.quantile(0.75), 2.0, 1e-9);
1247        close(m.quantile(0.25), 0.0, 1e-9);
1248    }
1249}