Skip to main content

probl_engine/
dist.rs

1//! Finite distributions: dice, choices, counts, and the results of computing
2//! with them.
3
4use crate::continuous::Rng;
5use crate::error::{OpError, OpResult};
6use crate::value::Value;
7use std::cmp::Ordering;
8use std::collections::BTreeMap;
9use std::hash::{Hash, Hasher};
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as Atomic};
12
13/// Outcomes below this probability are dropped from infinite supports.
14const TAIL: f64 = 1e-18;
15
16#[derive(Clone, Debug)]
17pub struct Dist {
18    /// Sorted by value, each value once, every weight positive.
19    pub outcomes: Vec<(Value, f64)>,
20    /// Probability not represented by `outcomes`: a tail cut off to keep the
21    /// support finite, or weight a `simulate` left unresolved. Drawing counts
22    /// it as unresolved.
23    pub missing: f64,
24    /// The law the outcomes were listed from, if they were: `pmf` and `cdf`
25    /// ask it, rather than the outcomes, which leave out the tail. It isn't
26    /// part of the distribution's identity, which its outcomes are.
27    pub law: Option<Box<Counts>>,
28}
29
30/// Work taken from a shared budget at a time: threads rarely touch it, and
31/// between them hold back little of it.
32const WORK_CHUNK: u64 = 1 << 14;
33
34/// Limits on building distributions, shared by everything in a run.
35#[derive(Clone, Debug)]
36pub struct Budget {
37    /// Maximum bits in the magnitude of a single integer.
38    pub max_integer_bits: u64,
39    /// Cumulative allowance for produced large integer payloads, shared by runs.
40    pub integer_bytes_left: Arc<AtomicU64>,
41    pub max_string_bytes: usize,
42    /// Cumulative string payload allowance, shared by sampled workers.
43    pub string_bytes_left: Arc<AtomicU64>,
44    pub cancel: Option<Arc<AtomicBool>>,
45    /// The most outcomes one distribution (or combination) may have.
46    pub max_outcomes: usize,
47    /// The most elements a collection built by the program may have.
48    pub max_collection: usize,
49    /// Units of work left: world-steps plus outcomes computed.
50    pub work_left: u64,
51    /// Work shared with other threads, which `work_left` is topped up from
52    /// (when batches of runs are sampled in parallel).
53    pub shared: Option<Arc<AtomicU64>>,
54}
55
56impl Budget {
57    pub fn unlimited() -> Budget {
58        Budget {
59            max_integer_bits: probl_number::MAX_INTEGER_BITS,
60            integer_bytes_left: Arc::new(AtomicU64::new(u64::MAX)),
61            max_string_bytes: usize::MAX,
62            string_bytes_left: Arc::new(AtomicU64::new(u64::MAX)),
63            cancel: None,
64            max_outcomes: usize::MAX,
65            max_collection: usize::MAX,
66            work_left: u64::MAX,
67            shared: None,
68        }
69    }
70
71    pub fn integer_bits(&self, bits: u64) -> OpResult<()> {
72        let limit = self.max_integer_bits.min(probl_number::MAX_INTEGER_BITS);
73        if bits > limit {
74            return Err(OpError::limit(format!(
75                "integer size exceeds the limit of {limit} bits"
76            )));
77        }
78        Ok(())
79    }
80
81    pub fn string_size(&self, bytes: usize) -> OpResult<()> {
82        if bytes > self.max_string_bytes {
83            return Err(OpError::limit(format!(
84                "string size exceeds the limit of {} UTF-8 bytes",
85                self.max_string_bytes
86            )));
87        }
88        Ok(())
89    }
90
91    /// Charge a scan before doing it, in units of 64 UTF-8 bytes.
92    pub fn string_work(&mut self, s: &str) -> OpResult<()> {
93        self.string_size(s.len())?;
94        self.work((s.len() as u64).div_ceil(64).max(1))
95    }
96
97    /// Reserve payload bytes before allocation or growing a text builder.
98    // `try_update`, its new name, needs a newer Rust than the crates support (1.85).
99    #[allow(deprecated)]
100    pub fn string_allocation(&self, bytes: usize) -> OpResult<()> {
101        self.string_size(bytes)?;
102        self.string_bytes_left
103            .fetch_update(Atomic::Relaxed, Atomic::Relaxed, |left| left.checked_sub(bytes as u64))
104            .map(|_| ())
105            .map_err(|_| OpError::limit("the run used up its string memory allowance"))
106    }
107
108    /// Reserve before materializing a collection, or after a single bounded
109    /// result. Conservative: shared results may be charged again; no refunds.
110    // `try_update`, its new name, needs a newer Rust than the crates support (1.85).
111    #[allow(deprecated)]
112    pub fn integer_allocation(&self, bits: u64, count: u64) -> OpResult<()> {
113        self.integer_bits(bits)?;
114        if bits <= 63 || count == 0 {
115            return Ok(());
116        }
117        let bytes = bits
118            .div_ceil(64)
119            .saturating_mul(8)
120            .saturating_add(48)
121            .saturating_mul(count);
122        self.integer_bytes_left
123            .fetch_update(Atomic::Relaxed, Atomic::Relaxed, |left| left.checked_sub(bytes))
124            .map(|_| ())
125            .map_err(|_| OpError::limit("the run used up its large integer memory allowance"))
126    }
127
128    /// Charge before arithmetic, including copying and processing large operands.
129    pub fn integer_work(
130        &mut self,
131        a: &probl_number::Integer,
132        b: &probl_number::Integer,
133        quadratic: bool,
134    ) -> OpResult<()> {
135        self.integer_bits(a.bits())?;
136        self.integer_bits(b.bits())?;
137        let x = a.bits().div_ceil(64).max(1);
138        let y = b.bits().div_ceil(64).max(1);
139        self.work(if quadratic { x.saturating_mul(y) } else { x.max(y) })
140    }
141
142    /// Check that a distribution with `n` outcomes may be built.
143    pub fn outcomes(&self, n: u128) -> OpResult<()> {
144        if n > self.max_outcomes as u128 {
145            let shown = if n > 1_000_000_000_000 {
146                "more than 10¹²".to_string()
147            } else {
148                n.to_string()
149            };
150            return Err(OpError::limit(format!(
151                "a distribution with {shown} outcomes is over the limit of {}",
152                self.max_outcomes
153            )));
154        }
155        Ok(())
156    }
157
158    /// Check that a collection with `n` elements may be built.
159    pub fn collection(&self, n: u128) -> OpResult<()> {
160        if n > self.max_collection as u128 {
161            return Err(OpError::limit(format!(
162                "a collection with {n} elements is over the limit of {}",
163                self.max_collection
164            )));
165        }
166        Ok(())
167    }
168
169    /// Spend `n` units of work.
170    pub fn work(&mut self, n: u64) -> OpResult<()> {
171        if self.cancel.as_ref().is_some_and(|c| c.load(Atomic::Relaxed)) {
172            return Err(OpError::limit("the run was cancelled"));
173        }
174        if n > self.work_left && !self.top_up(n - self.work_left) {
175            self.work_left = 0;
176            return Err(OpError::limit("the run used up its work budget"));
177        }
178        self.work_left -= n;
179        Ok(())
180    }
181
182    /// Take at least `need` units from the shared budget, if it has them.
183    // `try_update`, its new name, needs a newer Rust than the crates support (1.85).
184    #[allow(deprecated)]
185    fn top_up(&mut self, need: u64) -> bool {
186        let Some(shared) = &self.shared else {
187            return false;
188        };
189        let want = need.max(WORK_CHUNK);
190        let taken = shared.fetch_update(Atomic::Relaxed, Atomic::Relaxed, |left| {
191            (left >= need).then(|| left - want.min(left))
192        });
193        match taken {
194            Ok(left) => {
195                self.work_left += want.min(left);
196                true
197            }
198            Err(_) => false,
199        }
200    }
201
202    /// Give the work not spent back to the shared budget.
203    pub fn give_back(&mut self) {
204        if let Some(shared) = &self.shared {
205            shared.fetch_add(std::mem::take(&mut self.work_left), Atomic::Relaxed);
206        }
207    }
208}
209
210impl Dist {
211    pub fn point(v: Value) -> Dist {
212        Dist {
213            outcomes: vec![(v, 1.0)],
214            missing: 0.0,
215            law: None,
216        }
217    }
218
219    /// Build from (value, weight) pairs in any order, whose weights add up
220    /// to `1 - missing`; equal values are merged.
221    ///
222    /// The weights are rescaled to add up to exactly that, which removes the
223    /// rounding of the sums that produced them: six sixths of `d6 > 0` are
224    /// 0.9999999999999999, but the distribution is certainly `true`. Without
225    /// this, a certain condition would leave a branch of weight 1e-16.
226    pub fn from_pairs(mut pairs: Vec<(Value, f64)>, missing: f64) -> Dist {
227        pairs.retain(|(_, w)| *w > 0.0);
228        pairs.sort_by(|a, b| a.0.cmp(&b.0));
229        let mut outcomes: Vec<(Value, f64)> = Vec::with_capacity(pairs.len());
230        for (v, w) in pairs {
231            match outcomes.last_mut() {
232                Some((last, total)) if *last == v => *total += w,
233                _ => outcomes.push((v, w)),
234            }
235        }
236        Dist::normalized(outcomes, missing)
237    }
238
239    /// Like `from_pairs`, for pairs already sorted by value, each value once.
240    fn from_sorted(mut pairs: Vec<(Value, f64)>, missing: f64) -> Dist {
241        debug_assert!(pairs.windows(2).all(|p| p[0].0 < p[1].0), "unsorted outcomes");
242        pairs.retain(|(_, w)| *w > 0.0);
243        Dist::normalized(pairs, missing)
244    }
245
246    fn normalized(mut outcomes: Vec<(Value, f64)>, missing: f64) -> Dist {
247        let target = 1.0 - missing;
248        let total = crate::stats::sum(outcomes.iter().map(|(_, w)| *w));
249        if let [(_, w)] = outcomes.as_mut_slice() {
250            *w = target;
251        } else if target > 0.0 && total > 0.0 && total != target {
252            let scale = target / total;
253            for (_, w) in &mut outcomes {
254                *w *= scale;
255            }
256        }
257        Dist {
258            outcomes,
259            missing,
260            law: None,
261        }
262    }
263
264    pub fn into_value(self) -> Value {
265        Value::Dist(Arc::new(self))
266    }
267
268    pub fn uniform(values: Vec<Value>) -> Dist {
269        let p = 1.0 / values.len() as f64;
270        Dist::from_pairs(values.into_iter().map(|v| (v, p)).collect(), 0.0)
271    }
272
273    /// `true` with probability `p`.
274    pub fn bernoulli(p: f64) -> Dist {
275        Dist::from_sorted(vec![(Value::Bool(false), 1.0 - p), (Value::Bool(true), p)], 0.0)
276    }
277
278    /// The sum of `count` dice with `sides` sides.
279    pub fn dice(count: u32, sides: u32, budget: &mut Budget) -> OpResult<Dist> {
280        let support = count as u128 * (sides as u128 - 1) + 1;
281        budget.outcomes(support)?;
282        budget.work((count as u128 * support * sides as u128).min(u64::MAX as u128) as u64)?;
283        let mut sums = vec![1.0];
284        let p = 1.0 / sides as f64;
285        for _ in 0..count {
286            let mut next = vec![0.0; sums.len() + sides as usize];
287            for (s, w) in sums.iter().enumerate() {
288                if *w == 0.0 {
289                    continue;
290                }
291                for face in 1..=sides as usize {
292                    next[s + face] += w * p;
293                }
294            }
295            sums = next;
296        }
297        let pairs = sums
298            .into_iter()
299            .enumerate()
300            .filter(|(_, w)| *w > 0.0)
301            .map(|(s, w)| (Value::Int((s as i64).into()), w))
302            .collect();
303        Ok(Dist::from_sorted(pairs, 0.0))
304    }
305
306    pub fn binomial(n: u64, p: f64, budget: &mut Budget) -> OpResult<Dist> {
307        if p <= 0.0 {
308            return Ok(Dist::point(Value::Int(0.into())));
309        }
310        if p >= 1.0 {
311            return Ok(Dist::point(Value::Int((n as i64).into())));
312        }
313        let odds = p / (1.0 - p);
314        let mode = (((n as f64) + 1.0) * p).floor().min(n as f64) as u64;
315        walk_from_mode(
316            mode,
317            n,
318            |k| (n - k) as f64 / (k + 1) as f64 * odds,
319            |k| k as f64 / (n - k + 1) as f64 / odds,
320            budget,
321        )
322    }
323
324    pub fn poisson(rate: f64, budget: &mut Budget) -> OpResult<Dist> {
325        if rate <= 0.0 {
326            return Ok(Dist::point(Value::Int(0.into())));
327        }
328        walk_from_mode(
329            rate.floor() as u64,
330            u64::MAX,
331            |k| rate / (k + 1) as f64,
332            |k| k as f64 / rate,
333            budget,
334        )
335    }
336
337    /// Number of tries up to and including the first success.
338    pub fn geometric(p: f64, budget: &mut Budget) -> OpResult<Dist> {
339        if p >= 1.0 {
340            return Ok(Dist::point(Value::Int(1.into())));
341        }
342        // Outcomes until the tail (1 - p)^k drops below the threshold.
343        let count = (libm::log(TAIL) / libm::log(1.0 - p)).ceil();
344        budget.outcomes(if count.is_finite() { count as u128 } else { u128::MAX })?;
345        budget.work(count as u64)?;
346        let mut pairs = Vec::with_capacity(count as usize);
347        let mut tail = 1.0;
348        let mut k: i64 = 1;
349        while tail >= TAIL {
350            pairs.push((Value::Int(k.into()), tail * p));
351            tail *= 1.0 - p;
352            k += 1;
353        }
354        Ok(Dist::from_sorted(pairs, tail))
355    }
356
357    /// The dice of `count` rolls of `die`, sorted from highest to lowest.
358    pub fn pool(count: u32, die: &Dist, budget: &mut Budget) -> OpResult<Dist> {
359        // As many pools as multisets of `count` faces.
360        let faces = die.outcomes.len() as u128;
361        budget.outcomes(multisets(faces, count as u128))?;
362        let mut pools: BTreeMap<Vec<Value>, f64> = BTreeMap::new();
363        pools.insert(Vec::new(), 1.0);
364        for _ in 0..count {
365            budget.work(pools.len() as u64 * faces as u64)?;
366            let mut next: BTreeMap<Vec<Value>, f64> = BTreeMap::new();
367            for (pool, w) in &pools {
368                for (face, p) in &die.outcomes {
369                    let mut grown = pool.clone();
370                    let at = grown.iter().position(|v| v < face).unwrap_or(grown.len());
371                    grown.insert(at, face.clone());
372                    *next.entry(grown).or_insert(0.0) += w * p;
373                }
374            }
375            pools = next;
376        }
377        let pairs = pools.into_iter().map(|(pool, w)| (Value::list(pool), w)).collect();
378        // Each die independently misses `die.missing` of its probability.
379        let missing = -libm::expm1(count as f64 * libm::log1p(-die.missing));
380        Ok(Dist::from_pairs(pairs, missing))
381    }
382
383    pub fn total(&self) -> f64 {
384        crate::stats::sum(self.outcomes.iter().map(|(_, w)| *w))
385    }
386
387    /// For a distribution of facts: the probabilities of `true` and `false`.
388    pub fn truth(&self) -> Option<(f64, f64)> {
389        let (mut yes, mut no) = (0.0, 0.0);
390        for (v, w) in &self.outcomes {
391            match v {
392                Value::Bool(true) => yes += w,
393                Value::Bool(false) => no += w,
394                _ => return None,
395            }
396        }
397        Some((yes, no))
398    }
399
400    fn numbers(&self) -> Option<Vec<(f64, f64)>> {
401        self.outcomes
402            .iter()
403            .map(|(v, w)| match v {
404                Value::Bool(_) => None,
405                _ => v.as_f64().map(|x| (x, *w)),
406            })
407            .collect()
408    }
409
410    /// The mean of the resolved outcomes.
411    pub fn mean(&self) -> Option<f64> {
412        let nums = self.numbers()?;
413        Some(crate::stats::weighted_mean(nums.iter().copied()))
414    }
415
416    pub fn variance(&self) -> Option<f64> {
417        let sd = self.sd()?;
418        Some(sd * sd)
419    }
420
421    pub fn sd(&self) -> Option<f64> {
422        let nums = self.numbers()?;
423        Some(crate::stats::weighted_sd(nums.iter().map(|(x, w)| (*x, 0.0, *w))))
424    }
425
426    /// Quantile in storage order. Language queries first build a population in
427    /// language order, which can differ for nested mixed numeric values.
428    pub fn quantile(&self, q: f64) -> Option<Value> {
429        crate::stats::quantile(&self.outcomes, q).cloned()
430    }
431}
432
433/// `binomial`, `poisson` or `geometric` by their parameters: their
434/// probabilities and draws, without listing their outcomes. Sampling uses
435/// them (docs/semantics.md, section 14).
436#[derive(Clone, Copy, Debug)]
437pub enum Counts {
438    Binomial {
439        n: u64,
440        p: f64,
441    },
442    Poisson {
443        rate: f64,
444    },
445    /// Tries up to and including the first success.
446    Geometric {
447        p: f64,
448    },
449}
450
451impl Counts {
452    /// Whether a draw can walk out from the mode: the spread is small enough
453    /// for that to take a bounded number of steps. Otherwise listing the
454    /// outcomes, which is budgeted, fails cleanly.
455    pub fn direct(&self) -> bool {
456        match *self {
457            Counts::Binomial { n, p } => (n as f64) * p * (1.0 - p) <= 1e10,
458            Counts::Poisson { rate } => rate <= 1e10,
459            Counts::Geometric { .. } => true,
460        }
461    }
462
463    /// The distribution with its outcomes listed, and this law.
464    pub fn list(&self, budget: &mut Budget) -> OpResult<Dist> {
465        let mut d = match *self {
466            Counts::Binomial { n, p } => Dist::binomial(n, p, budget),
467            Counts::Poisson { rate } => Dist::poisson(rate, budget),
468            Counts::Geometric { p } => Dist::geometric(p, budget),
469        }?;
470        d.law = Some(Box::new(*self));
471        Ok(d)
472    }
473
474    /// About how many outcomes listing it would take: until the weight is
475    /// below `TAIL` of the mode's, some 9 standard deviations each side.
476    pub fn listed_size(&self) -> f64 {
477        match *self {
478            Counts::Geometric { p } if p < 1.0 => (libm::log(TAIL) / libm::log1p(-p)).ceil(),
479            Counts::Geometric { .. } => 1.0,
480            _ => 18.2 * libm::sqrt(self.variance()) + 1.0,
481        }
482    }
483
484    pub fn mean(&self) -> f64 {
485        match *self {
486            Counts::Binomial { n, p } => n as f64 * p,
487            Counts::Poisson { rate } => rate,
488            Counts::Geometric { p } => 1.0 / p,
489        }
490    }
491
492    pub fn variance(&self) -> f64 {
493        match *self {
494            Counts::Binomial { n, p } => n as f64 * p * (1.0 - p),
495            Counts::Poisson { rate } => rate,
496            Counts::Geometric { p } => (1.0 - p) / (p * p),
497        }
498    }
499
500    /// The smallest k with P(X ≤ k) ≥ q, for 0 < q < 1.
501    pub fn quantile(&self, q: f64) -> f64 {
502        if let Counts::Geometric { p } = *self {
503            if p >= 1.0 {
504                return 1.0;
505            }
506            // 1 − (1 − p)^k ≥ q, then a step back or forward for rounding.
507            let k = (libm::log1p(-q) / libm::log1p(-p)).ceil().max(1.0);
508            return if k > 1.0 && self.cdf(k - 1.0) >= q {
509                k - 1.0
510            } else if self.cdf(k) < q {
511                k + 1.0
512            } else {
513                k
514            };
515        }
516        let (mut lo, mut hi) = (-1.0, self.mean() + 10.0 * libm::sqrt(self.variance()) + 10.0);
517        while self.cdf(hi) < q {
518            hi *= 2.0;
519        }
520        // P(X ≤ lo) < q ≤ P(X ≤ hi).
521        while hi - lo > 1.0 {
522            let mid = ((lo + hi) / 2.0).floor();
523            if self.cdf(mid) >= q { hi = mid } else { lo = mid }
524        }
525        hi
526    }
527
528    /// What tells two laws apart.
529    pub fn key(&self) -> (u8, u64, u64) {
530        match *self {
531            Counts::Binomial { n, p } => (0, n, p.to_bits()),
532            Counts::Poisson { rate } => (1, rate.to_bits(), 0),
533            Counts::Geometric { p } => (2, p.to_bits(), 0),
534        }
535    }
536
537    /// P(X ≤ x).
538    pub fn cdf(&self, x: f64) -> f64 {
539        let k = x.floor();
540        match *self {
541            Counts::Binomial { n, p } => {
542                let n = n as f64;
543                if k < 0.0 {
544                    0.0
545                } else if k >= n || p <= 0.0 {
546                    1.0
547                } else if p >= 1.0 {
548                    0.0
549                } else {
550                    // I_{1−p}(n − k, k + 1).
551                    crate::continuous::beta_cdf(n - k, k + 1.0, 1.0 - p)
552                }
553            }
554            Counts::Poisson { rate } => {
555                if k < 0.0 {
556                    0.0
557                } else if rate <= 0.0 {
558                    1.0
559                } else {
560                    // Q(k + 1, rate), the upper regularized gamma.
561                    1.0 - crate::continuous::gamma_cdf(k + 1.0, rate)
562                }
563            }
564            Counts::Geometric { p } => {
565                if k < 1.0 {
566                    0.0
567                } else if p >= 1.0 {
568                    1.0
569                } else {
570                    -libm::expm1(k * libm::log1p(-p))
571                }
572            }
573        }
574    }
575
576    /// P(X = k).
577    pub fn pmf(&self, k: f64) -> f64 {
578        if k < 0.0 || k.fract() != 0.0 {
579            return 0.0;
580        }
581        let exactly = |x: f64| if k == x { 1.0 } else { 0.0 };
582        match *self {
583            Counts::Binomial { n, p } => {
584                let n = n as f64;
585                if k > n {
586                    0.0
587                } else if p <= 0.0 {
588                    exactly(0.0)
589                } else if p >= 1.0 {
590                    exactly(n)
591                } else {
592                    crate::math::exp(
593                        libm::lgamma(n + 1.0) - libm::lgamma(k + 1.0) - libm::lgamma(n - k + 1.0)
594                            + k * libm::log(p)
595                            + (n - k) * libm::log1p(-p),
596                    )
597                }
598            }
599            Counts::Poisson { rate } => {
600                if rate <= 0.0 {
601                    exactly(0.0)
602                } else {
603                    crate::math::exp(k * libm::log(rate) - rate - libm::lgamma(k + 1.0))
604                }
605            }
606            Counts::Geometric { p } => {
607                if k < 1.0 {
608                    0.0
609                } else if p >= 1.0 {
610                    exactly(1.0)
611                } else {
612                    p * crate::math::exp((k - 1.0) * libm::log1p(-p))
613                }
614            }
615        }
616    }
617
618    /// One draw.
619    pub fn sample(&self, rng: &mut Rng) -> i64 {
620        match *self {
621            Counts::Binomial { n, p } => {
622                if p <= 0.0 {
623                    return 0;
624                }
625                if p >= 1.0 {
626                    return n as i64;
627                }
628                let odds = p / (1.0 - p);
629                let mode = (((n as f64) + 1.0) * p).floor().min(n as f64) as u64;
630                let up = |k: u64| (n - k) as f64 / (k + 1) as f64 * odds;
631                let down = |k: u64| k as f64 / (n - k + 1) as f64 / odds;
632                from_mode(rng, mode, self.pmf(mode as f64), n, up, down) as i64
633            }
634            Counts::Poisson { rate } => {
635                if rate <= 0.0 {
636                    return 0;
637                }
638                let mode = rate.floor() as u64;
639                let up = |k: u64| rate / (k + 1) as f64;
640                let down = |k: u64| k as f64 / rate;
641                from_mode(rng, mode, self.pmf(mode as f64), u64::MAX, up, down) as i64
642            }
643            Counts::Geometric { p } => {
644                if p >= 1.0 {
645                    return 1;
646                }
647                // The inverse of P(X ≤ k) = 1 − (1 − p)^k.
648                (libm::log(rng.open()) / libm::log1p(-p)).ceil().max(1.0) as i64
649            }
650        }
651    }
652}
653
654impl std::fmt::Display for Counts {
655    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656        use crate::value::fmt_float;
657        match *self {
658            Counts::Binomial { n, p } => write!(f, "binomial({n}, {})", fmt_float(p)),
659            Counts::Poisson { rate } => write!(f, "poisson({})", fmt_float(rate)),
660            Counts::Geometric { p } => write!(f, "geometric({})", fmt_float(p)),
661        }
662    }
663}
664
665/// Inversion from the mode outward: a uniform draw is compared with the
666/// probabilities added up from the mode, taking the more likely neighbour
667/// next, so that a draw usually takes a few steps. `up` and `down` are as in
668/// `walk_from_mode`.
669fn from_mode(
670    rng: &mut Rng,
671    mode: u64,
672    p_mode: f64,
673    max: u64,
674    up: impl Fn(u64) -> f64,
675    down: impl Fn(u64) -> f64,
676) -> u64 {
677    let u = rng.uniform();
678    let mut total = p_mode;
679    let (mut lo, mut hi, mut p_lo, mut p_hi) = (mode, mode, p_mode, p_mode);
680    let mut last = mode;
681    while u >= total {
682        let below = if lo > 0 { p_lo * down(lo) } else { 0.0 };
683        let above = if hi < max { p_hi * up(hi) } else { 0.0 };
684        if below <= 0.0 && above <= 0.0 {
685            // The probabilities ran out before reaching the draw: that is
686            // only rounding.
687            break;
688        }
689        if above >= below {
690            hi += 1;
691            p_hi = above;
692            total += above;
693            last = hi;
694        } else {
695            lo -= 1;
696            p_lo = below;
697            total += below;
698            last = lo;
699        }
700    }
701    last
702}
703
704/// The outcomes `0..=max` of a count distribution with a single mode, built
705/// from the ratios between neighbouring probabilities: `up(k)` is
706/// P(k + 1) / P(k) and `down(k)` is P(k − 1) / P(k). Working with ratios
707/// relative to the mode stays accurate for huge parameters. The walk stops on
708/// each side once an outcome is below the threshold; the rest of that tail
709/// shrinks at least geometrically, which bounds the missing mass.
710fn walk_from_mode(
711    mode: u64,
712    max: u64,
713    up: impl Fn(u64) -> f64,
714    down: impl Fn(u64) -> f64,
715    budget: &mut Budget,
716) -> OpResult<Dist> {
717    let mut below: Vec<(u64, f64)> = Vec::new();
718    let mut above: Vec<(u64, f64)> = Vec::new();
719    let mut tails = 0.0;
720    let (mut k, mut w) = (mode, 1.0);
721    while k > 0 {
722        let next = w * down(k);
723        k -= 1;
724        if next < TAIL {
725            tails += geometric_tail(next, if k > 0 { down(k) } else { 0.0 });
726            break;
727        }
728        below.push((k, next));
729        w = next;
730        budget.outcomes((below.len() + above.len() + 1) as u128)?;
731        budget.work(1)?;
732    }
733    let (mut k, mut w) = (mode, 1.0);
734    while k < max {
735        let next = w * up(k);
736        k += 1;
737        if next < TAIL {
738            tails += geometric_tail(next, if k < max { up(k) } else { 0.0 });
739            break;
740        }
741        above.push((k, next));
742        w = next;
743        budget.outcomes((below.len() + above.len() + 1) as u128)?;
744        budget.work(1)?;
745    }
746    let sum = 1.0 + below.iter().map(|(_, w)| w).sum::<f64>() + above.iter().map(|(_, w)| w).sum::<f64>() + tails;
747    let pairs = below
748        .into_iter()
749        .rev()
750        .chain(std::iter::once((mode, 1.0)))
751        .chain(above)
752        .map(|(k, w)| (Value::Int((k as i64).into()), w / sum))
753        .collect();
754    Ok(Dist::from_sorted(pairs, tails / sum))
755}
756
757/// An upper bound on `first + first·r + first·r² + …` for ratios at most `r`.
758fn geometric_tail(first: f64, ratio: f64) -> f64 {
759    if first <= 0.0 {
760        return 0.0;
761    }
762    first / (1.0 - ratio.clamp(0.0, 1.0 - 1e-6))
763}
764
765/// The number of multisets of size `k` drawn from `n` kinds: C(n + k − 1, k),
766/// saturating.
767fn multisets(n: u128, k: u128) -> u128 {
768    if n == 0 {
769        return if k == 0 { 1 } else { 0 };
770    }
771    let mut result: u128 = 1;
772    for i in 1..=k {
773        result = result.saturating_mul(n + i - 1) / i;
774        if result > u64::MAX as u128 {
775            return u128::MAX;
776        }
777    }
778    result
779}
780
781impl PartialEq for Dist {
782    fn eq(&self, other: &Dist) -> bool {
783        self.missing.to_bits() == other.missing.to_bits()
784            && self.outcomes.len() == other.outcomes.len()
785            && self
786                .outcomes
787                .iter()
788                .zip(&other.outcomes)
789                .all(|((a, p), (b, q))| a == b && p.to_bits() == q.to_bits())
790    }
791}
792
793impl Eq for Dist {}
794
795impl Hash for Dist {
796    fn hash<H: Hasher>(&self, state: &mut H) {
797        self.outcomes.len().hash(state);
798        for (v, w) in &self.outcomes {
799            v.hash(state);
800            w.to_bits().hash(state);
801        }
802        self.missing.to_bits().hash(state);
803    }
804}
805
806impl Ord for Dist {
807    fn cmp(&self, other: &Dist) -> Ordering {
808        for ((a, p), (b, q)) in self.outcomes.iter().zip(&other.outcomes) {
809            let c = a.cmp(b).then_with(|| p.total_cmp(q));
810            if c != Ordering::Equal {
811                return c;
812            }
813        }
814        self.outcomes
815            .len()
816            .cmp(&other.outcomes.len())
817            .then_with(|| self.missing.total_cmp(&other.missing))
818    }
819}
820
821impl PartialOrd for Dist {
822    fn partial_cmp(&self, other: &Dist) -> Option<Ordering> {
823        Some(self.cmp(other))
824    }
825}
826
827/// Natural log of the gamma function (Lanczos approximation, g = 7).
828pub fn ln_gamma(x: f64) -> f64 {
829    const G: f64 = 7.0;
830    const C: [f64; 9] = [
831        0.999_999_999_999_809_9,
832        676.520_368_121_885_1,
833        -1_259.139_216_722_402_8,
834        771.323_428_777_653_1,
835        -176.615_029_162_140_6,
836        12.507_343_278_686_905,
837        -0.138_571_095_265_720_12,
838        9.984_369_578_019_572e-6,
839        1.505_632_735_149_311_6e-7,
840    ];
841    if x < 0.5 {
842        // Reflection formula.
843        return libm::log(std::f64::consts::PI / libm::sin(std::f64::consts::PI * x)) - ln_gamma(1.0 - x);
844    }
845    let x = x - 1.0;
846    let mut a = C[0];
847    let t = x + G + 0.5;
848    for (i, c) in C.iter().enumerate().skip(1) {
849        a += c / (x + i as f64);
850    }
851    0.5 * libm::log(2.0 * std::f64::consts::PI) + (x + 0.5) * libm::log(t) - t + libm::log(a)
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857
858    fn close(a: f64, b: f64) -> bool {
859        (a - b).abs() < 1e-12
860    }
861
862    #[test]
863    fn two_dice() {
864        let d = Dist::dice(2, 6, &mut Budget::unlimited()).unwrap();
865        assert_eq!(d.outcomes.len(), 11);
866        assert!(close(d.outcomes[5].1, 6.0 / 36.0));
867        assert!(close(d.mean().unwrap(), 7.0));
868        assert!(close(d.variance().unwrap(), 35.0 / 6.0));
869        assert_eq!(d.quantile(0.5), Some(Value::Int(7.into())));
870    }
871
872    #[test]
873    fn counts_sum_to_one() {
874        let b = &mut Budget::unlimited();
875        for d in [
876            Dist::binomial(30, 0.3, b).unwrap(),
877            Dist::poisson(4.5, b).unwrap(),
878            Dist::geometric(1.0 / 6.0, b).unwrap(),
879            Dist::binomial(30_000, 0.03, b).unwrap(),
880            Dist::poisson(1e9, b).unwrap(),
881        ] {
882            assert!((d.total() + d.missing - 1.0).abs() < 1e-9, "{:?}", d.outcomes.len());
883            assert!(d.missing < 1e-12);
884        }
885        assert!((Dist::poisson(4.5, b).unwrap().mean().unwrap() - 4.5).abs() < 1e-9);
886        assert!((Dist::binomial(30_000, 0.03, b).unwrap().mean().unwrap() - 900.0).abs() < 1e-6);
887    }
888
889    /// Drawing a count directly (as sampling does) follows the same
890    /// distribution as listing its outcomes: a chi-square test with the
891    /// cells of small expected counts pooled.
892    #[test]
893    fn direct_draws_follow_the_listed_distributions() {
894        let b = &mut Budget::unlimited();
895        let cases = [
896            Counts::Binomial { n: 250, p: 0.034 },
897            Counts::Binomial { n: 10, p: 0.5 },
898            Counts::Binomial { n: 5, p: 0.97 },
899            Counts::Binomial { n: 12_000, p: 0.03 },
900            Counts::Poisson { rate: 0.3 },
901            Counts::Poisson { rate: 100.5 },
902            Counts::Geometric { p: 0.2 },
903        ];
904        let mut rng = Rng::new(3);
905        for c in cases {
906            let d = c.list(b).unwrap();
907            for (v, p) in &d.outcomes {
908                let q = c.pmf(v.as_f64().unwrap());
909                assert!(
910                    (q - p).abs() <= 1e-10 * p.max(1e-300) + 1e-15,
911                    "{c:?} at {v}: {q} vs {p}"
912                );
913            }
914            let n = 200_000;
915            let mut seen: BTreeMap<i64, f64> = BTreeMap::new();
916            for _ in 0..n {
917                *seen.entry(c.sample(&mut rng)).or_default() += 1.0;
918            }
919            let (mut chi2, mut cells) = (0.0, 0);
920            let (mut expected, mut observed) = (0.0, 0.0);
921            for (v, p) in &d.outcomes {
922                expected += p * n as f64;
923                observed += seen.remove(&(v.as_f64().unwrap() as i64)).unwrap_or(0.0);
924                if expected >= 20.0 {
925                    chi2 += (observed - expected).powi(2) / expected;
926                    cells += 1;
927                    (expected, observed) = (0.0, 0.0);
928                }
929            }
930            chi2 += (observed - expected).powi(2) / expected.max(1.0);
931            assert!(seen.is_empty(), "{c:?} drew values outside its outcomes: {seen:?}");
932            let df = cells as f64;
933            assert!(
934                chi2 < df + 6.0 * (2.0 * df).sqrt() + 10.0,
935                "{c:?}: chi² {chi2} with {cells} cells"
936            );
937        }
938    }
939
940    #[test]
941    fn dice_pools() {
942        let b = &mut Budget::unlimited();
943        let pool = Dist::pool(3, &Dist::dice(1, 6, b).unwrap(), b).unwrap();
944        assert_eq!(pool.outcomes.len(), 56);
945        assert!(close(pool.total(), 1.0));
946        // A die with missing mass m: a pool of n misses 1 - (1 - m)^n.
947        let leaky = Dist::from_pairs(vec![(Value::Int(1.into()), 0.9)], 0.1);
948        let pool = Dist::pool(2, &leaky, b).unwrap();
949        assert!(close(pool.missing, 1.0 - 0.81));
950        assert!(close(pool.total() + pool.missing, 1.0));
951    }
952
953    #[test]
954    fn budgets_are_checked_before_building() {
955        let mut small = Budget {
956            max_outcomes: 1000,
957            max_collection: 1000,
958            ..Budget::unlimited()
959        };
960        assert!(Dist::dice(1, 100_000, &mut small).is_err());
961        assert!(Dist::pool(40, &Dist::dice(1, 6, &mut Budget::unlimited()).unwrap(), &mut small).is_err());
962        assert!(Dist::geometric(1e-9, &mut small).is_err());
963        assert_eq!(multisets(6, 3), 56);
964        assert_eq!(multisets(u64::MAX as u128, 40), u128::MAX);
965    }
966
967    #[test]
968    fn a_shared_budget_is_spent_once() {
969        let shared = Arc::new(AtomicU64::new(100_000));
970        let budget = Budget {
971            work_left: 0,
972            shared: Some(shared.clone()),
973            ..Budget::unlimited()
974        };
975        let (mut a, mut b) = (budget.clone(), budget);
976        a.work(60_000).unwrap();
977        assert!(b.work(60_000).is_err(), "only 40,000 are left");
978        b.work(30_000).unwrap();
979        a.give_back();
980        b.give_back();
981        assert_eq!(shared.load(Atomic::Relaxed), 10_000);
982    }
983
984    #[test]
985    fn gamma() {
986        assert!((ln_gamma(1.0)).abs() < 1e-13);
987        assert!((ln_gamma(5.0) - 24f64.ln()).abs() < 1e-13);
988        assert!((ln_gamma(0.5) - std::f64::consts::PI.sqrt().ln()).abs() < 1e-13);
989    }
990}