use super::{Probability, Quantile};
use crate::errors::QlResult;
use crate::types::Real;
use crate::{fail, require};
#[derive(Clone, Copy, Debug)]
pub struct PoissonDistribution {
mu: Real,
log_mu: Real,
}
impl PoissonDistribution {
pub fn new(mu: Real) -> QlResult<Self> {
require!(
mu.is_finite() && mu >= 0.0,
"Poisson mean must be a finite non-negative number, got {mu}"
);
let log_mu = if mu == 0.0 { 0.0 } else { mu.ln() };
Ok(PoissonDistribution { mu, log_mu })
}
pub fn pmf(&self, k: u64) -> Real {
if self.mu == 0.0 {
return if k == 0 { 1.0 } else { 0.0 };
}
let ln_factorial: Real = (1..=k).map(|j| (j as Real).ln()).sum();
(k as Real * self.log_mu - ln_factorial - self.mu).exp()
}
}
#[derive(Clone, Copy, Debug)]
pub struct CumulativePoissonDistribution {
mu: Real,
}
impl CumulativePoissonDistribution {
pub fn new(mu: Real) -> QlResult<Self> {
require!(
mu.is_finite() && mu >= 0.0,
"Poisson mean must be a finite non-negative number, got {mu}"
);
if (-mu).exp() < Real::MIN_POSITIVE {
fail!("Poisson mean too large for the mass recurrence (e^-mu underflows), got {mu}");
}
Ok(CumulativePoissonDistribution { mu })
}
pub fn cdf(&self, k: u64) -> Real {
let mut term = (-self.mu).exp();
let mut sum = term;
for j in 1..=k {
term *= self.mu / j as Real;
sum += term;
}
sum
}
}
#[derive(Clone, Copy, Debug)]
pub struct InverseCumulativePoisson {
lambda: Real,
}
impl InverseCumulativePoisson {
pub fn new(lambda: Real) -> QlResult<Self> {
require!(
lambda.is_finite() && lambda > 0.0,
"Poisson lambda must be a finite positive number, got {lambda}"
);
if (-lambda).exp() < Real::MIN_POSITIVE {
fail!(
"Poisson lambda too large for the mass recurrence (e^-lambda underflows), got {lambda}"
);
}
Ok(InverseCumulativePoisson { lambda })
}
}
impl Quantile for InverseCumulativePoisson {
fn quantile(&self, p: Probability) -> QlResult<Real> {
let p = p.value();
if p == 0.0 {
return Ok(0.0);
}
if p == 1.0 {
return Ok(Real::INFINITY);
}
let mut sum = 0.0;
let mut index: u64 = 0;
let mut mass = (-self.lambda).exp();
while p > sum {
if sum + mass == sum {
fail!(
"Poisson quantile cannot resolve p={p} (too close to 1) for lambda={}",
self.lambda
);
}
sum += mass;
index += 1;
mass *= self.lambda / index as Real;
}
Ok((index - 1) as Real)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pmf_matches_reference() {
let mut mean = 0.0;
while mean <= 10.0 {
let pdf = PoissonDistribution::new(mean).unwrap();
let mut log_helper = -mean;
assert!((pdf.pmf(0) - log_helper.exp()).abs() <= 1e-16);
for i in 1..25u64 {
let expected = if mean == 0.0 {
0.0
} else {
log_helper = log_helper + mean.ln() - (i as Real).ln();
log_helper.exp()
};
assert!(
(pdf.pmf(i) - expected).abs() <= 1e-13,
"pmf(mean={mean})({i}) = {} vs {expected}",
pdf.pmf(i)
);
}
mean += 0.5;
}
}
#[test]
fn cdf_matches_reference() {
let mut mean = 0.0;
while mean <= 10.0 {
let cdf = CumulativePoissonDistribution::new(mean).unwrap();
let mut log_helper = -mean;
let mut cum_expected = log_helper.exp();
assert!((cdf.cdf(0) - cum_expected).abs() <= 1e-13);
for i in 1..25u64 {
if mean == 0.0 {
cum_expected = 1.0;
} else {
log_helper = log_helper + mean.ln() - (i as Real).ln();
cum_expected += log_helper.exp();
}
assert!(
(cdf.cdf(i) - cum_expected).abs() <= 1e-12,
"cdf(mean={mean})({i}) = {} vs {cum_expected}",
cdf.cdf(i)
);
}
mean += 0.5;
}
}
#[test]
fn inverse_matches_reference() {
let icp = InverseCumulativePoisson::new(1.0).unwrap();
let data = [
0.2, 0.5, 0.9, 0.98, 0.99, 0.999, 0.9999, 0.99995, 0.99999, 0.999999, 0.9999999,
0.99999999,
];
for (i, &x) in data.iter().enumerate() {
let got = icp.quantile(Probability::try_from(x).unwrap()).unwrap();
assert_eq!(got, i as Real, "icp({x}) = {got}, expected {i}");
}
}
#[test]
fn mu_zero_is_point_mass_at_zero() {
let pdf = PoissonDistribution::new(0.0).unwrap();
assert_eq!(pdf.pmf(0), 1.0);
assert_eq!(pdf.pmf(1), 0.0);
let cdf = CumulativePoissonDistribution::new(0.0).unwrap();
assert_eq!(cdf.cdf(0), 1.0);
assert_eq!(cdf.cdf(5), 1.0);
}
#[test]
fn inverse_endpoints() {
let icp = InverseCumulativePoisson::new(2.5).unwrap();
assert_eq!(
icp.quantile(Probability::try_from(0.0).unwrap()).unwrap(),
0.0
);
assert_eq!(
icp.quantile(Probability::try_from(1.0).unwrap()).unwrap(),
Real::INFINITY
);
}
#[test]
fn inverse_errors_for_p_within_rounding_of_one() {
let icp = InverseCumulativePoisson::new(707.0).unwrap();
let p = Probability::try_from(1.0_f64.next_down()).unwrap();
assert!(icp.quantile(p).is_err());
}
#[test]
fn constructors_reject_invalid_parameters() {
assert!(PoissonDistribution::new(-1.0).is_err());
assert!(PoissonDistribution::new(Real::NAN).is_err());
assert!(PoissonDistribution::new(Real::INFINITY).is_err());
assert!(CumulativePoissonDistribution::new(-0.5).is_err());
assert!(InverseCumulativePoisson::new(0.0).is_err());
assert!(InverseCumulativePoisson::new(Real::INFINITY).is_err());
}
#[test]
fn recurrence_paths_reject_means_that_underflow() {
assert!(CumulativePoissonDistribution::new(750.0).is_err());
assert!(InverseCumulativePoisson::new(750.0).is_err());
assert!(PoissonDistribution::new(750.0).is_ok());
let cdf = CumulativePoissonDistribution::new(700.0).unwrap();
assert!(
cdf.cdf(700) > 0.4 && cdf.cdf(700) < 0.6,
"cdf(700) = {}",
cdf.cdf(700)
);
let icp = InverseCumulativePoisson::new(700.0).unwrap();
let median = icp.quantile(Probability::try_from(0.5).unwrap()).unwrap();
assert!((median - 700.0).abs() < 25.0, "median = {median}");
}
}