use std::collections::HashMap;
use crate::deriv::{DerivationLog, DerivedExpr, RewriteStep, SideCondition};
use crate::integrate::integrate_definite;
use crate::kernel::{subs, ExprData, ExprId, ExprPool};
use crate::simplify::simplify;
use super::dists::{self, Sign};
use super::verify;
use super::{sub, Distribution, ProbError, Support};
const MAX_PIECES: usize = 4;
const MAX_TERMS: usize = 64;
pub fn expectation(
f: ExprId,
x: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let definition = verify::definition_integrand(f, x, dist, pool);
if let Some(divergent) = verify::convergence_probe(definition, x, dist, pool) {
return Err(divergent);
}
let support = dist.support(pool);
let (claim, mut log, breaks) = if support.is_discrete() {
let (c, l) = discrete_expectation(f, x, dist, &support, pool)?;
(c, l, Vec::new())
} else {
continuous_expectation(f, x, dist, pool)?
};
let claim = simplify(claim, pool).value;
let evidence = verify::check_with_breaks(claim, definition, x, dist, &breaks, pool)?;
log.push(verify::evidence_step(&evidence, claim, dist, pool));
super::stash_prob_side_conditions(super::conditions_of(&log));
Ok(DerivedExpr::with_log(claim, log))
}
fn discrete_expectation(
f: ExprId,
x: ExprId,
dist: &Distribution,
support: &Support,
pool: &ExprPool,
) -> Result<(ExprId, DerivationLog), ProbError> {
let mut log = DerivationLog::new();
match support {
Support::IntegersUpTo(n) => {
let mut terms = Vec::with_capacity(*n as usize + 1);
for k in 0..=*n {
let kk = pool.integer(k);
let mut m = HashMap::new();
m.insert(x, kk);
let fk = subs(f, &m, pool);
terms.push(pool.mul(vec![fk, dists::pdf(dist, kk, pool)]));
}
let total = simplify(pool.add(terms), pool).value;
log.push(RewriteStep::simple("prob_finite_expectation_sum", f, total));
Ok((total, log))
}
Support::NonNegativeIntegers => {
let Some(coeffs) = polynomial_coefficients(f, x, pool) else {
return Err(ProbError::Unsupported(format!(
"E[f(X)] over {} needs f to be a polynomial in the variate: the support is \
countably infinite and a general f has no closed-form sum here",
dist.kind().name()
)));
};
if coeffs.len() > super::MAX_MOMENT_ORDER as usize + 1 {
return Err(ProbError::Unsupported(format!(
"polynomial degree {} exceeds MAX_MOMENT_ORDER = {}",
coeffs.len() - 1,
super::MAX_MOMENT_ORDER
)));
}
let mut terms = Vec::new();
for (i, c) in coeffs.iter().enumerate() {
if is_zero(*c, pool) {
continue;
}
let m = super::moments::raw_moment_unchecked(dist, i as u32, pool)?;
terms.push(pool.mul(vec![*c, m]));
}
let total = simplify(pool.add(terms), pool).value;
log.push(RewriteStep::simple("prob_moment_expansion", f, total));
Ok((total, log))
}
_ => unreachable!("only discrete supports reach here"),
}
}
fn is_zero(e: ExprId, pool: &ExprPool) -> bool {
matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0)
}
fn polynomial_coefficients(f: ExprId, x: ExprId, pool: &ExprPool) -> Option<Vec<ExprId>> {
fn go(e: ExprId, x: ExprId, pool: &ExprPool, depth: u32) -> Option<Vec<ExprId>> {
if depth > 16 {
return None;
}
if !dists::contains(e, x, pool) {
return Some(vec![e]);
}
if e == x {
return Some(vec![pool.integer(0), pool.integer(1)]);
}
match pool.get(e) {
ExprData::Add(args) => {
let mut acc: Vec<ExprId> = vec![pool.integer(0)];
for a in args {
let c = go(a, x, pool, depth + 1)?;
if c.len() > acc.len() {
acc.resize(c.len(), pool.integer(0));
}
for (i, v) in c.iter().enumerate() {
acc[i] = pool.add(vec![acc[i], *v]);
}
}
Some(acc)
}
ExprData::Mul(args) => {
let mut acc: Vec<ExprId> = vec![pool.integer(1)];
for a in args {
let c = go(a, x, pool, depth + 1)?;
if acc.len() + c.len() - 1 > super::MAX_MOMENT_ORDER as usize + 1 {
return None;
}
let mut next = vec![pool.integer(0); acc.len() + c.len() - 1];
for (i, u) in acc.iter().enumerate() {
for (j, v) in c.iter().enumerate() {
next[i + j] = pool.add(vec![next[i + j], pool.mul(vec![*u, *v])]);
}
}
acc = next;
}
Some(acc)
}
ExprData::Pow { base, exp } => {
let ExprData::Integer(n) = pool.get(exp) else {
return None;
};
let n = n.0.to_u32()?;
if n > super::MAX_MOMENT_ORDER {
return None;
}
let b = go(base, x, pool, depth + 1)?;
let mut acc = vec![pool.integer(1)];
for _ in 0..n {
let mut next = vec![pool.integer(0); acc.len() + b.len() - 1];
for (i, u) in acc.iter().enumerate() {
for (j, v) in b.iter().enumerate() {
next[i + j] = pool.add(vec![next[i + j], pool.mul(vec![*u, *v])]);
}
}
acc = next;
}
Some(acc)
}
_ => None,
}
}
let c = go(f, x, pool, 0)?;
Some(c.into_iter().map(|e| simplify(e, pool).value).collect())
}
struct Piece {
lo: ExprId,
hi: ExprId,
f: ExprId,
conditions: Vec<SideCondition>,
}
type ContinuousResult = (ExprId, DerivationLog, Vec<ExprId>);
fn continuous_expectation(
f: ExprId,
x: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Result<ContinuousResult, ProbError> {
let mut log = DerivationLog::new();
let Some(red) = dists::reduction(dist, x, pool) else {
return Err(ProbError::Unsupported(
"no reduction for this distribution".to_string(),
));
};
let (support_lo, support_hi) = support_bounds(dist, pool);
let pieces = split_payoff(f, x, support_lo, support_hi, pool)?;
if pieces.len() > 1 {
log.push(RewriteStep::simple("prob_split_payoff_at_kink", f, f));
}
let mut total: Vec<ExprId> = Vec::new();
for piece in &pieces {
let z_lo = map_bound(piece.lo, x, &red, support_lo, red.lo, pool);
let z_hi = map_bound(piece.hi, x, &red, support_hi, red.hi, pool);
let mut m = HashMap::new();
m.insert(x, red.x_of_z);
let f_of_z = subs(piece.f, &m, pool);
let integrand = simplify(pool.mul(vec![f_of_z, red.density]), pool).value;
for (constant, kernel) in prepare_terms(integrand, red.z, pool)? {
if is_zero(kernel, pool) || is_zero(constant, pool) {
continue;
}
let (kernel, constant, z_lo, z_hi) =
match complete_the_square(kernel, red.z, z_lo, z_hi, pool) {
Some((k, extra, lo, hi)) => {
log.push(RewriteStep::simple("prob_complete_the_square", kernel, k));
(k, pool.mul(vec![constant, extra]), lo, hi)
}
None => (kernel, constant, z_lo, z_hi),
};
let piece_value = integrate_definite(kernel, red.z, z_lo, z_hi, pool).map_err(|e| {
ProbError::IntegralDidNotClose(format!(
"∫_{{{}}}^{{{}}} {} d{} — {e}",
pool.display(z_lo),
pool.display(z_hi),
pool.display(kernel),
pool.display(red.z)
))
})?;
log = log.merge(piece_value.log);
total.push(pool.mul(vec![constant, piece_value.value]));
}
for c in &piece.conditions {
log.push(RewriteStep::with_conditions(
"prob_kink_inside_support",
piece.f,
piece.f,
vec![c.clone()],
));
}
}
let value = if total.is_empty() {
pool.integer(0)
} else {
pool.add(total)
};
let breaks = pieces.iter().skip(1).map(|p| p.lo).collect::<Vec<ExprId>>();
Ok((value, log, breaks))
}
fn support_bounds(dist: &Distribution, pool: &ExprPool) -> (ExprId, ExprId) {
let inf = pool.pos_infinity();
let neg_inf = pool.mul(vec![pool.integer(-1), inf]);
match dist.support(pool) {
Support::Real => (neg_inf, inf),
Support::Positive => (pool.integer(0), inf),
Support::Interval(lo, hi) => (lo, hi),
_ => (neg_inf, inf),
}
}
fn map_bound(
bound: ExprId,
x: ExprId,
red: &dists::Reduction,
support_end: ExprId,
reduced_end: ExprId,
pool: &ExprPool,
) -> ExprId {
if bound == support_end {
return reduced_end;
}
let mut m = HashMap::new();
m.insert(x, bound);
simplify(subs(red.z_of_x, &m, pool), pool).value
}
fn split_payoff(
f: ExprId,
x: ExprId,
lo: ExprId,
hi: ExprId,
pool: &ExprPool,
) -> Result<Vec<Piece>, ProbError> {
let mut pieces = vec![Piece {
lo,
hi,
f,
conditions: Vec::new(),
}];
for _ in 0..MAX_PIECES {
let mut split_index = None;
for (i, p) in pieces.iter().enumerate() {
if find_kink(p.f, x, pool).is_some() {
split_index = Some(i);
break;
}
}
let Some(i) = split_index else {
return Ok(pieces);
};
let piece = pieces.remove(i);
let (node, lower_branch, upper_branch, root) =
find_kink(piece.f, x, pool).expect("just found");
let mut conditions = piece.conditions.clone();
if let Some(c) = kink_inside_condition(root, piece.lo, piece.hi, pool) {
conditions.push(c);
}
let below = replace(piece.f, node, lower_branch, pool);
let above = replace(piece.f, node, upper_branch, pool);
pieces.insert(
i,
Piece {
lo: root,
hi: piece.hi,
f: above,
conditions: conditions.clone(),
},
);
pieces.insert(
i,
Piece {
lo: piece.lo,
hi: root,
f: below,
conditions,
},
);
}
Err(ProbError::Unsupported(format!(
"the payoff has more than {MAX_PIECES} kinks in the integration variable"
)))
}
fn find_kink(e: ExprId, x: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId, ExprId, ExprId)> {
match pool.get(e) {
ExprData::Func { name, args } if args.len() == 2 && (name == "max" || name == "min") => {
let (u, v) = (args[0], args[1]);
let d = simplify(sub(u, v, pool), pool).value;
let (slope, intercept) = affine_in(d, x, pool)?;
let slope_sign = dists::sign_of(slope, pool);
let root = simplify(
super::div(pool.mul(vec![pool.integer(-1), intercept]), slope, pool),
pool,
)
.value;
let (below, above) = match (name.as_str(), slope_sign) {
("max", Sign::Positive) => (v, u),
("max", Sign::Negative) => (u, v),
("min", Sign::Positive) => (u, v),
("min", Sign::Negative) => (v, u),
_ => return None,
};
Some((e, below, above, root))
}
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().find_map(|&a| find_kink(a, x, pool))
}
ExprData::Pow { base, exp } => find_kink(base, x, pool).or_else(|| find_kink(exp, x, pool)),
_ => None,
}
}
fn affine_in(e: ExprId, x: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId)> {
let coeffs = polynomial_coefficients(e, x, pool)?;
match coeffs.len() {
0 => None,
1 => None, 2 => Some((coeffs[1], coeffs[0])),
_ => {
if coeffs[2..].iter().all(|&c| is_zero(c, pool)) {
Some((coeffs[1], coeffs[0]))
} else {
None
}
}
}
}
fn kink_inside_condition(
root: ExprId,
lo: ExprId,
hi: ExprId,
pool: &ExprPool,
) -> Option<SideCondition> {
let inf = pool.pos_infinity();
let mut gaps = Vec::new();
if lo != pool.mul(vec![pool.integer(-1), inf]) {
gaps.push(sub(root, lo, pool));
}
if hi != inf {
gaps.push(sub(hi, root, pool));
}
for g in gaps {
if matches!(dists::sign_of(g, pool), Sign::Unknown) {
return Some(SideCondition::Positive(simplify(g, pool).value));
}
}
None
}
fn replace(e: ExprId, from: ExprId, to: ExprId, pool: &ExprPool) -> ExprId {
let mut m = HashMap::new();
m.insert(from, to);
subs(e, &m, pool)
}
fn prepare_terms(
integrand: ExprId,
z: ExprId,
pool: &ExprPool,
) -> Result<Vec<(ExprId, ExprId)>, ProbError> {
let expanded = expand(integrand, pool, 0);
let terms: Vec<ExprId> = match pool.get(expanded) {
ExprData::Add(args) => args.to_vec(),
_ => vec![expanded],
};
if terms.len() > MAX_TERMS {
return Err(ProbError::Unsupported(format!(
"the reduced integrand expanded to {} terms, past the ceiling of {MAX_TERMS}",
terms.len()
)));
}
Ok(terms
.into_iter()
.map(|t| split_constant(t, z, pool))
.collect())
}
fn split_constant(term: ExprId, z: ExprId, pool: &ExprPool) -> (ExprId, ExprId) {
let factors: Vec<ExprId> = match pool.get(term) {
ExprData::Mul(args) => args.to_vec(),
_ => vec![term],
};
let mut constants = Vec::new();
let mut kernels = Vec::new();
let mut exp_args = Vec::new();
for fct in factors {
if !dists::contains(fct, z, pool) {
constants.push(fct);
continue;
}
match pool.get(fct) {
ExprData::Func { name, args } if name == "exp" && args.len() == 1 => {
exp_args.push(args[0])
}
_ => kernels.push(fct),
}
}
if !exp_args.is_empty() {
kernels.push(pool.func("exp", vec![pool.add(exp_args)]));
}
let constant = if constants.is_empty() {
pool.integer(1)
} else {
simplify(pool.mul(constants), pool).value
};
let kernel = if kernels.is_empty() {
pool.integer(1)
} else {
pool.mul(kernels)
};
(constant, kernel)
}
fn complete_the_square(
kernel: ExprId,
z: ExprId,
lo: ExprId,
hi: ExprId,
pool: &ExprPool,
) -> Option<(ExprId, ExprId, ExprId, ExprId)> {
let factors: Vec<ExprId> = match pool.get(kernel) {
ExprData::Mul(args) => args.to_vec(),
_ => vec![kernel],
};
let mut exp_index = None;
for (i, &fct) in factors.iter().enumerate() {
if let ExprData::Func { name, args } = pool.get(fct) {
if name == "exp" && args.len() == 1 {
exp_index = Some((i, args[0]));
break;
}
}
}
let (idx, arg) = exp_index?;
let coeffs = polynomial_coefficients(arg, z, pool)?;
if coeffs.len() != 3 {
return None;
}
let (c0, c1, c2) = (coeffs[0], coeffs[1], coeffs[2]);
if is_zero(c1, pool) {
return None;
}
if !matches!(dists::sign_of(c2, pool), Sign::Negative) {
return None;
}
let two = pool.integer(2);
let four = pool.integer(4);
let shift = super::div(c1, pool.mul(vec![two, c2]), pool);
let constant = pool.func(
"exp",
vec![sub(
c0,
super::div(pool.pow(c1, two), pool.mul(vec![four, c2]), pool),
pool,
)],
);
let new_exp = pool.func("exp", vec![pool.mul(vec![c2, pool.pow(z, two)])]);
let mut rest: Vec<ExprId> = Vec::with_capacity(factors.len());
let mut m = HashMap::new();
m.insert(z, sub(z, shift, pool));
for (i, &fct) in factors.iter().enumerate() {
if i == idx {
continue;
}
rest.push(subs(fct, &m, pool));
}
rest.push(new_exp);
let inf = pool.pos_infinity();
let neg_inf = pool.mul(vec![pool.integer(-1), inf]);
let shift_bound = |b: ExprId| -> ExprId {
if b == inf || b == neg_inf {
b
} else {
simplify(pool.add(vec![b, shift]), pool).value
}
};
Some((
simplify(pool.mul(rest), pool).value,
simplify(constant, pool).value,
shift_bound(lo),
shift_bound(hi),
))
}
fn expand(e: ExprId, pool: &ExprPool, depth: u32) -> ExprId {
if depth > 8 {
return e;
}
match pool.get(e) {
ExprData::Add(args) => pool.add(args.iter().map(|&a| expand(a, pool, depth + 1)).collect()),
ExprData::Mul(args) => {
let parts: Vec<Vec<ExprId>> = args
.iter()
.map(|&a| match pool.get(expand(a, pool, depth + 1)) {
ExprData::Add(inner) => inner.to_vec(),
other => vec![pool.intern(other)],
})
.collect();
let mut acc: Vec<Vec<ExprId>> = vec![Vec::new()];
for part in &parts {
if acc.len() * part.len() > MAX_TERMS {
return e;
}
let mut next = Vec::with_capacity(acc.len() * part.len());
for a in &acc {
for &p in part {
let mut row = a.clone();
row.push(p);
next.push(row);
}
}
acc = next;
}
pool.add(acc.into_iter().map(|row| pool.mul(row)).collect())
}
_ => e,
}
}
pub fn expectation_affine(
expr: ExprId,
variates: &[(ExprId, Distribution)],
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let (coeffs, constant) = affine_decomposition(expr, variates, pool)?;
let mut log = DerivationLog::new();
let mut terms = vec![constant];
for ((_, dist), a) in variates.iter().zip(coeffs.iter()) {
if is_zero(*a, pool) {
continue;
}
let m = dist.mean(pool)?;
log = log.merge(m.log);
terms.push(pool.mul(vec![*a, m.value]));
}
let value = simplify(pool.add(terms), pool).value;
log.push(RewriteStep::simple(
"prob_linearity_of_expectation",
expr,
value,
));
super::stash_prob_side_conditions(super::conditions_of(&log));
Ok(DerivedExpr::with_log(value, log))
}
pub fn variance_affine_independent(
expr: ExprId,
variates: &[(ExprId, Distribution)],
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let (coeffs, _) = affine_decomposition(expr, variates, pool)?;
let mut log = DerivationLog::new();
let mut terms = Vec::new();
for ((_, dist), a) in variates.iter().zip(coeffs.iter()) {
if is_zero(*a, pool) {
continue;
}
let v = dist.variance(pool)?;
log = log.merge(v.log);
terms.push(pool.mul(vec![pool.pow(*a, pool.integer(2)), v.value]));
}
let value = if terms.is_empty() {
pool.integer(0)
} else {
simplify(pool.add(terms), pool).value
};
log.push(RewriteStep::simple(
"prob_variance_of_independent_sum",
expr,
value,
));
super::stash_prob_side_conditions(super::conditions_of(&log));
Ok(DerivedExpr::with_log(value, log))
}
fn affine_decomposition(
expr: ExprId,
variates: &[(ExprId, Distribution)],
pool: &ExprPool,
) -> Result<(Vec<ExprId>, ExprId), ProbError> {
let vars: Vec<ExprId> = variates.iter().map(|(v, _)| *v).collect();
let mut coeffs = vec![pool.integer(0); vars.len()];
let mut constant = pool.integer(0);
let terms: Vec<ExprId> = match pool.get(simplify(expr, pool).value) {
ExprData::Add(args) => args.to_vec(),
other => vec![pool.intern(other)],
};
for term in terms {
let present: Vec<usize> = vars
.iter()
.enumerate()
.filter(|(_, &v)| dists::contains(term, v, pool))
.map(|(i, _)| i)
.collect();
match present.as_slice() {
[] => constant = pool.add(vec![constant, term]),
[i] => {
let v = vars[*i];
let Some(c) = linear_coefficient(term, v, pool) else {
return Err(ProbError::Unsupported(format!(
"E[·] of `{}` is not affine in `{}`; a non-linear function of a variate \
needs `expectation`, and a product of two variates needs a joint law \
this module does not model",
pool.display(term),
pool.display(v)
)));
};
coeffs[*i] = pool.add(vec![coeffs[*i], c]);
}
_ => {
return Err(ProbError::Unsupported(format!(
"`{}` involves more than one variate at once: its expectation depends on \
their joint law, which this module does not model",
pool.display(term)
)))
}
}
}
Ok((
coeffs
.into_iter()
.map(|c| simplify(c, pool).value)
.collect(),
simplify(constant, pool).value,
))
}
fn linear_coefficient(term: ExprId, v: ExprId, pool: &ExprPool) -> Option<ExprId> {
let coeffs = polynomial_coefficients(term, v, pool)?;
if coeffs.len() != 2 || !is_zero(coeffs[0], pool) {
return None;
}
Some(coeffs[1])
}