use super::Binomial;
#[cfg(not(feature = "std"))]
use num_traits::Float as _;
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
impl ::rand::distr::Distribution<u64> for Binomial {
fn sample<R: ::rand::Rng + ?Sized>(&self, rng: &mut R) -> u64 {
sample_unchecked(rng, self.n, self.p)
}
}
#[cfg(feature = "rand")]
#[derive(Debug, Clone, Copy, PartialEq)]
enum Path {
Degenerate(u64),
PoissonLimit { mean: f64 },
Binv { p: f64, q: f64 },
Btpe { p: f64, q: f64 },
}
#[cfg(feature = "rand")]
#[derive(Debug, Clone, Copy, PartialEq)]
struct Plan {
path: Path,
flipped: bool,
}
#[cfg(feature = "rand")]
impl Plan {
fn new(n: u64, p: f64) -> Self {
if p <= 0.0 || n == 0 {
return Self {
path: Path::Degenerate(0),
flipped: false,
};
}
if p >= 1.0 {
return Self {
path: Path::Degenerate(n),
flipped: false,
};
}
let flipped = p > 0.5;
let p = if flipped { 1.0 - p } else { p };
let q = 1.0 - p;
let path = if q == 1.0 {
Path::PoissonLimit { mean: n as f64 * p }
} else if n as f64 * p < 10.0 {
Path::Binv { p, q }
} else {
Path::Btpe { p, q }
};
Self { path, flipped }
}
}
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum BinomialAlgorithm {
#[default]
Automatic,
Inversion,
Rejection,
}
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
#[derive(Copy, Clone, PartialEq, Debug)]
#[non_exhaustive]
pub enum BinomialAlgorithmError {
RejectionMeanTooSmall,
}
#[cfg(feature = "rand")]
impl core::fmt::Display for BinomialAlgorithmError {
#[cfg_attr(coverage_nightly, coverage(off))]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
BinomialAlgorithmError::RejectionMeanTooSmall => write!(
f,
"BTPE rejection sampling is undefined for this mean; 2.195*sqrt(n*p*q) must be at least 4.6*q"
),
}
}
}
#[cfg(feature = "rand")]
impl core::error::Error for BinomialAlgorithmError {}
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct BinomialSampler {
n: u64,
plan: Plan,
}
#[cfg(feature = "rand")]
impl ::rand::distr::Distribution<u64> for BinomialSampler {
fn sample<R: ::rand::Rng + ?Sized>(&self, rng: &mut R) -> u64 {
sample_plan(rng, self.n, self.plan)
}
}
#[cfg(feature = "rand")]
impl Binomial {
pub fn sampler(
&self,
algorithm: BinomialAlgorithm,
) -> Result<BinomialSampler, BinomialAlgorithmError> {
let plan = Plan::new(self.n, self.p);
let path = match (algorithm, plan.path) {
(BinomialAlgorithm::Automatic, path) => path,
(BinomialAlgorithm::Inversion, Path::Binv { p, q } | Path::Btpe { p, q }) => {
Path::Binv { p, q }
}
(BinomialAlgorithm::Rejection, Path::Binv { p, q } | Path::Btpe { p, q }) => {
if !btpe_is_defined(self.n, p, q) {
return Err(BinomialAlgorithmError::RejectionMeanTooSmall);
}
Path::Btpe { p, q }
}
(_, path @ (Path::Degenerate(_) | Path::PoissonLimit { .. })) => path,
};
Ok(BinomialSampler {
n: self.n,
plan: Plan { path, ..plan },
})
}
}
#[cfg(feature = "rand")]
fn btpe_is_defined(n: u64, p: f64, q: f64) -> bool {
2.195 * (n as f64 * p * q).sqrt() - 4.6 * q >= 0.0
}
#[cfg(feature = "rand")]
pub fn sample_unchecked<R: ::rand::Rng + ?Sized>(rng: &mut R, n: u64, p: f64) -> u64 {
sample_plan(rng, n, Plan::new(n, p))
}
#[cfg(feature = "rand")]
fn sample_plan<R: ::rand::Rng + ?Sized>(rng: &mut R, n: u64, plan: Plan) -> u64 {
let Plan { path, flipped } = plan;
let sample = match path {
Path::Degenerate(x) => return x,
Path::PoissonLimit { mean } => {
(crate::distribution::poisson::sample_unchecked(rng, mean) as u64).min(n)
}
Path::Binv { p, q } => binv(rng, n, p, q),
Path::Btpe { p, q } => btpe(rng, n, p, q),
};
if flipped { n - sample } else { sample }
}
#[cfg(feature = "rand")]
fn binv<R: ::rand::Rng + ?Sized>(rng: &mut R, n: u64, p: f64, q: f64) -> u64 {
const BINV_MAX_X: u64 = 110;
let s = p / q;
let a = (n as f64 + 1.0) * s;
let r0 = ((-p).ln_1p() * n as f64).exp();
'restart: loop {
let mut r = r0;
let mut u: f64 = ::rand::RngExt::random(rng);
let mut x = 0u64;
while u > r {
u -= r;
x += 1;
if x > BINV_MAX_X {
continue 'restart;
}
r *= a / (x as f64) - s;
}
return x;
}
}
#[cfg(feature = "rand")]
#[allow(clippy::many_single_char_names)] fn btpe<R: ::rand::Rng + ?Sized>(rng: &mut R, n: u64, p: f64, q: f64) -> u64 {
use core::cmp::Ordering;
const SQUEEZE_THRESHOLD: u64 = 20;
let n_f = n as f64;
let np = n_f * p;
let npq = np * q;
let f_m = np + p;
let m = f_m as u64; let p1 = (2.195 * npq.sqrt() - 4.6 * q).floor() + 0.5;
let x_m = m as f64 + 0.5; let x_l = x_m - p1; let x_r = x_m + p1; let c = 0.134 + 20.5 / (15.3 + m as f64);
let p2 = p1 * (1.0 + 2.0 * c);
let lambda = |a: f64| a * (1.0 + 0.5 * a);
let lambda_l = lambda((f_m - x_l) / (f_m - x_l * p));
let lambda_r = lambda((x_r - f_m) / (x_r * q));
let p3 = p2 + c / lambda_l;
let p4 = p3 + c / lambda_r;
loop {
let u: f64 = ::rand::RngExt::random::<f64>(rng) * p4;
let mut v: f64 = ::rand::RngExt::random(rng);
let y: u64;
if u <= p1 {
return (x_m - p1 * v + u) as u64;
} else if u <= p2 {
let x = x_l + (u - p1) / c;
v = v * c + 1.0 - (x - x_m).abs() / p1;
if v > 1.0 {
continue;
}
y = x as u64;
} else if u <= p3 {
let y_tmp = x_l + v.ln() / lambda_l;
if y_tmp < 0.0 {
continue;
}
y = y_tmp as u64;
v *= (u - p2) * lambda_l;
} else {
let y_tmp = x_r - v.ln() / lambda_r;
if y_tmp > n_f {
continue;
}
y = y_tmp as u64;
v *= (u - p3) * lambda_r;
}
let k = y.abs_diff(m);
if k <= SQUEEZE_THRESHOLD || (k as f64) >= 0.5 * npq - 1.0 {
let s = p / q;
let a = s * (n_f + 1.0);
let mut f = 1.0;
match m.cmp(&y) {
Ordering::Less => {
for i in (m + 1)..=y {
f *= a / (i as f64) - s;
}
}
Ordering::Greater => {
for i in (y + 1)..=m {
f /= a / (i as f64) - s;
}
}
Ordering::Equal => {}
}
if v <= f {
return y;
}
continue;
}
let kf = k as f64;
let rho = (kf / npq) * ((kf * (kf / 3.0 + 0.625) + 1.0 / 6.0) / npq + 0.5);
let t = -0.5 * kf * kf / npq;
let alpha = v.ln();
if alpha < t - rho {
return y;
}
if alpha > t + rho {
continue;
}
let x1 = (y + 1) as f64;
let f1 = (m + 1) as f64;
let z = ((n - m) + 1) as f64;
let w = ((n - y) + 1) as f64;
let stirling = |a: f64| {
let a2 = a * a;
(13860.0 - (462.0 - (132.0 - (99.0 - 140.0 / a2) / a2) / a2) / a2) / a / 166320.0
};
let y_sub_m = if y > m {
(y - m) as f64
} else {
-((m - y) as f64)
};
if alpha
<= x_m * (f1 / x1).ln()
+ (((n - m) as f64) + 0.5) * (z / w).ln()
+ y_sub_m * (w * p / (x1 * q)).ln()
+ stirling(f1)
+ stirling(z)
- stirling(x1)
- stirling(w)
{
return y;
}
}
}
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
impl ::rand::distr::Distribution<f64> for Binomial {
fn sample<R: ::rand::Rng + ?Sized>(&self, rng: &mut R) -> f64 {
::rand::RngExt::sample::<u64, _>(rng, self) as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::distribution::Discrete;
use crate::prec;
fn create_ok(p: f64, n: u64) -> Binomial {
let dist = Binomial::new(p, n);
assert!(dist.is_ok());
dist.unwrap()
}
#[cfg(all(feature = "rand", feature = "std"))]
#[test]
fn test_forced_algorithms_sample_correctly() {
use crate::distribution::{BinomialAlgorithm, BinomialAlgorithmError};
use crate::stats_tests::chisquare::chisquare;
use ::rand::SeedableRng;
use ::rand::distr::Distribution as _;
use ::rand::rngs::StdRng;
const SAMPLES: usize = 100_000;
let cases: &[(u64, f64, &[BinomialAlgorithm])] = &[
(
20,
0.1,
&[BinomialAlgorithm::Automatic, BinomialAlgorithm::Inversion],
),
(
200,
0.4,
&[
BinomialAlgorithm::Automatic,
BinomialAlgorithm::Inversion,
BinomialAlgorithm::Rejection,
],
),
(
500,
0.85,
&[
BinomialAlgorithm::Automatic,
BinomialAlgorithm::Inversion,
BinomialAlgorithm::Rejection,
],
),
];
for &(n, p, algorithms) in cases {
let dist = create_ok(p, n);
for &algorithm in algorithms {
let sampler = dist.sampler(algorithm).unwrap();
let mut rng = StdRng::seed_from_u64(0xA1_60 + n);
let mut counts = vec![0usize; (n + 1) as usize];
for _ in 0..SAMPLES {
let x: u64 = sampler.sample(&mut rng);
assert!(x <= n, "{algorithm:?} n={n} p={p}: sampled {x} > n");
counts[x as usize] += 1;
}
let (mut observed, mut expected) = (Vec::new(), Vec::new());
let mut acc = (0.0f64, 0usize);
for k in 0..=n {
acc.0 += SAMPLES as f64 * dist.pmf(k);
acc.1 += counts[k as usize];
if acc.0 >= 5.0 {
expected.push(acc.0);
observed.push(acc.1);
acc = (0.0, 0);
}
}
*expected.last_mut().unwrap() += acc.0;
*observed.last_mut().unwrap() += acc.1;
let last = expected.len() - 1;
let rest: f64 = expected[..last].iter().sum();
expected[last] = SAMPLES as f64 - rest;
let (statistic, pvalue) = chisquare(&observed, Some(&expected), None).unwrap();
assert!(
pvalue > 1e-6,
"{algorithm:?} n={n} p={p}: chi-square = {statistic:.1}, p = {pvalue:.3e}"
);
}
}
let small = create_ok(0.04, 100);
assert_eq!(
small.sampler(BinomialAlgorithm::Rejection),
Err(BinomialAlgorithmError::RejectionMeanTooSmall)
);
assert!(small.sampler(BinomialAlgorithm::Inversion).is_ok());
assert!(small.sampler(BinomialAlgorithm::Automatic).is_ok());
}
#[cfg(feature = "rand")]
#[test]
fn test_rejection_validity_bound() {
use super::{Path, Plan, btpe_is_defined};
use crate::distribution::BinomialAlgorithm;
assert!(!btpe_is_defined(100, 0.04, 0.96));
assert!(btpe_is_defined(100, 0.044, 0.956));
for n in [1u64, 5, 20, 100, 1000, 100_000] {
for i in 1..100 {
let p = i as f64 / 100.0;
if let Path::Btpe { p: rp, q } = Plan::new(n, p).path {
assert!(
btpe_is_defined(n, rp, q),
"automatic chose BTPE where it is undefined: n={n} p={p}"
);
}
let dist = create_ok(p, n);
let forced = dist.sampler(BinomialAlgorithm::Rejection);
let plan = Plan::new(n, p);
if let Path::Binv { p: rp, q } | Path::Btpe { p: rp, q } = plan.path {
assert_eq!(forced.is_ok(), btpe_is_defined(n, rp, q), "n={n} p={p}");
} else {
assert!(forced.is_ok(), "n={n} p={p}");
}
}
}
}
#[cfg(all(feature = "rand", feature = "std"))]
#[test]
fn test_sample_chi_square_goodness_of_fit() {
use ::rand::SeedableRng;
use ::rand::distr::Distribution as _;
use ::rand::rngs::StdRng;
use crate::stats_tests::chisquare::chisquare;
const SAMPLES: usize = 100_000;
for &(n, p) in &[
(20u64, 0.3f64), (100, 0.4), (1000, 0.02), (100, 0.93), (2000, 0.995), ] {
let dist = create_ok(p, n);
let mut rng = StdRng::seed_from_u64(0x5EED + n);
let mut counts = vec![0usize; (n + 1) as usize];
for _ in 0..SAMPLES {
let x: u64 = dist.sample(&mut rng);
assert!(x <= n, "n={n} p={p}: sampled {x}, outside the support");
counts[x as usize] += 1;
}
let mut observed: Vec<usize> = Vec::new();
let mut expected: Vec<f64> = Vec::new();
let mut acc = (0.0f64, 0usize);
for k in 0..=n {
acc.0 += SAMPLES as f64 * dist.pmf(k);
acc.1 += counts[k as usize];
if acc.0 >= 5.0 {
expected.push(acc.0);
observed.push(acc.1);
acc = (0.0, 0);
}
}
*expected.last_mut().unwrap() += acc.0;
*observed.last_mut().unwrap() += acc.1;
assert_eq!(
observed.iter().sum::<usize>(),
SAMPLES,
"n={n} p={p}: binning lost samples"
);
let last = expected.len() - 1;
let rest: f64 = expected[..last].iter().sum();
expected[last] = SAMPLES as f64 - rest;
debug_assert_eq!(expected.iter().sum::<f64>(), SAMPLES as f64);
let (statistic, pvalue) = chisquare(&observed, Some(&expected), None)
.expect("observed and expected totals agree by construction");
assert!(
pvalue > 1e-6,
"n={n} p={p}: chi-square = {statistic:.1} over {} cells, p = {pvalue:.3e}",
observed.len()
);
}
}
#[cfg(feature = "rand")]
#[test]
fn test_sample_extreme_parameters_moments() {
use ::rand::SeedableRng;
use ::rand::distr::Distribution as _;
use ::rand::rngs::StdRng;
let dist = create_ok(0.4, 1_000_000_000);
let mut rng = StdRng::seed_from_u64(99);
const SAMPLES: usize = 20_000;
let mean = 4.0e8_f64;
let sd = (1.0e9_f64 * 0.4 * 0.6).sqrt();
let mut sum = 0.0;
for _ in 0..SAMPLES {
let x: u64 = dist.sample(&mut rng);
assert!(
(x as f64 - mean).abs() < 8.0 * sd,
"sample {x} implausibly far out"
);
sum += x as f64;
}
let observed_mean = sum / SAMPLES as f64;
prec::assert_abs_diff_eq!(
observed_mean,
mean,
epsilon = 6.0 * sd / (SAMPLES as f64).sqrt()
);
let dist = create_ok(1e-18, 10_000_000_000_000_000);
let mut rng = StdRng::seed_from_u64(7);
let mut total = 0u64;
for _ in 0..200_000 {
total += <Binomial as ::rand::distr::Distribution<u64>>::sample(&dist, &mut rng);
}
assert!(
(total as f64 - 2000.0).abs() < 268.0,
"Poisson-limit path total {total}"
);
}
#[cfg(feature = "rand")]
#[test]
fn test_sampling_path_selection() {
use super::{Path, Plan};
assert_eq!(
Plan::new(50, 0.0),
Plan {
path: Path::Degenerate(0),
flipped: false
}
);
assert_eq!(
Plan::new(50, 1.0),
Plan {
path: Path::Degenerate(50),
flipped: false
}
);
assert_eq!(
Plan::new(0, 0.5),
Plan {
path: Path::Degenerate(0),
flipped: false
}
);
assert!(matches!(
Plan::new(20, 0.3),
Plan {
path: Path::Binv { .. },
flipped: false
}
));
assert!(matches!(
Plan::new(100, 0.4),
Plan {
path: Path::Btpe { .. },
flipped: false
}
));
assert!(matches!(
Plan::new(100, 0.93),
Plan {
path: Path::Binv { .. },
flipped: true
}
));
assert!(matches!(
Plan::new(2000, 0.995),
Plan {
path: Path::Btpe { .. },
flipped: true
}
));
assert_eq!(
Plan::new(10_000_000_000_000_000, 1e-18),
Plan {
path: Path::PoissonLimit { mean: 0.01 },
flipped: false
}
);
}
#[cfg(feature = "rand")]
#[test]
fn test_poisson_limit_never_reached_flipped() {
use super::{Path, Plan};
let mut p = 1.0f64;
for _ in 0..64 {
p = f64::from_bits(p.to_bits() - 1);
let plan = Plan::new(u64::MAX, p);
assert!(
!(plan.flipped && matches!(plan.path, Path::PoissonLimit { .. })),
"p = {p:e} reached the Poisson limit while flipped"
);
}
let largest_below_one = f64::from_bits(1.0f64.to_bits() - 1);
assert_eq!(
1.0 - largest_below_one,
(-53f64).exp2(),
"reflection bottoms out at 2^-53"
);
assert_eq!(
1.0 - (-54f64).exp2(),
1.0,
"the guard admits 2^-54 and below"
);
}
#[cfg(feature = "rand")]
#[test]
fn test_sample_p_adjacent_to_one() {
use ::rand::SeedableRng;
use ::rand::distr::Distribution as _;
use ::rand::rngs::StdRng;
let n = 10_000_000_000_000_000u64;
let p = f64::from_bits(1.0f64.to_bits() - 1);
let dist = create_ok(p, n);
let mut rng = StdRng::seed_from_u64(11);
const DRAWS: usize = 200_000;
let mut failures = 0u64;
for _ in 0..DRAWS {
let x: u64 = dist.sample(&mut rng);
assert!(x <= n, "sample {x} exceeds n");
failures += n - x;
}
let mean = DRAWS as f64 * n as f64 * (-53f64).exp2();
let tol = 6.0 * mean.sqrt();
assert!(
(failures as f64 - mean).abs() < tol,
"{failures} failures, expected {mean:.0} +- {tol:.0}"
);
}
#[cfg(feature = "rand")]
#[test]
fn test_sample_degenerate() {
use ::rand::SeedableRng;
use ::rand::rngs::StdRng;
let mut rng = StdRng::seed_from_u64(3);
for _ in 0..10 {
assert_eq!(
<Binomial as ::rand::distr::Distribution<u64>>::sample(
&create_ok(0.0, 50),
&mut rng
),
0
);
assert_eq!(
<Binomial as ::rand::distr::Distribution<u64>>::sample(
&create_ok(1.0, 50),
&mut rng
),
50
);
assert_eq!(
<Binomial as ::rand::distr::Distribution<u64>>::sample(
&create_ok(0.5, 0),
&mut rng
),
0
);
}
}
}