use crate::deriv::{DerivationLog, DerivedExpr, RewriteStep, SideCondition};
use crate::kernel::{Domain, ExprId, ExprPool};
use crate::simplify::simplify;
use super::dists::{self, Sign};
use super::verify;
use super::{div, sub, DistKind, Distribution, ProbError};
pub const MAX_BINOMIAL_ENTROPY_N: u32 = 64;
fn log(arg: ExprId, pool: &ExprPool) -> ExprId {
pool.func("log", vec![arg])
}
fn is_zero(e: ExprId, pool: &ExprPool) -> bool {
matches!(dists::sign_of(simplify(e, pool).value, pool), Sign::Zero)
}
fn gamma_fn(arg: ExprId, pool: &ExprPool) -> ExprId {
pool.func("gamma", vec![arg])
}
fn digamma(arg: ExprId, pool: &ExprPool) -> ExprId {
pool.func("digamma", vec![arg])
}
fn base_factor(
base: Option<ExprId>,
pool: &ExprPool,
) -> Result<(Option<ExprId>, Vec<SideCondition>), ProbError> {
let Some(b) = base else {
return Ok((None, Vec::new()));
};
let mut conds = Vec::new();
match dists::sign_of(b, pool) {
Sign::Positive => {}
Sign::Unknown => conds.push(SideCondition::Positive(b)),
Sign::Negative | Sign::Zero => {
return Err(ProbError::InvalidParameter {
parameter: "base",
requirement: "> 0",
})
}
}
let gap = simplify(sub(b, pool.integer(1), pool), pool).value;
match dists::sign_of(gap, pool) {
Sign::Zero => {
return Err(ProbError::InvalidParameter {
parameter: "base",
requirement: "≠ 1 (log 1 = 0, so there is no such unit)",
})
}
Sign::Unknown => conds.push(SideCondition::NonZero(gap)),
Sign::Positive | Sign::Negative => {}
}
Ok((Some(pool.pow(log(b, pool), pool.integer(-1))), conds))
}
fn rescale(x: ExprId, factor: Option<ExprId>, pool: &ExprPool) -> ExprId {
match factor {
Some(f) => pool.mul(vec![f, x]),
None => x,
}
}
fn shannon_two(p: ExprId, pool: &ExprPool) -> ExprId {
let neg1 = pool.integer(-1);
let q = sub(pool.integer(1), p, pool);
pool.add(vec![
pool.mul(vec![neg1, p, log(p, pool)]),
pool.mul(vec![neg1, q, log(q, pool)]),
])
}
fn two_pi_sigma_sq(sigma: ExprId, pool: &ExprPool) -> ExprId {
pool.mul(vec![
pool.integer(2),
super::pi(pool),
pool.pow(sigma, pool.integer(2)),
])
}
fn entropy_claim(dist: &Distribution, pool: &ExprPool) -> Result<ExprId, ProbError> {
let p = dist.params();
let one = pool.integer(1);
let two = pool.integer(2);
let neg1 = pool.integer(-1);
let half = pool.rational(1, 2);
Ok(match dist.kind() {
DistKind::Normal => pool.add(vec![
pool.mul(vec![half, log(two_pi_sigma_sq(p[1], pool), pool)]),
half,
]),
DistKind::LogNormal => pool.add(vec![
p[0],
pool.mul(vec![half, log(two_pi_sigma_sq(p[1], pool), pool)]),
half,
]),
DistKind::Uniform => log(sub(p[1], p[0], pool), pool),
DistKind::Exponential => sub(one, log(p[0], pool), pool),
DistKind::Gamma => pool.add(vec![
p[0],
log(p[1], pool),
log(gamma_fn(p[0], pool), pool),
pool.mul(vec![sub(one, p[0], pool), digamma(p[0], pool)]),
]),
DistKind::Beta => {
let (a, b) = (p[0], p[1]);
let ab = pool.add(vec![a, b]);
let log_beta = log(
pool.mul(vec![
gamma_fn(a, pool),
gamma_fn(b, pool),
pool.pow(gamma_fn(ab, pool), pool.integer(-1)),
]),
pool,
);
pool.add(vec![
log_beta,
pool.mul(vec![neg1, sub(a, one, pool), digamma(a, pool)]),
pool.mul(vec![neg1, sub(b, one, pool), digamma(b, pool)]),
pool.mul(vec![sub(ab, two, pool), digamma(ab, pool)]),
])
}
DistKind::Bernoulli => shannon_two(p[0], pool),
DistKind::Binomial => {
let n = dists::count_of(p[0], pool);
if n > MAX_BINOMIAL_ENTROPY_N {
return Err(ProbError::Unsupported(format!(
"H(Binomial(n, p)) has no closed form; the identity \
n·H(Bernoulli(p)) - E[log C(n, k)] leaves a residual sum of n - 1 terms, \
and n = {n} exceeds MAX_BINOMIAL_ENTROPY_N = {MAX_BINOMIAL_ENTROPY_N}"
)));
}
let prob = p[1];
let q = sub(one, prob, pool);
let mut terms = vec![pool.mul(vec![p[0], shannon_two(prob, pool)])];
for k in 1..n {
let c = rug::Integer::from(n).binomial(k);
let log_c = log(pool.integer(c.clone()), pool);
terms.push(pool.mul(vec![
neg1,
pool.integer(c),
pool.pow(prob, pool.integer(k)),
pool.pow(q, pool.integer(n - k)),
log_c,
]));
}
pool.add(terms)
}
DistKind::Poisson => {
return Err(ProbError::NoClosedForm {
quantity: "the Poisson entropy",
missing: "a closed form for e^{-λ} Σ_k λ^k log(k!)/k! — the residual sum \
after λ(1 - log λ) is not an elementary or standard special \
function, and only has asymptotic expansions",
})
}
})
}
fn degenerate_point_mass(dist: &Distribution, pool: &ExprPool) -> bool {
let p = dist.params();
let prob = match dist.kind() {
DistKind::Bernoulli => p[0],
DistKind::Binomial => {
if dists::count_of(p[0], pool) == 0 {
return true;
}
p[1]
}
_ => return false,
};
let one = pool.integer(1);
is_zero(prob, pool) || is_zero(sub(prob, one, pool), pool)
}
fn open_probability_conditions(dist: &Distribution, pool: &ExprPool) -> Vec<SideCondition> {
let p = dist.params();
let prob = match dist.kind() {
DistKind::Bernoulli => p[0],
DistKind::Binomial => p[1],
_ => return Vec::new(),
};
let one = pool.integer(1);
let mut out = Vec::new();
if matches!(
dists::sign_of(simplify(prob, pool).value, pool),
Sign::Unknown
) {
out.push(SideCondition::Positive(prob));
}
let gap = simplify(sub(one, prob, pool), pool).value;
if matches!(dists::sign_of(gap, pool), Sign::Unknown) {
out.push(SideCondition::Positive(gap));
}
out
}
pub fn entropy(
dist: &Distribution,
base: Option<ExprId>,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let raw = entropy_claim(dist, pool)?;
let (factor, base_conds) = base_factor(base, pool)?;
let discrete = dist.support(pool).is_discrete();
let rule = if discrete {
"prob_shannon_entropy"
} else {
"prob_differential_entropy"
};
if degenerate_point_mass(dist, pool) {
let zero = pool.integer(0);
let mut log_ = DerivationLog::new();
log_.push(RewriteStep::simple(
"prob_entropy_of_a_point_mass_is_zero",
zero,
zero,
));
super::stash_prob_side_conditions(super::conditions_of(&log_));
return Ok(DerivedExpr::with_log(zero, log_));
}
let claim = simplify(rescale(raw, factor, pool), pool).value;
let x = dists::fresh_var(dist.params(), pool);
let density = dists::pdf(dist, x, pool);
let f = rescale(
pool.mul(vec![pool.integer(-1), log(density, pool)]),
factor,
pool,
);
let integrand = verify::definition_integrand(f, x, dist, pool);
let evidence = verify::check(claim, integrand, x, dist, pool)?;
let mut log_ = DerivationLog::new();
log_.push(RewriteStep::simple(rule, x, claim));
let mut conds = open_probability_conditions(dist, pool);
conds.extend(base_conds);
if !conds.is_empty() {
log_.push(RewriteStep::with_conditions(
"prob_entropy_hypotheses",
claim,
claim,
conds,
));
}
log_.push(verify::evidence_step(&evidence, claim, dist, pool));
super::stash_prob_side_conditions(super::conditions_of(&log_));
Ok(DerivedExpr::with_log(claim, log_))
}
struct Gate {
gap: ExprId,
strict: bool,
reads: &'static str,
}
fn diverged(reads: &str) -> ProbError {
ProbError::Divergent(format!(
"D(P‖Q) = +∞: P charges a set Q gives probability zero ({reads} fails), so \
log(dP/dQ) is +∞ on a set of positive P-measure. The same-family closed form is \
derived under the containment hypothesis and returns a finite — often negative — \
number when it is substituted through regardless, which no divergence is"
))
}
fn atom_gates(p1: ExprId, p2: ExprId, pool: &ExprPool) -> Vec<Gate> {
let one = pool.integer(1);
let mut out = Vec::new();
if !is_zero(p1, pool) {
out.push(Gate {
gap: p2,
strict: true,
reads: "q(1) > 0",
});
}
if !is_zero(sub(p1, one, pool), pool) {
out.push(Gate {
gap: sub(one, p2, pool),
strict: true,
reads: "q(0) > 0",
});
}
out
}
fn support_gates(
p: &Distribution,
q: &Distribution,
pool: &ExprPool,
) -> Result<Vec<Gate>, ProbError> {
if p == q {
return Ok(Vec::new());
}
let (pp, qp) = (p.params(), q.params());
let raw: Vec<Gate> = match p.kind() {
DistKind::Normal
| DistKind::LogNormal
| DistKind::Exponential
| DistKind::Gamma
| DistKind::Beta
| DistKind::Poisson => Vec::new(),
DistKind::Uniform => vec![
Gate {
gap: sub(pp[0], qp[0], pool),
strict: false,
reads: "a₁ ≥ a₂",
},
Gate {
gap: sub(qp[1], pp[1], pool),
strict: false,
reads: "b₁ ≤ b₂",
},
],
DistKind::Bernoulli => atom_gates(pp[0], qp[0], pool),
DistKind::Binomial => {
let (n1, n2) = (dists::count_of(pp[0], pool), dists::count_of(qp[0], pool));
if n1 > n2 {
return Err(diverged("{0…n₁} ⊆ {0…n₂}"));
}
atom_gates(pp[1], qp[1], pool)
}
};
let mut carried = Vec::new();
for g in raw {
let gap = simplify(g.gap, pool).value;
match dists::sign_of(gap, pool) {
Sign::Positive => {}
Sign::Zero if !g.strict => {}
Sign::Zero | Sign::Negative => return Err(diverged(g.reads)),
Sign::Unknown => carried.push(Gate { gap, ..g }),
}
}
Ok(carried)
}
fn kl_claim(p: &Distribution, q: &Distribution, pool: &ExprPool) -> Result<ExprId, ProbError> {
let (a, b) = (p.params(), q.params());
let two = pool.integer(2);
let neg1 = pool.integer(-1);
let half = pool.rational(1, 2);
Ok(match p.kind() {
DistKind::Normal | DistKind::LogNormal => {
let (m1, s1, m2, s2) = (a[0], a[1], b[0], b[1]);
pool.add(vec![
log(div(s2, s1, pool), pool),
div(
pool.add(vec![pool.pow(s1, two), pool.pow(sub(m1, m2, pool), two)]),
pool.mul(vec![two, pool.pow(s2, two)]),
pool,
),
pool.mul(vec![neg1, half]),
])
}
DistKind::Uniform => log(
div(sub(b[1], b[0], pool), sub(a[1], a[0], pool), pool),
pool,
),
DistKind::Exponential => pool.add(vec![
log(div(a[0], b[0], pool), pool),
div(b[0], a[0], pool),
neg1,
]),
DistKind::Gamma => {
let (k1, t1, k2, t2) = (a[0], a[1], b[0], b[1]);
pool.add(vec![
pool.mul(vec![sub(k1, k2, pool), digamma(k1, pool)]),
pool.mul(vec![k2, log(div(t2, t1, pool), pool)]),
pool.mul(vec![k1, div(sub(t1, t2, pool), t2, pool)]),
log(div(gamma_fn(k2, pool), gamma_fn(k1, pool), pool), pool),
])
}
DistKind::Beta => {
let (a1, b1, a2, b2) = (a[0], a[1], b[0], b[1]);
let s1 = pool.add(vec![a1, b1]);
let inv = |e: ExprId| pool.pow(e, pool.integer(-1));
let s2 = pool.add(vec![a2, b2]);
pool.add(vec![
log(
pool.mul(vec![
gamma_fn(a2, pool),
gamma_fn(b2, pool),
inv(gamma_fn(s2, pool)),
inv(gamma_fn(a1, pool)),
inv(gamma_fn(b1, pool)),
gamma_fn(s1, pool),
]),
pool,
),
pool.mul(vec![
sub(a1, a2, pool),
sub(digamma(a1, pool), digamma(s1, pool), pool),
]),
pool.mul(vec![
sub(b1, b2, pool),
sub(digamma(b1, pool), digamma(s1, pool), pool),
]),
])
}
DistKind::Bernoulli => bernoulli_kl(a[0], b[0], pool),
DistKind::Binomial => {
let (n1, n2) = (dists::count_of(a[0], pool), dists::count_of(b[0], pool));
if n1 != n2 {
return Err(ProbError::Unsupported(format!(
"D(Binomial({n1}, p₁) ‖ Binomial({n2}, p₂)) with n₁ ≠ n₂ has no closed \
form: log(dP/dQ) carries log(C({n1},k)/C({n2},k)), which does not sum \
in closed form. Equal n gives n·D(Bernoulli(p₁)‖Bernoulli(p₂))"
)));
}
pool.mul(vec![a[0], bernoulli_kl(a[1], b[1], pool)])
}
DistKind::Poisson => pool.add(vec![
pool.mul(vec![a[0], log(div(a[0], b[0], pool), pool)]),
b[0],
pool.mul(vec![neg1, a[0]]),
]),
})
}
fn bernoulli_kl(p1: ExprId, p2: ExprId, pool: &ExprPool) -> ExprId {
let one = pool.integer(1);
let (q1, q2) = (sub(one, p1, pool), sub(one, p2, pool));
pool.add(vec![
pool.mul(vec![p1, log(div(p1, p2, pool), pool)]),
pool.mul(vec![q1, log(div(q1, q2, pool), pool)]),
])
}
pub fn kl_divergence(
p: &Distribution,
q: &Distribution,
base: Option<ExprId>,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let (claim, evidence, conds) = kl_parts(p, q, base, pool)?;
let mut log_ = DerivationLog::new();
log_.push(RewriteStep::simple("prob_kl_divergence", claim, claim));
if !conds.is_empty() {
log_.push(RewriteStep::with_conditions(
"prob_kl_support_containment_and_parameters",
claim,
claim,
conds,
));
}
log_.push(verify::evidence_step(&evidence, claim, p, pool));
super::stash_prob_side_conditions(super::conditions_of(&log_));
Ok(DerivedExpr::with_log(claim, log_))
}
fn kl_parts(
p: &Distribution,
q: &Distribution,
base: Option<ExprId>,
pool: &ExprPool,
) -> Result<(ExprId, verify::Evidence, Vec<SideCondition>), ProbError> {
if p.kind() != q.kind() {
return Err(ProbError::Unsupported(format!(
"D({} ‖ {}) is a cross-family divergence; this table carries the same-family \
closed forms only. ∫p log(p/q) between different families generally has no \
elementary value, and the nearest same-family formula is not it",
p.kind().name(),
q.kind().name()
)));
}
let raw = kl_claim(p, q, pool)?;
let (factor, base_conds) = base_factor(base, pool)?;
let gates = support_gates(p, q, pool)?;
let claim = simplify(rescale(raw, factor, pool), pool).value;
let x = dists::fresh_var(&[p.params(), q.params()].concat(), pool);
let (dp, dq) = (dists::pdf(p, x, pool), dists::pdf(q, x, pool));
let f = rescale(log(div(dp, dq, pool), pool), factor, pool);
let integrand = pool.mul(vec![f, dp]);
let mut extra = q.constraints(pool);
let zero = pool.integer(0);
for g in &gates {
extra.push(if g.strict {
pool.pred_gt(g.gap, zero)
} else {
pool.pred_ge(g.gap, zero)
});
}
let evidence = verify::check_pair(claim, integrand, x, p, q, &extra, pool)?;
let mut conds: Vec<SideCondition> = gates
.iter()
.map(|g| {
if g.strict {
SideCondition::Positive(g.gap)
} else {
SideCondition::InDomain(g.gap, Domain::NonNegative)
}
})
.collect();
for c in q.constraints(pool) {
if let Some(side) = verify::undischarged(c, pool) {
if !conds.contains(&side) {
conds.push(side);
}
}
}
conds.extend(base_conds);
Ok((claim, evidence, conds))
}
pub fn cross_entropy(
p: &Distribution,
q: &Distribution,
base: Option<ExprId>,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
if p.kind() != q.kind() {
return Err(ProbError::Unsupported(format!(
"H({}, {}) is a cross-family cross-entropy; this table carries the same-family \
closed forms only",
p.kind().name(),
q.kind().name()
)));
}
let h = entropy_claim(p, pool)?;
let d = kl_claim(p, q, pool)?;
let (factor, base_conds) = base_factor(base, pool)?;
let gates = support_gates(p, q, pool)?;
if degenerate_point_mass(p, pool) {
return kl_divergence(p, q, base, pool);
}
let claim = simplify(rescale(pool.add(vec![h, d]), factor, pool), pool).value;
let x = dists::fresh_var(&[p.params(), q.params()].concat(), pool);
let (dp, dq) = (dists::pdf(p, x, pool), dists::pdf(q, x, pool));
let f = rescale(
pool.mul(vec![pool.integer(-1), log(dq, pool)]),
factor,
pool,
);
let integrand = pool.mul(vec![f, dp]);
let mut extra = q.constraints(pool);
let zero = pool.integer(0);
for g in &gates {
extra.push(if g.strict {
pool.pred_gt(g.gap, zero)
} else {
pool.pred_ge(g.gap, zero)
});
}
let evidence = verify::check_pair(claim, integrand, x, p, q, &extra, pool)?;
let mut log_ = DerivationLog::new();
log_.push(RewriteStep::simple(
"prob_cross_entropy_is_entropy_plus_divergence",
claim,
claim,
));
let mut conds: Vec<SideCondition> = gates
.iter()
.map(|g| {
if g.strict {
SideCondition::Positive(g.gap)
} else {
SideCondition::InDomain(g.gap, Domain::NonNegative)
}
})
.collect();
conds.extend(open_probability_conditions(p, pool));
for c in q.constraints(pool) {
if let Some(side) = verify::undischarged(c, pool) {
if !conds.contains(&side) {
conds.push(side);
}
}
}
conds.extend(base_conds);
if !conds.is_empty() {
log_.push(RewriteStep::with_conditions(
"prob_cross_entropy_hypotheses",
claim,
claim,
conds,
));
}
log_.push(verify::evidence_step(&evidence, claim, p, pool));
super::stash_prob_side_conditions(super::conditions_of(&log_));
Ok(DerivedExpr::with_log(claim, log_))
}
pub fn mutual_information_independent(
x: &Distribution,
y: &Distribution,
base: Option<ExprId>,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let (_, base_conds) = base_factor(base, pool)?;
let zero = pool.integer(0);
let mut log_ = DerivationLog::new();
let mut conds = base_conds;
for d in [x, y] {
for c in d.constraints(pool) {
if let Some(side) = verify::undischarged(c, pool) {
if !conds.contains(&side) {
conds.push(side);
}
}
}
}
log_.push(RewriteStep::with_conditions(
"prob_mutual_information_under_assumed_independence",
zero,
zero,
conds,
));
super::stash_prob_side_conditions(super::conditions_of(&log_));
Ok(DerivedExpr::with_log(zero, log_))
}