use crate::api::context::Context;
use crate::api::expr::{BoolEx, Ex, Expr, Sort};
use crate::api::poly_ex::Poly;
use crate::base::errors::SymplexError;
use crate::base::node::ExprNode;
use super::continuous::ContinuousFamily;
use super::discrete::DiscreteFamily;
use super::rv::{Distribution, RandomVariable, Support};
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::InvalidArgument {
operation,
reason: reason.into(),
}
}
fn check_vars(operation: &'static str, vars: &[&RandomVariable]) -> Result<Context, SymplexError> {
let Some(first) = vars.first() else {
return Err(invalid(
operation,
"at least one random variable is required",
));
};
let ctx = first.context().clone();
for (i, v) in vars.iter().enumerate() {
if v.context().id != ctx.id {
return Err(invalid(
operation,
"the random variables must live in the same context",
));
}
if vars[..i].iter().any(|u| u.symbol() == v.symbol()) {
return Err(invalid(
operation,
format!(
"`{}` is listed twice; the variables must be distinct (and independent)",
v.symbol()
),
));
}
}
Ok(ctx)
}
fn check_context<S: Sort>(
operation: &'static str,
ctx: &Context,
e: &Expr<S>,
) -> Result<(), SymplexError> {
if e.ctx_id == ctx.id {
Ok(())
} else {
Err(invalid(
operation,
"the expression and the random variables live in different contexts",
))
}
}
pub fn expectation(vars: &[&RandomVariable], g: &Ex) -> Result<Ex, SymplexError> {
const OP: &str = "stats::expectation";
let ctx = check_vars(OP, vars)?;
check_context(OP, &ctx, g)?;
let present: Vec<&RandomVariable> = vars
.iter()
.copied()
.filter(|v| g.contains(v.symbol()))
.collect();
if present.is_empty() {
return Ok(g.clone());
}
let expanded = g.expand();
let symbols: Vec<&Ex> = present.iter().map(|v| v.symbol()).collect();
if let Ok(poly) = Poly::try_new(&expanded, &symbols) {
let mut acc = ctx.zero();
for (exps, coeff) in poly.terms() {
let mut term = coeff;
for (v, &n) in present.iter().zip(&exps) {
if n > 0 {
term *= v.moment(n);
}
}
acc += term;
}
return Ok(acc.simplify());
}
let (poly_vars, other_vars): (Vec<&RandomVariable>, Vec<&RandomVariable>) = present
.iter()
.partition(|v| Poly::try_new(&expanded, &[v.symbol()]).is_ok());
let mut acc = g.clone();
for v in poly_vars.iter().chain(&other_vars) {
acc = v.expectation(&acc);
}
Ok(acc.simplify())
}
pub fn variance(vars: &[&RandomVariable], g: &Ex) -> Result<Ex, SymplexError> {
let mean = expectation(vars, g)?;
let second = expectation(vars, &g.powi(2))?;
Ok((second - mean.powi(2)).simplify())
}
pub fn covariance(vars: &[&RandomVariable], g: &Ex, h: &Ex) -> Result<Ex, SymplexError> {
let joint = expectation(vars, &(g * h))?;
let eg = expectation(vars, g)?;
let eh = expectation(vars, h)?;
Ok((joint - eg * eh).simplify())
}
pub fn correlation(vars: &[&RandomVariable], g: &Ex, h: &Ex) -> Result<Ex, SymplexError> {
const OP: &str = "stats::correlation";
let cov = covariance(vars, g, h)?;
let vg = variance(vars, g)?;
let vh = variance(vars, h)?;
if vg.is_zero() == Some(true) || vh.is_zero() == Some(true) {
return Err(invalid(
OP,
"the correlation of a degenerate (zero-variance) quantity is undefined",
));
}
Ok((cov / (vg * vh).sqrt()).simplify())
}
pub fn sum_distribution(a: &RandomVariable, b: &RandomVariable) -> Option<Distribution> {
if a.symbol() == b.symbol() || a.context().id != b.context().id {
return None;
}
if let (Some((n, p)), Some((m, q))) =
(as_binomial(a.distribution()), as_binomial(b.distribution()))
&& p == q
{
return Some(Distribution::binomial((n + m).simplify(), p));
}
match (a.distribution(), b.distribution()) {
(
Distribution::Continuous(ContinuousFamily::Normal { mean: m1, std: s1 }),
Distribution::Continuous(ContinuousFamily::Normal { mean: m2, std: s2 }),
) => Some(Distribution::normal(
(m1 + m2).simplify(),
(s1.powi(2) + s2.powi(2)).sqrt().simplify(),
)),
(
Distribution::Discrete(DiscreteFamily::Poisson { rate: l1 }),
Distribution::Discrete(DiscreteFamily::Poisson { rate: l2 }),
) => Some(Distribution::poisson((l1 + l2).simplify())),
(
Distribution::Discrete(DiscreteFamily::NegativeBinomial { r: r1, p }),
Distribution::Discrete(DiscreteFamily::NegativeBinomial { r: r2, p: q }),
) if p == q => Some(Distribution::negative_binomial(
(r1 + r2).simplify(),
p.clone(),
)),
(Distribution::Continuous(f), Distribution::Continuous(g)) => {
let (k1, t1) = as_gamma(f)?;
let (k2, t2) = as_gamma(g)?;
if t1 != t2 {
return None;
}
let shape = (k1 + k2).simplify();
if matches!(
(f, g),
(
ContinuousFamily::ChiSquared { .. },
ContinuousFamily::ChiSquared { .. }
)
) {
return Some(Distribution::chi_squared(
(shape * a.context().int(2)).simplify(),
));
}
Some(Distribution::gamma(shape, t1))
}
_ => None,
}
}
fn as_gamma(f: &ContinuousFamily) -> Option<(Ex, Ex)> {
match f {
ContinuousFamily::Gamma { shape, scale } => Some((shape.clone(), scale.clone())),
ContinuousFamily::Exponential { rate } => {
let ctx = rate.context();
Some((ctx.one(), (ctx.one() / rate).simplify()))
}
ContinuousFamily::ChiSquared { dof } => {
let ctx = dof.context();
Some(((dof / ctx.int(2)).simplify(), ctx.int(2)))
}
_ => None,
}
}
fn as_binomial(d: &Distribution) -> Option<(Ex, Ex)> {
match d {
Distribution::Discrete(DiscreteFamily::Binomial { n, p }) => Some((n.clone(), p.clone())),
Distribution::Discrete(DiscreteFamily::Bernoulli { p }) => {
Some((p.context().one(), p.clone()))
}
_ => None,
}
}
#[derive(Clone, Copy)]
enum Rel {
Gt,
Ge,
Eq,
}
struct Relation {
kind: Rel,
lhs: Ex,
rhs: Ex,
node: BoolEx,
}
fn flatten_relations(event: &BoolEx) -> Option<Vec<Relation>> {
let inner = event.inner.read();
let arena = &inner.arena;
let mut rels = Vec::new();
let mut stack = vec![event.raw_id()];
while let Some(id) = stack.pop() {
let (kind, l, r) = match arena.node(id) {
ExprNode::And(parts) => {
stack.extend(parts.iter().copied());
continue;
}
ExprNode::Gt(l, r) => (Rel::Gt, *l, *r),
ExprNode::Ge(l, r) => (Rel::Ge, *l, *r),
ExprNode::Eq_(l, r) => (Rel::Eq, *l, *r),
_ => return None,
};
rels.push(Relation {
kind,
lhs: event.wrap_as(l),
rhs: event.wrap_as(r),
node: event.wrap(id),
});
}
Some(rels)
}
#[derive(Default)]
struct Bounds {
lo: Option<Ex>,
hi: Option<Ex>,
lo_strict: bool,
hi_strict: bool,
point: Option<Ex>,
}
impl Bounds {
fn raise_lo(&mut self, a: Ex, strict: bool) {
self.lo = Some(match self.lo.take() {
Some(l0) => l0.max_with(&a),
None => a,
});
self.lo_strict |= strict;
}
fn lower_hi(&mut self, b: Ex, strict: bool) {
self.hi = Some(match self.hi.take() {
Some(h) => h.min_with(&b),
None => b,
});
self.hi_strict |= strict;
}
}
fn event_bounds(x: &RandomVariable, event: &BoolEx) -> Result<Bounds, SymplexError> {
let unsupported = || {
SymplexError::NotImplemented(format!(
"the event `{event}`: only relations `X < a`, `X ≤ a`, `X > a`, `X ≥ a`, `X = a` in \
the random variable and their conjunctions are supported"
))
};
let rels = flatten_relations(event).ok_or_else(unsupported)?;
let sym = x.symbol();
let mut bounds = Bounds::default();
for rel in rels {
let (bound, x_on_left) = if rel.lhs == *sym && !rel.rhs.contains(sym) {
(rel.rhs, true)
} else if rel.rhs == *sym && !rel.lhs.contains(sym) {
(rel.lhs, false)
} else {
return Err(unsupported());
};
match (rel.kind, x_on_left) {
(Rel::Gt, true) => bounds.raise_lo(bound, true),
(Rel::Ge, true) => bounds.raise_lo(bound, false),
(Rel::Gt, false) => bounds.lower_hi(bound, true),
(Rel::Ge, false) => bounds.lower_hi(bound, false),
(Rel::Eq, _) => bounds.point = Some(bound),
}
}
Ok(bounds)
}
fn merge_bounds(a: Option<Ex>, b: Option<Ex>, f: impl Fn(&Ex, &Ex) -> Ex) -> Option<Ex> {
match (a, b) {
(Some(a), Some(b)) => Some(f(&a, &b)),
(Some(a), None) | (None, Some(a)) => Some(a),
(None, None) => None,
}
}
fn restricted_expectation(x: &RandomVariable, g: &Ex, bounds: &Bounds) -> Ex {
let ctx = x.context();
let (slo, shi) = match x.support() {
Support::Continuous { lo, hi } | Support::Discrete { lo, hi } => (lo, hi),
Support::Finite(values) => {
let t = x.symbol();
let inside = |v: &Ex| -> bool {
let above = bounds.lo.as_ref().is_none_or(|a| {
let d = v - a;
(if bounds.lo_strict {
d.is_positive()
} else {
d.is_nonnegative()
}) == Some(true)
});
let below = bounds.hi.as_ref().is_none_or(|b| {
let d = b - v;
(if bounds.hi_strict {
d.is_positive()
} else {
d.is_nonnegative()
}) == Some(true)
});
above && below
};
let mut acc = ctx.zero();
for v in values.iter().filter(|v| inside(v)) {
acc += g.subs(t, v).eval() * x.density(v).eval();
}
return acc.simplify();
}
};
let lo = merge_bounds(bounds.lo.clone(), slo, |a, s| a.max_with(s));
let hi = merge_bounds(bounds.hi.clone(), shi, |b, s| b.min_with(s));
let t = x.symbol();
let integrand = g * x.density(t);
match x.distribution() {
Distribution::Continuous(_) => {
let lo = lo.unwrap_or_else(|| ctx.neg_infinity());
let hi = hi.unwrap_or_else(|| ctx.infinity());
integrand.integrate_definite(t, &lo, &hi).simplify()
}
Distribution::Discrete(_) => {
let one = ctx.one();
let lo = lo.map(|a| {
let a = if bounds.lo_strict {
a.floor() + &one
} else {
a.ceiling()
};
a.simplify()
});
let hi = hi.map(|b| {
let b = if bounds.hi_strict {
b.ceiling() - &one
} else {
b.floor()
};
b.simplify()
});
let lo = lo.unwrap_or_else(|| ctx.neg_infinity());
let hi = hi.unwrap_or_else(|| ctx.infinity());
integrand.summation(t, &lo, &hi).simplify()
}
}
}
pub fn probability(vars: &[&RandomVariable], event: &BoolEx) -> Result<Ex, SymplexError> {
const OP: &str = "stats::probability";
let ctx = check_vars(OP, vars)?;
check_context(OP, &ctx, event)?;
let unsupported = || {
SymplexError::NotImplemented(format!(
"probability of the joint event `{event}`: supported are conjunctions of relations \
each in a single listed variable (`X > a`, `a ≤ X`, `X = a`, …), and a single \
ordering `X < Y` / `X ≤ Y` of two continuous variables"
))
};
let rels = flatten_relations(event).ok_or_else(unsupported)?;
let mut per_var: Vec<Vec<BoolEx>> = vec![Vec::new(); vars.len()];
let mut orderings: Vec<(Rel, usize, usize)> = Vec::new();
for rel in rels {
let mentioned: Vec<usize> = (0..vars.len())
.filter(|&i| {
let s = vars[i].symbol();
rel.lhs.contains(s) || rel.rhs.contains(s)
})
.collect();
match mentioned.as_slice() {
[i] => per_var[*i].push(rel.node),
[i, j] => {
let (si, sj) = (vars[*i].symbol(), vars[*j].symbol());
if rel.lhs == *si && rel.rhs == *sj {
orderings.push((rel.kind, *i, *j));
} else if rel.lhs == *sj && rel.rhs == *si {
orderings.push((rel.kind, *j, *i));
} else {
return Err(unsupported());
}
}
_ => return Err(unsupported()),
}
}
if !orderings.is_empty() {
let [(kind, l, r)] = orderings.as_slice() else {
return Err(unsupported());
};
if per_var.iter().any(|parts| !parts.is_empty()) {
return Err(unsupported());
}
let (left, right) = (vars[*l], vars[*r]);
return match kind {
Rel::Gt | Rel::Ge => probability_less(right, left),
Rel::Eq => {
if left.distribution().is_continuous() && right.distribution().is_continuous() {
Ok(ctx.zero())
} else {
Err(unsupported())
}
}
};
}
let mut total = ctx.one();
for (v, parts) in vars.iter().zip(per_var) {
let mut parts = parts.into_iter();
let Some(first) = parts.next() else {
continue;
};
let conjunction = parts.fold(first, |acc, b| acc.and(&b));
total *= v.probability(&conjunction)?;
}
Ok(total.simplify())
}
fn probability_less(lower: &RandomVariable, upper: &RandomVariable) -> Result<Ex, SymplexError> {
let ctx = lower.context();
let not_implemented = || {
SymplexError::NotImplemented(format!(
"P({} < {}): the ordering of two variables is supported for two continuous \
variables whose distribution functions have closed forms",
lower.symbol(),
upper.symbol()
))
};
if let (
Distribution::Continuous(ContinuousFamily::Normal { mean: m1, std: s1 }),
Distribution::Continuous(ContinuousFamily::Normal { mean: m2, std: s2 }),
) = (lower.distribution(), upper.distribution())
{
let diff = RandomVariable::new(
ctx,
"_stats_diff",
Distribution::normal(
(m1 - m2).simplify(),
(s1.powi(2) + s2.powi(2)).sqrt().simplify(),
),
);
return diff.probability(&diff.symbol().lt(&ctx.zero()));
}
let (Support::Continuous { lo: lo_l, hi: hi_l }, Support::Continuous { lo: lo_u, hi: hi_u }) =
(lower.support(), upper.support())
else {
return Err(not_implemented());
};
let t = lower.symbol();
let (Some(_), Some(cdf_upper)) = (lower.distribution().cdf(t), upper.distribution().cdf(t))
else {
return Err(not_implemented());
};
let mut total = ctx.zero();
if let Some(a) = &lo_u {
total += lower.probability(&t.lt(a))?;
}
let lo = merge_bounds(lo_l, lo_u, |a, b| a.max_with(b))
.map_or_else(|| ctx.neg_infinity(), |e| e.simplify());
let hi = merge_bounds(hi_l, hi_u, |a, b| a.min_with(b))
.map_or_else(|| ctx.infinity(), |e| e.simplify());
let integrand = lower.density(t) * (ctx.one() - cdf_upper);
total += integrand.integrate_definite(t, &lo, &hi);
Ok(total.simplify())
}
pub fn conditional_expectation(
x: &RandomVariable,
g: &Ex,
event: &BoolEx,
) -> Result<Ex, SymplexError> {
const OP: &str = "stats::conditional_expectation";
let ctx = x.context();
check_context(OP, ctx, g)?;
check_context(OP, ctx, event)?;
let bounds = event_bounds(x, event)?;
let p = x.probability(event)?;
if p.is_zero() == Some(true) {
return Err(invalid(
OP,
format!("the event `{event}` has probability zero"),
));
}
if let Some(a) = &bounds.point {
return Ok(g.subs(x.symbol(), a).simplify());
}
let numerator = restricted_expectation(x, g, &bounds);
Ok((numerator / p).simplify())
}
pub fn conditional_probability(
x: &RandomVariable,
event: &BoolEx,
given: &BoolEx,
) -> Result<Ex, SymplexError> {
const OP: &str = "stats::conditional_probability";
let ctx = x.context();
check_context(OP, ctx, event)?;
check_context(OP, ctx, given)?;
let p_given = x.probability(given)?;
if p_given.is_zero() == Some(true) {
return Err(invalid(
OP,
format!("the conditioning event `{given}` has probability zero"),
));
}
let p_both = x.probability(&event.and(given))?;
Ok((p_both / p_given).simplify())
}
pub fn entropy(x: &RandomVariable) -> Ex {
let t = x.symbol();
let neg_log_density = (-x.density(t).ln().expand_log()).simplify();
x.expectation(&neg_log_density)
}