use std::fmt;
use num_traits::{One, Zero};
use crate::api::context::Context;
use crate::api::expr::{BoolEx, Ex};
use crate::base::errors::SymplexError;
use crate::base::numeric::Q;
use crate::domains::combinatorics::stirling2;
use super::continuous::sampler_positive;
use super::family::{Distribution, Family, Sampler, family_boilerplate};
use super::sample::{self, Rng};
use super::support::Support;
fn invalid(reason: impl Into<String>) -> SymplexError {
SymplexError::invalid_argument("stats", reason)
}
fn numeric(e: &Ex) -> Option<Q> {
e.eval().as_rational()
}
fn require_probability(p: &Ex, what: &str) -> Result<(), SymplexError> {
if let Some(q) = numeric(p)
&& (q < Q::zero() || q > Q::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 <= Q::zero() || q > Q::one() || (!allow_one && q == Q::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 < Q::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_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 sampler_probability(p: &Ex, what: &str) -> Result<f64, SymplexError> {
let v = p.eval_f64()?;
if !(v > 0.0 && v <= 1.0) {
return Err(invalid(format!(
"{what} must lie in (0, 1] to sample, got `{p}`"
)));
}
Ok(v)
}
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())
}
fn moment_from_mgf(mgf: impl Fn(&Ex) -> Option<Ex>, n: u32, ctx: &Context) -> Option<Ex> {
let t = ctx.symbol("_t_mgf");
let mut m = 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)
}
#[derive(Clone, Debug)]
pub struct Finite {
pub table: Vec<(Ex, Ex)>,
ctx: Context,
}
impl PartialEq for Finite {
fn eq(&self, other: &Self) -> bool {
self.table == other.table
}
}
impl Finite {
pub fn new(ctx: &Context, table: Vec<(Ex, Ex)>) -> Self {
Finite {
table,
ctx: ctx.clone(),
}
}
}
impl Family for Finite {
family_boilerplate!(Finite, "Finite");
fn context(&self) -> Context {
self.ctx.clone()
}
fn parameters(&self) -> Vec<(&'static str, Ex)> {
self.table
.iter()
.flat_map(|(v, p)| [("value", v.clone()), ("probability", p.clone())])
.collect()
}
fn support(&self) -> Support {
Support::points(self.table.iter().map(|(v, _)| v.clone()).collect())
}
fn density(&self, k: &Ex) -> Ex {
let zero = self.ctx.zero();
let true_ = self.ctx.bool_true();
let pairs: Vec<(Ex, BoolEx)> = self
.table
.iter()
.map(|(v, p)| (p.clone(), k.eq_expr(v)))
.chain(std::iter::once((zero, true_)))
.collect();
let refs: Vec<(&Ex, &BoolEx)> = pairs.iter().map(|(a, b)| (a, b)).collect();
Ex::piecewise(&refs)
}
fn mean(&self) -> Option<Ex> {
Some(
self.table
.iter()
.fold(self.ctx.zero(), |acc, (v, p)| acc + v * p)
.simplify(),
)
}
fn variance(&self) -> Option<Ex> {
let mean = self
.table
.iter()
.fold(self.ctx.zero(), |acc, (v, p)| acc + v * p);
let second = self
.table
.iter()
.fold(self.ctx.zero(), |acc, (v, p)| acc + v.powi(2) * p);
Some((second - mean.powi(2)).simplify())
}
fn raw_moment(&self, n: u32) -> Option<Ex> {
Some(
self.table
.iter()
.fold(self.ctx.zero(), |acc, (v, p)| {
acc + v.powi(i64::from(n)) * p
})
.simplify(),
)
}
fn mgf(&self, t: &Ex) -> Option<Ex> {
Some(
self.table
.iter()
.fold(self.ctx.zero(), |acc, (v, p)| acc + p * (v * t).exp())
.simplify(),
)
}
fn fmt_display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Finite({{")?;
for (i, (v, p)) in self.table.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{v}: {p}")?;
}
write!(f, "}})")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Bernoulli {
pub p: Ex,
}
impl Family for Bernoulli {
family_boilerplate!(Bernoulli, "Bernoulli", [p]);
fn support(&self) -> Support {
let ctx = self.context();
Support::integers(&ctx, Some(ctx.zero()), Some(ctx.one()))
}
fn density(&self, k: &Ex) -> Ex {
let ctx = self.context();
self.p.pow(k) * (ctx.one() - &self.p).pow(&(ctx.one() - k))
}
fn mean(&self) -> Option<Ex> {
Some(self.p.clone())
}
fn variance(&self) -> Option<Ex> {
Some((&self.p * (self.context().one() - &self.p)).simplify())
}
fn raw_moment(&self, _n: u32) -> Option<Ex> {
Some(self.p.clone())
}
fn cdf(&self, k: &Ex) -> Option<Ex> {
let ctx = self.context();
let zero = ctx.zero();
let one = ctx.one();
let q = &one - &self.p;
Some(Ex::piecewise(&[
(&zero, &k.lt(&zero)),
(&q, &k.lt(&one)),
(&one, &k.ge(&one)),
]))
}
fn mgf(&self, t: &Ex) -> Option<Ex> {
Some(self.context().one() - &self.p + &self.p * t.exp())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Binomial {
pub n: Ex,
pub p: Ex,
}
impl Family for Binomial {
family_boilerplate!(Binomial, "Binomial", [n, p]);
fn support(&self) -> Support {
let ctx = self.context();
Support::integers(&ctx, Some(ctx.zero()), Some(self.n.clone()))
}
fn density(&self, k: &Ex) -> Ex {
let q = self.context().one() - &self.p;
self.n.binomial(k) * self.p.pow(k) * q.pow(&(&self.n - k))
}
fn mean(&self) -> Option<Ex> {
Some((&self.n * &self.p).simplify())
}
fn variance(&self) -> Option<Ex> {
Some((&self.n * &self.p * (self.context().one() - &self.p)).simplify())
}
fn raw_moment(&self, n: u32) -> Option<Ex> {
raw_moment_from_factorial_moments(n, &self.context(), |k| {
falling_factorial(&self.n, k) * self.p.powi(i64::from(k))
})
}
fn mgf(&self, t: &Ex) -> Option<Ex> {
Some((self.context().one() - &self.p + &self.p * t.exp()).pow(&self.n))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Poisson {
pub rate: Ex,
}
impl Family for Poisson {
family_boilerplate!(Poisson, "Poisson", [rate]);
fn support(&self) -> Support {
let ctx = self.context();
Support::integers(&ctx, Some(ctx.zero()), None)
}
fn density(&self, k: &Ex) -> Ex {
self.rate.pow(k) * (-&self.rate).exp() / k.factorial()
}
fn mean(&self) -> Option<Ex> {
Some(self.rate.clone())
}
fn variance(&self) -> Option<Ex> {
Some(self.rate.clone())
}
fn raw_moment(&self, n: u32) -> Option<Ex> {
raw_moment_from_factorial_moments(n, &self.context(), |k| self.rate.powi(i64::from(k)))
}
fn cdf(&self, k: &Ex) -> Option<Ex> {
let kf = k.floor();
Some(self.rate.uppergamma(&(&kf + self.context().one())) / kf.factorial())
}
fn mgf(&self, t: &Ex) -> Option<Ex> {
Some((&self.rate * (t.exp() - self.context().one())).exp())
}
fn sampler(&self) -> Option<Result<Sampler, SymplexError>> {
Some(
sampler_positive(&self.rate, "the rate").map(|lambda| -> Sampler {
Box::new(move |rng: &mut Rng| sample::poisson(rng, lambda))
}),
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Geometric {
pub p: Ex,
}
impl Family for Geometric {
family_boilerplate!(Geometric, "Geometric", [p]);
fn support(&self) -> Support {
let ctx = self.context();
Support::integers(&ctx, Some(ctx.one()), None)
}
fn density(&self, k: &Ex) -> Ex {
let ctx = self.context();
(ctx.one() - &self.p).pow(&(k - ctx.one())) * &self.p
}
fn mean(&self) -> Option<Ex> {
Some((self.context().one() / &self.p).simplify())
}
fn variance(&self) -> Option<Ex> {
Some(((self.context().one() - &self.p) / self.p.powi(2)).simplify())
}
fn raw_moment(&self, n: u32) -> Option<Ex> {
moment_from_mgf(|t| Family::mgf(self, t), n, &self.context())
}
fn cdf(&self, k: &Ex) -> Option<Ex> {
let ctx = self.context();
Some(ctx.one() - (ctx.one() - &self.p).pow(&k.floor()))
}
fn mgf(&self, t: &Ex) -> Option<Ex> {
let ctx = self.context();
Some(&self.p * t.exp() / (ctx.one() - (ctx.one() - &self.p) * t.exp()))
}
fn sampler(&self) -> Option<Result<Sampler, SymplexError>> {
Some(
sampler_probability(&self.p, "the success probability").map(|p| -> Sampler {
let ln_q = (-p).ln_1p();
Box::new(move |rng: &mut Rng| {
(sample::positive_uniform(rng).ln() / ln_q).floor() + 1.0
})
}),
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct NegativeBinomial {
pub r: Ex,
pub p: Ex,
}
impl Family for NegativeBinomial {
family_boilerplate!(NegativeBinomial, "NegativeBinomial", [r, p]);
fn support(&self) -> Support {
let ctx = self.context();
Support::integers(&ctx, Some(ctx.zero()), None)
}
fn density(&self, k: &Ex) -> Ex {
let ctx = self.context();
(k + &self.r - ctx.one()).binomial(k) * self.p.pow(&self.r) * (ctx.one() - &self.p).pow(k)
}
fn mean(&self) -> Option<Ex> {
Some((&self.r * (self.context().one() - &self.p) / &self.p).simplify())
}
fn variance(&self) -> Option<Ex> {
Some((&self.r * (self.context().one() - &self.p) / self.p.powi(2)).simplify())
}
fn raw_moment(&self, n: u32) -> Option<Ex> {
moment_from_mgf(|t| Family::mgf(self, t), n, &self.context())
}
fn mgf(&self, t: &Ex) -> Option<Ex> {
let ctx = self.context();
Some((&self.p / (ctx.one() - (ctx.one() - &self.p) * t.exp())).pow(&self.r))
}
fn sampler(&self) -> Option<Result<Sampler, SymplexError>> {
Some(self.build_sampler())
}
}
impl NegativeBinomial {
fn build_sampler(&self) -> Result<Sampler, SymplexError> {
let r = sampler_positive(&self.r, "the number of successes")?;
let p = sampler_probability(&self.p, "the success probability")?;
let scale = (1.0 - p) / p;
Ok(Box::new(move |rng: &mut Rng| {
let lambda = scale * sample::standard_gamma(rng, r);
sample::poisson(rng, lambda)
}))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Hypergeometric {
pub population: Ex,
pub successes: Ex,
pub draws: Ex,
}
impl Family for Hypergeometric {
family_boilerplate!(
Hypergeometric,
"Hypergeometric",
[population, successes, draws]
);
fn support(&self) -> Support {
let ctx = self.context();
let zero = ctx.zero();
Support::integers(
&ctx,
Some(
zero.max_with(&(&self.draws + &self.successes - &self.population))
.simplify(),
),
Some(self.draws.min_with(&self.successes).simplify()),
)
}
fn density(&self, k: &Ex) -> Ex {
self.successes.binomial(k)
* (&self.population - &self.successes).binomial(&(&self.draws - k))
/ self.population.binomial(&self.draws)
}
fn mean(&self) -> Option<Ex> {
Some((&self.draws * &self.successes / &self.population).simplify())
}
fn variance(&self) -> Option<Ex> {
let (n, m, big_n) = (&self.draws, &self.successes, &self.population);
Some(
(n * (m / big_n)
* ((big_n - m) / big_n)
* ((big_n - n) / (big_n - self.context().one())))
.simplify(),
)
}
fn raw_moment(&self, n: u32) -> Option<Ex> {
raw_moment_from_factorial_moments(n, &self.context(), |k| {
falling_factorial(&self.draws, k) * falling_factorial(&self.successes, k)
/ falling_factorial(&self.population, k)
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DiscreteUniform {
pub a: Ex,
pub b: Ex,
}
impl Family for DiscreteUniform {
family_boilerplate!(DiscreteUniform, "DiscreteUniform", [a, b]);
fn support(&self) -> Support {
Support::integers(&self.context(), Some(self.a.clone()), Some(self.b.clone()))
}
fn density(&self, _k: &Ex) -> Ex {
let ctx = self.context();
ctx.one() / (&self.b - &self.a + ctx.one())
}
fn mean(&self) -> Option<Ex> {
Some(((&self.a + &self.b) / self.context().int(2)).simplify())
}
fn variance(&self) -> Option<Ex> {
let ctx = self.context();
Some((((&self.b - &self.a + ctx.one()).powi(2) - ctx.one()) / ctx.int(12)).simplify())
}
fn cdf(&self, k: &Ex) -> Option<Ex> {
let ctx = self.context();
Some((k.floor() - &self.a + ctx.one()) / (&self.b - &self.a + ctx.one()))
}
fn mgf(&self, t: &Ex) -> Option<Ex> {
let ctx = self.context();
Some(
((&self.a * t).exp() - ((&self.b + ctx.one()) * t).exp())
/ ((&self.b - &self.a + ctx.one()) * (ctx.one() - t.exp())),
)
}
fn quantile(&self, p: &Ex) -> Option<Ex> {
let ctx = self.context();
Some((&self.a + (p * (&self.b - &self.a + ctx.one())).ceiling() - ctx.one()).simplify())
}
}
impl Distribution {
pub fn try_finite(ctx: &Context, table: Vec<(Ex, Ex)>) -> Result<Distribution, SymplexError> {
if table.is_empty() {
return Err(invalid("a finite distribution needs at least one value"));
}
let mut total = Q::zero();
let mut all_numeric = true;
for (v, p) in &table {
match p.eval().as_rational() {
Some(q) => {
if q < Q::zero() {
return Err(invalid(format!("probability of `{v}` is negative: `{p}`")));
}
total += q;
}
None => all_numeric = false,
}
}
if all_numeric && total != Q::one() {
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::finite(ctx, table))
}
pub fn finite(ctx: &Context, table: Vec<(Ex, Ex)>) -> Distribution {
Distribution::from_family(Finite::new(ctx, table))
}
pub fn try_bernoulli(p: Ex) -> Result<Distribution, SymplexError> {
require_probability(&p, "the success probability")?;
Ok(Distribution::bernoulli(p))
}
pub fn bernoulli(p: Ex) -> Distribution {
Distribution::from_family(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::binomial(n, p))
}
pub fn binomial(n: Ex, p: Ex) -> Distribution {
Distribution::from_family(Binomial { n, p })
}
pub fn try_poisson(rate: Ex) -> Result<Distribution, SymplexError> {
super::continuous::require_positive(&rate, "the rate")?;
Ok(Distribution::poisson(rate))
}
pub fn poisson(rate: Ex) -> Distribution {
Distribution::from_family(Poisson { rate })
}
pub fn try_geometric(p: Ex) -> Result<Distribution, SymplexError> {
require_probability_positive(&p, "the success probability", true)?;
Ok(Distribution::geometric(p))
}
pub fn geometric(p: Ex) -> Distribution {
Distribution::from_family(Geometric { p })
}
pub fn try_negative_binomial(r: Ex, p: Ex) -> Result<Distribution, SymplexError> {
super::continuous::require_positive(&r, "the number of successes")?;
require_probability_positive(&p, "the success probability", false)?;
Ok(Distribution::negative_binomial(r, p))
}
pub fn negative_binomial(r: Ex, p: Ex) -> Distribution {
Distribution::from_family(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::hypergeometric(population, successes, draws))
}
pub fn hypergeometric(population: Ex, successes: Ex, draws: Ex) -> Distribution {
Distribution::from_family(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_uniform(a, b))
}
pub fn discrete_uniform(a: Ex, b: Ex) -> Distribution {
Distribution::from_family(DiscreteUniform { a, b })
}
pub fn try_die(sides: Ex) -> Result<Distribution, SymplexError> {
require_integer(&sides, "the number of sides")?;
super::continuous::require_positive(&sides, "the number of sides")?;
Ok(Distribution::die(sides))
}
pub fn die(sides: Ex) -> Distribution {
let one = sides.context().one();
Distribution::from_family(DiscreteUniform { a: one, b: sides })
}
}