1use 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
13const TAIL: f64 = 1e-18;
15
16#[derive(Clone, Debug)]
17pub struct Dist {
18 pub outcomes: Vec<(Value, f64)>,
20 pub missing: f64,
24 pub law: Option<Box<Counts>>,
28}
29
30const WORK_CHUNK: u64 = 1 << 14;
33
34#[derive(Clone, Debug)]
36pub struct Budget {
37 pub max_integer_bits: u64,
39 pub integer_bytes_left: Arc<AtomicU64>,
41 pub max_string_bytes: usize,
42 pub string_bytes_left: Arc<AtomicU64>,
44 pub cancel: Option<Arc<AtomicBool>>,
45 pub max_outcomes: usize,
47 pub max_collection: usize,
49 pub work_left: u64,
51 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 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 #[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 #[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 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 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 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 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 #[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 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 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 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 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 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 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 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 pub fn pool(count: u32, die: &Dist, budget: &mut Budget) -> OpResult<Dist> {
359 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 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 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 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 pub fn quantile(&self, q: f64) -> Option<Value> {
429 crate::stats::quantile(&self.outcomes, q).cloned()
430 }
431}
432
433#[derive(Clone, Copy, Debug)]
437pub enum Counts {
438 Binomial {
439 n: u64,
440 p: f64,
441 },
442 Poisson {
443 rate: f64,
444 },
445 Geometric {
447 p: f64,
448 },
449}
450
451impl Counts {
452 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 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 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 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 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 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 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 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 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 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 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 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 (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
665fn 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 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
704fn 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
757fn 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
765fn 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
827pub 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 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 #[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 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}