use std::fmt;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Zero};
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::domains::combinatorics::stirling2;
use super::rv::{Distribution, Support};
type Rat = Ratio<BigInt>;
fn invalid(reason: impl Into<String>) -> SymplexError {
SymplexError::InvalidArgument {
operation: "stats",
reason: reason.into(),
}
}
fn numeric(e: &Ex) -> Option<Rat> {
e.eval().as_rational()
}
fn require_probability(p: &Ex, what: &str) -> Result<(), SymplexError> {
if let Some(q) = numeric(p)
&& (q < Rat::zero() || q > Rat::one())
{
return Err(invalid(format!("{what} must lie in [0, 1], got `{p}`")));
}
Ok(())
}
fn require_probability_positive(p: &Ex, what: &str, allow_one: bool) -> Result<(), SymplexError> {
if let Some(q) = numeric(p)
&& (q <= Rat::zero() || q > Rat::one() || (!allow_one && q == Rat::one()))
{
let range = if allow_one { "(0, 1]" } else { "(0, 1)" };
return Err(invalid(format!("{what} must lie in {range}, got `{p}`")));
}
Ok(())
}
fn require_count(n: &Ex, what: &str) -> Result<(), SymplexError> {
if let Some(q) = numeric(n)
&& (!q.is_integer() || q < Rat::zero())
{
return Err(invalid(format!(
"{what} must be a non-negative integer, got `{n}`"
)));
}
Ok(())
}
fn require_integer(e: &Ex, what: &str) -> Result<(), SymplexError> {
if let Some(q) = numeric(e)
&& !q.is_integer()
{
return Err(invalid(format!("{what} must be an integer, got `{e}`")));
}
Ok(())
}
fn require_positive(e: &Ex, what: &str) -> Result<(), SymplexError> {
if e.is_positive() == Some(false) {
return Err(invalid(format!("{what} must be positive, got `{e}`")));
}
Ok(())
}
fn require_le(a: &Ex, b: &Ex, what: &str) -> Result<(), SymplexError> {
if let (Some(x), Some(y)) = (numeric(a), numeric(b))
&& x > y
{
return Err(invalid(format!("{what}: `{a}` exceeds `{b}`")));
}
Ok(())
}
fn falling_factorial(x: &Ex, k: u32) -> Ex {
let ctx = x.context();
let mut acc = ctx.one();
for j in 0..k {
acc *= x - ctx.int(i64::from(j));
}
acc
}
fn raw_moment_from_factorial_moments(
n: u32,
ctx: &Context,
factorial_moment: impl Fn(u32) -> Ex,
) -> Option<Ex> {
let mut acc = ctx.zero();
for k in 1..=n {
let s = ctx.from_bigint(stirling2(n, k)?);
acc += s * factorial_moment(k);
}
Some(acc.simplify())
}
const NEGATIVE_BINOMIAL_POLYNOMIAL_MAX_R: i64 = 32;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum DiscreteFamily {
Finite {
table: Vec<(Ex, Ex)>,
},
Bernoulli {
p: Ex,
},
Binomial {
n: Ex,
p: Ex,
},
Poisson {
rate: Ex,
},
Geometric {
p: Ex,
},
NegativeBinomial {
r: Ex,
p: Ex,
},
Hypergeometric {
population: Ex,
successes: Ex,
draws: Ex,
},
DiscreteUniform {
a: Ex,
b: Ex,
},
}
impl Distribution {
pub fn try_finite(table: Vec<(Ex, Ex)>) -> Result<Distribution, SymplexError> {
use num_bigint::BigInt;
use num_rational::Ratio;
if table.is_empty() {
return Err(invalid("a finite distribution needs at least one value"));
}
let mut total = Ratio::from_integer(BigInt::from(0));
let mut all_numeric = true;
for (v, p) in &table {
match p.eval().as_rational() {
Some(q) => {
if q < Ratio::from_integer(BigInt::from(0)) {
return Err(invalid(format!("probability of `{v}` is negative: `{p}`")));
}
total += q;
}
None => all_numeric = false,
}
}
if all_numeric && total != Ratio::from_integer(BigInt::from(1)) {
return Err(invalid(format!("the probabilities sum to {total}, not 1")));
}
for (i, (v, _)) in table.iter().enumerate() {
for (w, _) in &table[i + 1..] {
if v.equals(w) == Some(true) {
return Err(invalid(format!("the value `{v}` is listed twice")));
}
}
}
Ok(Distribution::Discrete(DiscreteFamily::Finite { table }))
}
pub fn finite(table: Vec<(Ex, Ex)>) -> Distribution {
Distribution::Discrete(DiscreteFamily::Finite { table })
}
pub fn try_bernoulli(p: Ex) -> Result<Distribution, SymplexError> {
require_probability(&p, "the success probability")?;
Ok(Distribution::Discrete(DiscreteFamily::Bernoulli { p }))
}
pub fn bernoulli(p: Ex) -> Distribution {
Distribution::Discrete(DiscreteFamily::Bernoulli { p })
}
pub fn try_binomial(n: Ex, p: Ex) -> Result<Distribution, SymplexError> {
require_count(&n, "the number of trials")?;
require_probability(&p, "the success probability")?;
Ok(Distribution::Discrete(DiscreteFamily::Binomial { n, p }))
}
pub fn binomial(n: Ex, p: Ex) -> Distribution {
Distribution::Discrete(DiscreteFamily::Binomial { n, p })
}
pub fn try_poisson(rate: Ex) -> Result<Distribution, SymplexError> {
require_positive(&rate, "the rate")?;
Ok(Distribution::Discrete(DiscreteFamily::Poisson { rate }))
}
pub fn poisson(rate: Ex) -> Distribution {
Distribution::Discrete(DiscreteFamily::Poisson { rate })
}
pub fn try_geometric(p: Ex) -> Result<Distribution, SymplexError> {
require_probability_positive(&p, "the success probability", true)?;
Ok(Distribution::Discrete(DiscreteFamily::Geometric { p }))
}
pub fn geometric(p: Ex) -> Distribution {
Distribution::Discrete(DiscreteFamily::Geometric { p })
}
pub fn try_negative_binomial(r: Ex, p: Ex) -> Result<Distribution, SymplexError> {
require_positive(&r, "the number of successes")?;
require_probability_positive(&p, "the success probability", false)?;
Ok(Distribution::Discrete(DiscreteFamily::NegativeBinomial {
r,
p,
}))
}
pub fn negative_binomial(r: Ex, p: Ex) -> Distribution {
Distribution::Discrete(DiscreteFamily::NegativeBinomial { r, p })
}
pub fn try_hypergeometric(
population: Ex,
successes: Ex,
draws: Ex,
) -> Result<Distribution, SymplexError> {
require_count(&population, "the population size")?;
require_count(&successes, "the number of successes")?;
require_count(&draws, "the number of draws")?;
require_le(
&successes,
&population,
"the number of successes must not exceed the population",
)?;
require_le(
&draws,
&population,
"the number of draws must not exceed the population",
)?;
Ok(Distribution::Discrete(DiscreteFamily::Hypergeometric {
population,
successes,
draws,
}))
}
pub fn hypergeometric(population: Ex, successes: Ex, draws: Ex) -> Distribution {
Distribution::Discrete(DiscreteFamily::Hypergeometric {
population,
successes,
draws,
})
}
pub fn try_discrete_uniform(a: Ex, b: Ex) -> Result<Distribution, SymplexError> {
require_integer(&a, "the lowest value")?;
require_integer(&b, "the highest value")?;
require_le(&a, &b, "the lowest value must not exceed the highest")?;
Ok(Distribution::Discrete(DiscreteFamily::DiscreteUniform {
a,
b,
}))
}
pub fn discrete_uniform(a: Ex, b: Ex) -> Distribution {
Distribution::Discrete(DiscreteFamily::DiscreteUniform { a, b })
}
pub fn try_die(sides: Ex) -> Result<Distribution, SymplexError> {
require_integer(&sides, "the number of sides")?;
require_positive(&sides, "the number of sides")?;
Ok(Distribution::die(sides))
}
pub fn die(sides: Ex) -> Distribution {
let one = sides.context().one();
Distribution::Discrete(DiscreteFamily::DiscreteUniform { a: one, b: sides })
}
}
impl DiscreteFamily {
pub fn name(&self) -> &'static str {
match self {
DiscreteFamily::Finite { .. } => "Finite",
DiscreteFamily::Bernoulli { .. } => "Bernoulli",
DiscreteFamily::Binomial { .. } => "Binomial",
DiscreteFamily::Poisson { .. } => "Poisson",
DiscreteFamily::Geometric { .. } => "Geometric",
DiscreteFamily::NegativeBinomial { .. } => "NegativeBinomial",
DiscreteFamily::Hypergeometric { .. } => "Hypergeometric",
DiscreteFamily::DiscreteUniform { .. } => "DiscreteUniform",
}
}
pub fn support(&self) -> Support {
match self {
DiscreteFamily::Finite { table } => {
Support::Finite(table.iter().map(|(v, _)| v.clone()).collect())
}
DiscreteFamily::Bernoulli { p } => Support::Discrete {
lo: Some(p.context().zero()),
hi: Some(p.context().one()),
},
DiscreteFamily::Binomial { n, .. } => Support::Discrete {
lo: Some(n.context().zero()),
hi: Some(n.clone()),
},
DiscreteFamily::Poisson { rate } => Support::Discrete {
lo: Some(rate.context().zero()),
hi: None,
},
DiscreteFamily::Geometric { p } => Support::Discrete {
lo: Some(p.context().one()),
hi: None,
},
DiscreteFamily::NegativeBinomial { p, .. } => Support::Discrete {
lo: Some(p.context().zero()),
hi: None,
},
DiscreteFamily::Hypergeometric {
population,
successes,
draws,
} => {
let zero = population.context().zero();
Support::Discrete {
lo: Some(zero.max_with(&(draws + successes - population)).simplify()),
hi: Some(draws.min_with(successes).simplify()),
}
}
DiscreteFamily::DiscreteUniform { a, b } => Support::Discrete {
lo: Some(a.clone()),
hi: Some(b.clone()),
},
}
}
pub fn pmf(&self, k: &Ex) -> Ex {
let ctx = k.context();
match self {
DiscreteFamily::Finite { table } => {
let zero = ctx.zero();
let true_ = ctx.bool_true();
let pairs: Vec<(Ex, crate::api::expr::BoolEx)> = table
.iter()
.map(|(v, p)| (p.clone(), k.eq_expr(v)))
.chain(std::iter::once((zero, true_)))
.collect();
let refs: Vec<(&Ex, &crate::api::expr::BoolEx)> =
pairs.iter().map(|(a, b)| (a, b)).collect();
Ex::piecewise(&refs)
}
DiscreteFamily::Bernoulli { p } => p.pow(k) * (ctx.one() - p).pow(&(ctx.one() - k)),
DiscreteFamily::Binomial { n, p } => {
let q = ctx.one() - p;
n.binomial(k) * p.pow(k) * q.pow(&(n - k))
}
DiscreteFamily::Poisson { rate } => rate.pow(k) * (-rate).exp() / k.factorial(),
DiscreteFamily::Geometric { p } => (ctx.one() - p).pow(&(k - ctx.one())) * p,
DiscreteFamily::NegativeBinomial { r, p } => {
let coeff = match r.as_i64() {
Some(ri) if (1..=NEGATIVE_BINOMIAL_POLYNOMIAL_MAX_R).contains(&ri) => {
let mut num = ctx.one();
let mut den = BigInt::one();
for j in 1..ri {
num *= k + ctx.int(j);
den *= BigInt::from(j);
}
num / ctx.from_bigint(den)
}
_ => (k + r - ctx.one()).binomial(k),
};
coeff * p.pow(r) * (ctx.one() - p).pow(k)
}
DiscreteFamily::Hypergeometric {
population,
successes,
draws,
} => {
successes.binomial(k) * (population - successes).binomial(&(draws - k))
/ population.binomial(draws)
}
DiscreteFamily::DiscreteUniform { a, b } => ctx.one() / (b - a + ctx.one()),
}
}
pub fn mean(&self, ctx: &Context) -> Option<Ex> {
match self {
DiscreteFamily::Finite { table } => Some(
table
.iter()
.fold(ctx.zero(), |acc, (v, p)| acc + v * p)
.simplify(),
),
DiscreteFamily::Bernoulli { p } => Some(p.clone()),
DiscreteFamily::Binomial { n, p } => Some((n * p).simplify()),
DiscreteFamily::Poisson { rate } => Some(rate.clone()),
DiscreteFamily::Geometric { p } => Some((ctx.one() / p).simplify()),
DiscreteFamily::NegativeBinomial { r, p } => Some((r * (ctx.one() - p) / p).simplify()),
DiscreteFamily::Hypergeometric {
population,
successes,
draws,
} => Some((draws * successes / population).simplify()),
DiscreteFamily::DiscreteUniform { a, b } => Some(((a + b) / ctx.int(2)).simplify()),
}
}
pub fn variance(&self, ctx: &Context) -> Option<Ex> {
match self {
DiscreteFamily::Finite { table } => {
let mean = table.iter().fold(ctx.zero(), |acc, (v, p)| acc + v * p);
let second = table
.iter()
.fold(ctx.zero(), |acc, (v, p)| acc + v.powi(2) * p);
Some((second - mean.powi(2)).simplify())
}
DiscreteFamily::Bernoulli { p } => Some((p * (ctx.one() - p)).simplify()),
DiscreteFamily::Binomial { n, p } => Some((n * p * (ctx.one() - p)).simplify()),
DiscreteFamily::Poisson { rate } => Some(rate.clone()),
DiscreteFamily::Geometric { p } => Some(((ctx.one() - p) / p.powi(2)).simplify()),
DiscreteFamily::NegativeBinomial { r, p } => {
Some((r * (ctx.one() - p) / p.powi(2)).simplify())
}
DiscreteFamily::Hypergeometric {
population,
successes,
draws,
} => Some(
(draws
* (successes / population)
* ((population - successes) / population)
* ((population - draws) / (population - ctx.one())))
.simplify(),
),
DiscreteFamily::DiscreteUniform { a, b } => {
Some((((b - a + ctx.one()).powi(2) - ctx.one()) / ctx.int(12)).simplify())
}
}
}
pub fn raw_moment(&self, n: u32, ctx: &Context) -> Option<Ex> {
if n == 0 {
return Some(ctx.one());
}
match self {
DiscreteFamily::Finite { table } => Some(
table
.iter()
.fold(ctx.zero(), |acc, (v, p)| acc + v.powi(i64::from(n)) * p)
.simplify(),
),
DiscreteFamily::Bernoulli { p } => Some(p.clone()),
DiscreteFamily::Binomial { n: trials, p } => {
raw_moment_from_factorial_moments(n, ctx, |k| {
falling_factorial(trials, k) * p.powi(i64::from(k))
})
}
DiscreteFamily::Poisson { rate } => {
raw_moment_from_factorial_moments(n, ctx, |k| rate.powi(i64::from(k)))
}
DiscreteFamily::Geometric { .. } | DiscreteFamily::NegativeBinomial { .. } => {
self.moment_from_mgf(n, ctx)
}
DiscreteFamily::Hypergeometric {
population,
successes,
draws,
} => raw_moment_from_factorial_moments(n, ctx, |k| {
falling_factorial(draws, k) * falling_factorial(successes, k)
/ falling_factorial(population, k)
}),
DiscreteFamily::DiscreteUniform { .. } => None,
}
}
fn moment_from_mgf(&self, n: u32, ctx: &Context) -> Option<Ex> {
let t = ctx.symbol("_t_mgf");
let mut m = self.mgf(&t)?;
for _ in 0..n {
m = m.diff(&t);
}
let at_zero = m.subs(&t, &ctx.zero()).simplify();
(!at_zero.has_unevaluated() && !at_zero.contains(&t)).then_some(at_zero)
}
pub fn cdf(&self, k: &Ex) -> Option<Ex> {
let ctx = k.context();
match self {
DiscreteFamily::Finite { .. } => None,
DiscreteFamily::Bernoulli { p } => {
let zero = ctx.zero();
let one = ctx.one();
let q = &one - p;
Some(Ex::piecewise(&[
(&zero, &k.lt(&zero)),
(&q, &k.lt(&one)),
(&one, &k.ge(&one)),
]))
}
DiscreteFamily::Binomial { .. } => None,
DiscreteFamily::Poisson { rate } => {
let kf = k.floor();
Some(rate.uppergamma(&(&kf + ctx.one())) / kf.factorial())
}
DiscreteFamily::Geometric { p } => Some(ctx.one() - (ctx.one() - p).pow(&k.floor())),
DiscreteFamily::NegativeBinomial { .. } => None,
DiscreteFamily::Hypergeometric { .. } => None,
DiscreteFamily::DiscreteUniform { a, b } => {
Some((k.floor() - a + ctx.one()) / (b - a + ctx.one()))
}
}
}
pub fn mgf(&self, t: &Ex) -> Option<Ex> {
let ctx = t.context();
match self {
DiscreteFamily::Finite { table } => Some(
table
.iter()
.fold(t.context().zero(), |acc, (v, p)| acc + p * (v * t).exp())
.simplify(),
),
DiscreteFamily::Bernoulli { p } => Some(ctx.one() - p + p * t.exp()),
DiscreteFamily::Binomial { n, p } => Some((ctx.one() - p + p * t.exp()).pow(n)),
DiscreteFamily::Poisson { rate } => Some((rate * (t.exp() - ctx.one())).exp()),
DiscreteFamily::Geometric { p } => {
Some(p * t.exp() / (ctx.one() - (ctx.one() - p) * t.exp()))
}
DiscreteFamily::NegativeBinomial { r, p } => {
Some((p / (ctx.one() - (ctx.one() - p) * t.exp())).pow(r))
}
DiscreteFamily::Hypergeometric { .. } => None,
DiscreteFamily::DiscreteUniform { a, b } => Some(
((a * t).exp() - ((b + ctx.one()) * t).exp())
/ ((b - a + ctx.one()) * (ctx.one() - t.exp())),
),
}
}
pub fn quantile(&self, p: &Ex) -> Option<Ex> {
let ctx = p.context();
match self {
DiscreteFamily::Finite { .. } => None,
DiscreteFamily::Bernoulli { .. }
| DiscreteFamily::Binomial { .. }
| DiscreteFamily::Poisson { .. }
| DiscreteFamily::Geometric { .. }
| DiscreteFamily::NegativeBinomial { .. }
| DiscreteFamily::Hypergeometric { .. } => None,
DiscreteFamily::DiscreteUniform { a, b } => {
Some((a + (p * (b - a + ctx.one())).ceiling() - ctx.one()).simplify())
}
}
}
}
impl fmt::Display for DiscreteFamily {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiscreteFamily::Finite { table } => {
write!(f, "Finite({{")?;
for (i, (v, p)) in table.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{v}: {p}")?;
}
write!(f, "}})")
}
DiscreteFamily::Bernoulli { p } => write!(f, "Bernoulli({p})"),
DiscreteFamily::Binomial { n, p } => write!(f, "Binomial({n}, {p})"),
DiscreteFamily::Poisson { rate } => write!(f, "Poisson({rate})"),
DiscreteFamily::Geometric { p } => write!(f, "Geometric({p})"),
DiscreteFamily::NegativeBinomial { r, p } => write!(f, "NegativeBinomial({r}, {p})"),
DiscreteFamily::Hypergeometric {
population,
successes,
draws,
} => write!(f, "Hypergeometric({population}, {successes}, {draws})"),
DiscreteFamily::DiscreteUniform { a, b } => write!(f, "DiscreteUniform({a}, {b})"),
}
}
}