use std::fmt;
use crate::deriv::SideCondition;
use crate::errors::AlkahestError;
use crate::kernel::{Domain, ExprId, ExprPool};
mod charfun;
mod cplx;
mod dists;
mod entropy;
mod expect;
mod genfun;
mod moments;
mod quad;
#[cfg(test)]
mod tests;
mod verify;
pub use charfun::characteristic_function;
pub use entropy::{
cross_entropy, entropy, kl_divergence, mutual_information_independent, MAX_BINOMIAL_ENTROPY_N,
};
pub use expect::{expectation, expectation_affine, variance_affine_independent};
pub use genfun::{
cumulant, cumulant_generating_function, excess_kurtosis, factorial_moment,
moment_generating_function, probability_generating_function, skewness, MAX_CUMULANT_ORDER,
};
pub use verify::Evidence;
pub(crate) use quad::numeric_ball;
std::thread_local! {
static PROB_CONDITIONS: std::cell::RefCell<Vec<SideCondition>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn take_prob_side_conditions() -> Vec<SideCondition> {
PROB_CONDITIONS.with(|c| std::mem::take(&mut *c.borrow_mut()))
}
pub(crate) fn stash_prob_side_conditions(conds: Vec<SideCondition>) {
PROB_CONDITIONS.with(|c| *c.borrow_mut() = conds);
}
pub(crate) fn conditions_of(log: &crate::deriv::DerivationLog) -> Vec<SideCondition> {
let mut out: Vec<SideCondition> = Vec::new();
for c in log.0.iter().flat_map(|s| s.side_conditions.iter()) {
if !out.contains(c) {
out.push(c.clone());
}
}
let strict: Vec<ExprId> = out
.iter()
.filter_map(|c| match c {
SideCondition::Positive(e) => Some(*e),
_ => None,
})
.collect();
out.retain(|c| {
!matches!(c,
SideCondition::InDomain(e, Domain::NonNegative) if strict.contains(e))
});
out
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProbError {
InvalidParameter {
parameter: &'static str,
requirement: &'static str,
},
Unsupported(String),
IntegralDidNotClose(String),
NoClosedForm {
quantity: &'static str,
missing: &'static str,
},
Unverified(UnverifiedReason),
Divergent(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnverifiedReason {
Disagreement(String),
ClaimNotEvaluable,
QuadratureInconclusive,
NoAdmissiblePoint,
}
impl fmt::Display for ProbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProbError::InvalidParameter {
parameter,
requirement,
} => write!(
f,
"prob: parameter `{parameter}` violates the constraint {requirement}"
),
ProbError::Unsupported(msg) => write!(f, "prob: unsupported: {msg}"),
ProbError::IntegralDidNotClose(msg) => {
write!(f, "prob: the reduction integral did not close: {msg}")
}
ProbError::NoClosedForm { quantity, missing } => write!(
f,
"prob: no closed form for {quantity} — it requires {missing}, \
which is not a registered primitive"
),
ProbError::Unverified(reason) => match reason {
UnverifiedReason::Disagreement(detail) => write!(
f,
"prob: a closed form was computed and withheld — it disagrees \
with quadrature of its own defining integral ({detail})"
),
UnverifiedReason::ClaimNotEvaluable => write!(
f,
"prob: a closed form was computed and withheld — it could not be \
evaluated to a number at any admissible parameter point"
),
UnverifiedReason::QuadratureInconclusive => write!(
f,
"prob: a closed form was computed and withheld — the verifying \
quadrature did not converge, so the check could not have failed"
),
UnverifiedReason::NoAdmissiblePoint => write!(
f,
"prob: a closed form was computed and withheld — no admissible \
parameter point could be constructed to check it at"
),
},
ProbError::Divergent(msg) => write!(f, "prob: the quantity does not exist: {msg}"),
}
}
}
impl std::error::Error for ProbError {}
impl AlkahestError for ProbError {
fn code(&self) -> &'static str {
match self {
ProbError::InvalidParameter { .. } => "E-PROB-001",
ProbError::Unsupported(_) => "E-PROB-002",
ProbError::IntegralDidNotClose(_) => "E-PROB-003",
ProbError::NoClosedForm { .. } => "E-PROB-004",
ProbError::Unverified(_) => "E-PROB-005",
ProbError::Divergent(_) => "E-PROB-006",
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
ProbError::InvalidParameter { .. } => Some(
"pass a parameter inside the distribution's constraint, or leave it symbolic \
(a symbolic parameter is carried as a side condition on `constraints` \
rather than decided)",
),
ProbError::Unsupported(_) => Some(
"reduce the query to a supported shape: a polynomial or exponential `f` for \
`expectation`, an affine combination for `expectation_affine`",
),
ProbError::IntegralDidNotClose(_) => Some(
"the named integral has no antiderivative this integrator found; integrate it \
numerically, or ask for a moment/CDF that is in the closed-form table",
),
ProbError::NoClosedForm { .. } => Some(
"the closed form needs a special function this library does not implement; \
use an integer shape parameter where that collapses the special function to \
a finite sum (Erlang, integer-parameter Beta), or integrate numerically",
),
ProbError::Unverified(_) => Some(
"an unconfirmed closed form is withheld rather than returned; report the \
distribution and query as a minimal failing example",
),
ProbError::Divergent(_) => Some(
"the integral defining this expectation does not converge — there is no value \
to return; restrict `f` so that `f(x)·p(x)` is integrable over the support",
),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Support {
Real,
Positive,
Interval(ExprId, ExprId),
IntegersUpTo(u32),
NonNegativeIntegers,
}
impl Support {
pub fn is_discrete(&self) -> bool {
matches!(
self,
Support::IntegersUpTo(_) | Support::NonNegativeIntegers
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DistKind {
Normal,
LogNormal,
Uniform,
Exponential,
Gamma,
Beta,
Bernoulli,
Binomial,
Poisson,
}
impl DistKind {
pub fn name(self) -> &'static str {
match self {
DistKind::Normal => "Normal",
DistKind::LogNormal => "LogNormal",
DistKind::Uniform => "Uniform",
DistKind::Exponential => "Exponential",
DistKind::Gamma => "Gamma",
DistKind::Beta => "Beta",
DistKind::Bernoulli => "Bernoulli",
DistKind::Binomial => "Binomial",
DistKind::Poisson => "Poisson",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Distribution {
kind: DistKind,
params: Vec<ExprId>,
}
impl Distribution {
pub fn kind(&self) -> DistKind {
self.kind
}
pub fn params(&self) -> &[ExprId] {
&self.params
}
pub fn normal(mu: ExprId, sigma: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_positive(sigma, "sigma", pool)?;
Ok(Distribution {
kind: DistKind::Normal,
params: vec![mu, sigma],
})
}
pub fn log_normal(mu: ExprId, sigma: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_positive(sigma, "sigma", pool)?;
Ok(Distribution {
kind: DistKind::LogNormal,
params: vec![mu, sigma],
})
}
pub fn uniform(a: ExprId, b: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_less(a, b, "b", "a < b", pool)?;
Ok(Distribution {
kind: DistKind::Uniform,
params: vec![a, b],
})
}
pub fn exponential(lambda: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_positive(lambda, "lambda", pool)?;
Ok(Distribution {
kind: DistKind::Exponential,
params: vec![lambda],
})
}
pub fn gamma(k: ExprId, theta: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_positive(k, "k", pool)?;
dists::require_positive(theta, "theta", pool)?;
Ok(Distribution {
kind: DistKind::Gamma,
params: vec![k, theta],
})
}
pub fn beta(alpha: ExprId, beta: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_positive(alpha, "alpha", pool)?;
dists::require_positive(beta, "beta", pool)?;
Ok(Distribution {
kind: DistKind::Beta,
params: vec![alpha, beta],
})
}
pub fn bernoulli(p: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_probability(p, "p", pool)?;
Ok(Distribution {
kind: DistKind::Bernoulli,
params: vec![p],
})
}
pub fn binomial(n: ExprId, p: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_count(n, "n", pool)?;
dists::require_probability(p, "p", pool)?;
Ok(Distribution {
kind: DistKind::Binomial,
params: vec![n, p],
})
}
pub fn poisson(lambda: ExprId, pool: &ExprPool) -> Result<Self, ProbError> {
dists::require_positive(lambda, "lambda", pool)?;
Ok(Distribution {
kind: DistKind::Poisson,
params: vec![lambda],
})
}
pub fn support(&self, pool: &ExprPool) -> Support {
dists::support(self, pool)
}
pub fn constraints(&self, pool: &ExprPool) -> Vec<ExprId> {
dists::constraints(self, pool)
}
pub fn pdf(&self, x: ExprId, pool: &ExprPool) -> ExprId {
dists::pdf(self, x, pool)
}
pub fn mean(&self, pool: &ExprPool) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
moments::mean(self, pool)
}
pub fn variance(
&self,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
moments::variance(self, pool)
}
pub fn moment(
&self,
n: u32,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
moments::moment(self, n, pool)
}
pub fn cdf(
&self,
x: ExprId,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
moments::cdf(self, x, pool)
}
pub fn characteristic_function(
&self,
t: ExprId,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
charfun::characteristic_function(self, t, pool)
}
pub fn entropy(
&self,
base: Option<ExprId>,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
entropy::entropy(self, base, pool)
}
pub fn quantile(
&self,
p: ExprId,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
moments::quantile(self, p, pool)
}
pub fn moment_generating_function(
&self,
t: ExprId,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
genfun::moment_generating_function(self, t, pool)
}
pub fn cumulant_generating_function(
&self,
t: ExprId,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
genfun::cumulant_generating_function(self, t, pool)
}
pub fn probability_generating_function(
&self,
z: ExprId,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
genfun::probability_generating_function(self, z, pool)
}
pub fn factorial_moment(
&self,
n: u32,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
genfun::factorial_moment(self, n, pool)
}
pub fn cumulant(
&self,
n: u32,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
genfun::cumulant(self, n, pool)
}
pub fn skewness(
&self,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
genfun::skewness(self, pool)
}
pub fn excess_kurtosis(
&self,
pool: &ExprPool,
) -> Result<crate::deriv::DerivedExpr<ExprId>, ProbError> {
genfun::excess_kurtosis(self, pool)
}
}
pub const MAX_MOMENT_ORDER: u32 = 12;
pub(crate) fn pi(pool: &ExprPool) -> ExprId {
pool.symbol("pi", Domain::Real)
}
pub(crate) fn sub(a: ExprId, b: ExprId, pool: &ExprPool) -> ExprId {
let neg = pool.integer(-1);
pool.add(vec![a, pool.mul(vec![neg, b])])
}
pub(crate) fn div(a: ExprId, b: ExprId, pool: &ExprPool) -> ExprId {
let inv = pool.pow(b, pool.integer(-1));
pool.mul(vec![a, inv])
}