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 super::continuous::{ChiSquared, Exponential, Gamma, Normal};
use super::discrete::{Bernoulli, Binomial, NegativeBinomial, Poisson};
use super::events::{Rel, flatten_relations};
use super::family::Distribution;
use super::rv::RandomVariable;
use super::support::{Kind, Support, is_neg_inf};
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::invalid_argument(operation, reason)
}
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();
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;
}
let ctx = a.context();
let (da, db) = (a.distribution(), b.distribution());
if let (Some((n, p)), Some((m, q))) = (as_binomial(da), as_binomial(db))
&& p == q
{
return Some(Distribution::binomial((n + m).simplify(), p));
}
if let (Some(x), Some(y)) = (da.downcast_ref::<Normal>(), db.downcast_ref::<Normal>()) {
return Some(Distribution::normal(
(&x.mean + &y.mean).simplify(),
(x.std.powi(2) + y.std.powi(2)).sqrt().simplify(),
));
}
if let (Some(x), Some(y)) = (da.downcast_ref::<Poisson>(), db.downcast_ref::<Poisson>()) {
return Some(Distribution::poisson((&x.rate + &y.rate).simplify()));
}
if let (Some(x), Some(y)) = (
da.downcast_ref::<NegativeBinomial>(),
db.downcast_ref::<NegativeBinomial>(),
) && x.p == y.p
{
return Some(Distribution::negative_binomial(
(&x.r + &y.r).simplify(),
x.p.clone(),
));
}
let (k1, t1) = as_gamma(da)?;
let (k2, t2) = as_gamma(db)?;
if t1 != t2 {
return None;
}
let shape = (k1 + k2).simplify();
if da.downcast_ref::<ChiSquared>().is_some() && db.downcast_ref::<ChiSquared>().is_some() {
return Some(Distribution::chi_squared((shape * ctx.int(2)).simplify()));
}
Some(Distribution::gamma(shape, t1))
}
fn as_gamma(d: &Distribution) -> Option<(Ex, Ex)> {
if let Some(g) = d.downcast_ref::<Gamma>() {
return Some((g.shape.clone(), g.scale.clone()));
}
if let Some(e) = d.downcast_ref::<Exponential>() {
let ctx = e.rate.context();
return Some((ctx.one(), (ctx.one() / &e.rate).simplify()));
}
if let Some(c) = d.downcast_ref::<ChiSquared>() {
let ctx = c.dof.context();
return Some(((&c.dof / ctx.int(2)).simplify(), ctx.int(2)));
}
None
}
fn as_binomial(d: &Distribution) -> Option<(Ex, Ex)> {
if let Some(b) = d.downcast_ref::<Binomial>() {
return Some((b.n.clone(), b.p.clone()));
}
if let Some(b) = d.downcast_ref::<Bernoulli>() {
return Some((b.p.context().one(), b.p.clone()));
}
None
}
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 (Some(x), Some(y)) = (
lower.distribution().downcast_ref::<Normal>(),
upper.distribution().downcast_ref::<Normal>(),
) {
let diff = Distribution::normal(
(&x.mean - &y.mean).simplify(),
(x.std.powi(2) + y.std.powi(2)).sqrt().simplify(),
);
return diff.probability_of(&Support::from_pieces(
Kind::Continuous,
vec![super::support::Piece::Interval(
crate::base::interval::Interval::open(ctx.neg_infinity(), ctx.zero()),
)],
));
}
let (sl, su) = (lower.support(), upper.support());
let (Some(iv_l), Some(iv_u)) = (sl.as_interval(), su.as_interval()) else {
return Err(not_implemented());
};
let (lo_l, hi_l) = (&iv_l.lower, &iv_l.upper);
let (lo_u, hi_u) = (&iv_u.lower, &iv_u.upper);
if sl.kind() != Kind::Continuous || su.kind() != Kind::Continuous {
return Err(not_implemented());
}
let t = lower.symbol();
let (Some(_), Some(cdf_upper)) = (
lower.distribution().family().cdf(t),
upper.distribution().family().cdf(t),
) else {
return Err(not_implemented());
};
let mut total = ctx.zero();
if !is_neg_inf(lo_u) {
total += lower.probability(&t.lt(lo_u))?;
}
let both = Support::interval(lo_l.clone(), hi_l.clone())
.intersect(&Support::interval(lo_u.clone(), hi_u.clone()))
.ok_or_else(not_implemented)?;
let integrand = lower.density(t) * (ctx.one() - cdf_upper);
total += lower.distribution().integrate_over(&integrand, t, &both);
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 region = x.event_region(event)?;
let p = x.probability(event)?;
if p.is_zero() == Some(true) {
return Err(invalid(
OP,
format!("the event `{event}` has probability zero"),
));
}
let numerator = x.distribution().expectation_over(g, x.symbol(), ®ion)?;
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 {
x.entropy()
}