use rug::Integer;
use crate::deriv::{DerivationLog, DerivedExpr, RewriteStep, SideCondition};
use crate::kernel::{ExprId, ExprPool};
use crate::simplify::simplify;
use super::dists::{self, Sign};
use super::moments;
use super::verify::{self, CentralTerm};
use super::{div, sub, DistKind, Distribution, ProbError, MAX_MOMENT_ORDER};
pub const MAX_CUMULANT_ORDER: u32 = 6;
fn exp(arg: ExprId, pool: &ExprPool) -> ExprId {
pool.func("exp", vec![arg])
}
fn log(arg: ExprId, pool: &ExprPool) -> ExprId {
pool.func("log", vec![arg])
}
fn mgf_strip(dist: &Distribution, t: ExprId, pool: &ExprPool) -> Option<(ExprId, &'static str)> {
let p = dist.params();
match dist.kind() {
DistKind::Exponential => Some((sub(p[0], t, pool), "t < lambda")),
DistKind::Gamma => Some((
sub(pool.integer(1), pool.mul(vec![p[1], t]), pool),
"t < 1/theta",
)),
_ => None,
}
}
fn record_strip(
log: &mut DerivationLog,
claim: ExprId,
dist: &Distribution,
t: ExprId,
what: &str,
pool: &ExprPool,
) -> Result<(), ProbError> {
let Some((gap, text)) = mgf_strip(dist, t, pool) else {
log.push(RewriteStep::simple(
"prob_mgf_converges_for_every_real_t",
claim,
claim,
));
return Ok(());
};
let gap = simplify(gap, pool).value;
match dists::sign_of(gap, pool) {
Sign::Positive => log.push(RewriteStep::simple(
"prob_mgf_argument_inside_the_convergence_strip",
claim,
claim,
)),
Sign::Negative | Sign::Zero => {
return Err(ProbError::Divergent(format!(
"the {what} of a {law} converges only for {text}, and the argument given is \
not: E[e^{{tX}}] = +∞ there. The closed form is still a finite, plausible \
number outside the strip, which is why it is refused rather than returned",
law = dist.kind().name(),
)))
}
Sign::Unknown => log.push(RewriteStep::with_conditions(
"prob_mgf_argument_inside_the_convergence_strip",
claim,
claim,
vec![SideCondition::Positive(gap)],
)),
}
Ok(())
}
fn mgf_ladder(dist: &Distribution, pool: &ExprPool) -> Vec<ExprId> {
let p = dist.params();
let half = pool.rational(1, 2);
let neg1 = pool.integer(-1);
match dist.kind() {
DistKind::Exponential => vec![
pool.mul(vec![half, p[0]]),
pool.mul(vec![neg1, p[0]]),
pool.mul(vec![pool.rational(9, 10), p[0]]),
],
DistKind::Gamma => {
let inv_theta = pool.pow(p[1], neg1);
vec![
pool.mul(vec![half, inv_theta]),
pool.mul(vec![neg1, inv_theta]),
pool.mul(vec![pool.rational(9, 10), inv_theta]),
]
}
_ => vec![half, pool.rational(-3, 4)],
}
}
fn mgf_claim(dist: &Distribution, t: ExprId, pool: &ExprPool) -> Result<ExprId, ProbError> {
let p = dist.params();
let one = pool.integer(1);
let two = pool.integer(2);
let neg1 = pool.integer(-1);
Ok(match dist.kind() {
DistKind::Normal => exp(
pool.add(vec![
pool.mul(vec![p[0], t]),
pool.mul(vec![
pool.rational(1, 2),
pool.pow(p[1], two),
pool.pow(t, two),
]),
]),
pool,
),
DistKind::Uniform => div(
sub(
exp(pool.mul(vec![p[1], t]), pool),
exp(pool.mul(vec![p[0], t]), pool),
pool,
),
pool.mul(vec![t, sub(p[1], p[0], pool)]),
pool,
),
DistKind::Exponential => div(p[0], sub(p[0], t, pool), pool),
DistKind::Gamma => pool.pow(
sub(one, pool.mul(vec![p[1], t]), pool),
pool.mul(vec![neg1, p[0]]),
),
DistKind::Bernoulli => pool.add(vec![
sub(one, p[0], pool),
pool.mul(vec![p[0], exp(t, pool)]),
]),
DistKind::Binomial => pool.pow(
pool.add(vec![
sub(one, p[1], pool),
pool.mul(vec![p[1], exp(t, pool)]),
]),
p[0],
),
DistKind::Poisson => exp(pool.mul(vec![p[0], sub(exp(t, pool), one, pool)]), pool),
DistKind::LogNormal => {
return Err(lognormal_refusal(
t,
"moment generating function",
"the log-normal moment generating function at a non-positive argument",
pool,
))
}
DistKind::Beta => {
return Err(ProbError::NoClosedForm {
quantity: "the Beta moment generating function",
missing: "the confluent hypergeometric function ₁F₁(α; α+β; t)",
})
}
})
}
fn lognormal_refusal(
t: ExprId,
what: &'static str,
quantity: &'static str,
pool: &ExprPool,
) -> ProbError {
match dists::sign_of(t, pool) {
Sign::Negative | Sign::Zero => ProbError::NoClosedForm {
quantity,
missing: "nothing this library could add — E[e^{tX}] is finite for t ≤ 0 but is \
not expressible in elementary or standard special functions",
},
_ => ProbError::Divergent(format!(
"a log-normal has no {what}: E[e^{{tX}}] = ∫e^{{tx}}p(x)dx diverges for every \
t > 0, because e^{{tx}} outgrows a density that is Gaussian in log x. Completing \
the square anyway yields a clean closed form that is the value of no integral. \
(The log-normal is also the standard example of a law not determined by its \
moments — Heyde 1963.)"
)),
}
}
pub fn moment_generating_function(
dist: &Distribution,
t: ExprId,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let claim = simplify(mgf_claim(dist, t, pool)?, pool).value;
let mut log = DerivationLog::new();
log.push(RewriteStep::simple(
"prob_moment_generating_function",
t,
claim,
));
record_strip(&mut log, claim, dist, t, "moment generating function", pool)?;
let x = dists::fresh_var(&[claim, t], pool);
let integrand = verify::definition_integrand(exp(pool.mul(vec![t, x]), pool), x, dist, pool);
let ladder = mgf_ladder(dist, pool);
let evidence = verify::check_generating(claim, integrand, x, t, &ladder, dist, 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 cgf_claim(dist: &Distribution, t: ExprId, pool: &ExprPool) -> Result<ExprId, ProbError> {
let p = dist.params();
let one = pool.integer(1);
let two = pool.integer(2);
let neg1 = pool.integer(-1);
Ok(match dist.kind() {
DistKind::Normal => pool.add(vec![
pool.mul(vec![p[0], t]),
pool.mul(vec![
pool.rational(1, 2),
pool.pow(p[1], two),
pool.pow(t, two),
]),
]),
DistKind::Uniform => log(
div(
sub(
exp(pool.mul(vec![p[1], t]), pool),
exp(pool.mul(vec![p[0], t]), pool),
pool,
),
pool.mul(vec![t, sub(p[1], p[0], pool)]),
pool,
),
pool,
),
DistKind::Exponential => log(div(p[0], sub(p[0], t, pool), pool), pool),
DistKind::Gamma => pool.mul(vec![
neg1,
p[0],
log(sub(one, pool.mul(vec![p[1], t]), pool), pool),
]),
DistKind::Bernoulli => log(
pool.add(vec![
sub(one, p[0], pool),
pool.mul(vec![p[0], exp(t, pool)]),
]),
pool,
),
DistKind::Binomial => pool.mul(vec![
p[0],
log(
pool.add(vec![
sub(one, p[1], pool),
pool.mul(vec![p[1], exp(t, pool)]),
]),
pool,
),
]),
DistKind::Poisson => pool.mul(vec![p[0], sub(exp(t, pool), one, pool)]),
DistKind::LogNormal => {
return Err(lognormal_refusal(
t,
"cumulant generating function",
"the log-normal cumulant generating function at a non-positive argument",
pool,
))
}
DistKind::Beta => {
return Err(ProbError::NoClosedForm {
quantity: "the Beta cumulant generating function",
missing: "the confluent hypergeometric function ₁F₁(α; α+β; t), of which K is \
the logarithm. The Beta *cumulants* do exist and are returned by \
`cumulant` — K is analytic at 0, it simply has no name here",
})
}
})
}
pub fn cumulant_generating_function(
dist: &Distribution,
t: ExprId,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let claim = simplify(cgf_claim(dist, t, pool)?, pool).value;
let mut log_ = DerivationLog::new();
log_.push(RewriteStep::simple(
"prob_cumulant_generating_function",
t,
claim,
));
record_strip(
&mut log_,
claim,
dist,
t,
"cumulant generating function",
pool,
)?;
let x = dists::fresh_var(&[claim, t], pool);
let integrand = verify::definition_integrand(exp(pool.mul(vec![t, x]), pool), x, dist, pool);
let ladder = mgf_ladder(dist, pool);
let exponentiated = exp(claim, pool);
let evidence = verify::check_generating(exponentiated, integrand, x, t, &ladder, dist, pool)?;
log_.push(RewriteStep::simple(
"prob_cgf_exponentiated_matches_the_moment_generating_integral",
claim,
claim,
));
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 require_integer_support(dist: &Distribution, pool: &ExprPool) -> Result<(), ProbError> {
if dist.support(pool).is_discrete() {
return Ok(());
}
Err(ProbError::Unsupported(format!(
"the probability generating function G_X(z) = E[z^X] = Σ_k z^k P(X = k) is defined \
for a law supported on the non-negative integers; {} is not one of them. The formal \
rewrite E[z^X] = E[e^{{X log z}}] does produce a closed form, but it is the moment \
generating function at log z and says nothing about P(X = k) — which is zero for \
every k here. Ask for `moment_generating_function` if that is what was wanted",
dist.kind().name()
)))
}
fn pgf_claim(dist: &Distribution, z: ExprId, pool: &ExprPool) -> ExprId {
let p = dist.params();
let one = pool.integer(1);
match dist.kind() {
DistKind::Bernoulli => pool.add(vec![sub(one, p[0], pool), pool.mul(vec![p[0], z])]),
DistKind::Binomial => pool.pow(
pool.add(vec![sub(one, p[1], pool), pool.mul(vec![p[1], z])]),
p[0],
),
DistKind::Poisson => exp(pool.mul(vec![p[0], sub(z, one, pool)]), pool),
_ => unreachable!("require_integer_support admits only the three integer laws"),
}
}
fn pgf_radius(dist: &Distribution) -> &'static str {
match dist.kind() {
DistKind::Bernoulli | DistKind::Binomial => {
"prob_pgf_is_a_polynomial_so_converges_for_all_z"
}
_ => "prob_pgf_is_entire_so_converges_for_all_z",
}
}
pub fn probability_generating_function(
dist: &Distribution,
z: ExprId,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
require_integer_support(dist, pool)?;
let claim = simplify(pgf_claim(dist, z, pool), pool).value;
let k = dists::fresh_var(&[claim, z], pool);
let integrand = verify::definition_integrand(pool.pow(z, k), k, dist, pool);
let ladder = vec![pool.rational(1, 2), pool.rational(5, 4)];
let evidence = verify::check_generating(claim, integrand, k, z, &ladder, dist, pool)?;
let mut log = DerivationLog::new();
log.push(RewriteStep::simple(
"prob_probability_generating_function",
z,
claim,
));
log.push(RewriteStep::simple(pgf_radius(dist), claim, claim));
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 factorial_moment_claim(dist: &Distribution, n: u32, pool: &ExprPool) -> ExprId {
let p = dist.params();
if n == 0 {
return pool.integer(1);
}
match dist.kind() {
DistKind::Bernoulli => {
if n == 1 {
p[0]
} else {
pool.integer(0)
}
}
DistKind::Binomial => {
let nn = dists::count_of(p[0], pool);
pool.mul(vec![
pool.integer(moments::falling_factorial(nn, n)),
pool.pow(p[1], pool.integer(n)),
])
}
DistKind::Poisson => pool.pow(p[0], pool.integer(n)),
_ => unreachable!("require_integer_support admits only the three integer laws"),
}
}
pub fn factorial_moment(
dist: &Distribution,
n: u32,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
require_integer_support(dist, pool)?;
if n > MAX_MOMENT_ORDER {
return Err(ProbError::Unsupported(format!(
"factorial moment order {n} exceeds MAX_MOMENT_ORDER = {MAX_MOMENT_ORDER}"
)));
}
let claim = simplify(factorial_moment_claim(dist, n, pool), pool).value;
let k = dists::fresh_var(&[claim], pool);
let mut factors: Vec<ExprId> = (0..n).map(|j| sub(k, pool.integer(j), pool)).collect();
if factors.is_empty() {
factors.push(pool.integer(1));
}
let integrand = verify::definition_integrand(pool.mul(factors), k, dist, pool);
let evidence = verify::check(claim, integrand, k, dist, pool)?;
let mut log = DerivationLog::new();
log.push(RewriteStep::simple("prob_factorial_moment", k, claim));
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 cumulant_claim(dist: &Distribution, n: u32, pool: &ExprPool) -> Result<ExprId, ProbError> {
let mut kappa: Vec<ExprId> = vec![pool.integer(0)];
for m in 1..=n {
let mut terms = vec![moments::raw_moment_unchecked(dist, m, pool)?];
for j in 1..m {
let c = Integer::from(m - 1).binomial(j - 1);
let mu = moments::raw_moment_unchecked(dist, m - j, pool)?;
terms.push(pool.mul(vec![pool.integer(-c), kappa[j as usize], mu]));
}
kappa.push(simplify(pool.add(terms), pool).value);
}
Ok(kappa[n as usize])
}
const CUMULANT_IN_CENTRAL_MOMENTS: [&[CentralTerm]; 5] = [
&[(1, &[(2, 1)])],
&[(1, &[(3, 1)])],
&[(1, &[(4, 1)]), (-3, &[(2, 2)])],
&[(1, &[(5, 1)]), (-10, &[(3, 1), (2, 1)])],
&[
(1, &[(6, 1)]),
(-15, &[(4, 1), (2, 1)]),
(-10, &[(3, 2)]),
(30, &[(2, 3)]),
],
];
pub fn cumulant(
dist: &Distribution,
n: u32,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
if n == 0 {
return Err(ProbError::Unsupported(
"there is no zeroth cumulant: K(0) = log M(0) = log 1 = 0 identically, for every \
law, and reporting it as κ₀ would be an answer about nothing"
.to_string(),
));
}
if n > MAX_CUMULANT_ORDER {
return Err(ProbError::Unsupported(format!(
"cumulant order {n} exceeds MAX_CUMULANT_ORDER = {MAX_CUMULANT_ORDER}; past that \
there is no second, independent expression for κ_n in this module to check the \
moment–cumulant recursion against, and an unchecked cumulant is not an answer"
)));
}
if dist.kind() == DistKind::LogNormal {
return Err(ProbError::Divergent(
"a log-normal has no cumulants: κ_n = K^{(n)}(0) with K = log E[e^{tX}], and \
E[e^{tX}] = +∞ for every t > 0, so K exists on no neighbourhood of the origin \
and there is nothing to differentiate. The moment–cumulant recursion does run — \
it computes the coefficients of a divergent series. Skewness and excess kurtosis \
are defined from central moments instead, and `skewness`/`excess_kurtosis` \
return them"
.to_string(),
));
}
if n == 1 {
return moments::mean(dist, pool);
}
let claim = simplify(cumulant_claim(dist, n, pool)?, pool).value;
let mean = simplify(moments::mean_claim(dist, pool), pool).value;
let x = dists::fresh_var(&[claim, mean], pool);
let terms = CUMULANT_IN_CENTRAL_MOMENTS[(n - 2) as usize];
let evidence = verify::check_cumulant(claim, terms, mean, x, dist, pool)?;
let mut log = DerivationLog::new();
log.push(RewriteStep::simple("prob_cumulant", mean, claim));
log.push(RewriteStep::simple(
"prob_cumulant_matches_its_central_moment_expression",
claim,
claim,
));
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 central_moment_claim(dist: &Distribution, k: u32, pool: &ExprPool) -> Result<ExprId, ProbError> {
let mu = moments::mean_claim(dist, pool);
let mut terms = Vec::with_capacity(k as usize + 1);
for j in 0..=k {
let c = Integer::from(k).binomial(j);
let signed = if (k - j) % 2 == 0 { c } else { -c };
terms.push(pool.mul(vec![
pool.integer(signed),
moments::raw_moment_unchecked(dist, j, pool)?,
pool.pow(mu, pool.integer(k - j)),
]));
}
Ok(pool.add(terms))
}
fn standardised_moment(
dist: &Distribution,
order: u32,
shift: i32,
rule: &'static str,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
super::stash_prob_side_conditions(Vec::new());
let mu2 = central_moment_claim(dist, 2, pool)?;
let mu_k = central_moment_claim(dist, order, pool)?;
let sigma_pow = pool.pow(mu2, pool.rational(i64::from(order), 2));
let claim = simplify(
pool.add(vec![div(mu_k, sigma_pow, pool), pool.integer(shift)]),
pool,
)
.value;
let mean = simplify(moments::mean_claim(dist, pool), pool).value;
let x = dists::fresh_var(&[claim, mean], pool);
let standardised = pool.pow(
div(sub(x, mean, pool), pool.pow(mu2, pool.rational(1, 2)), pool),
pool.integer(order),
);
let integrand = verify::definition_integrand(standardised, x, dist, pool);
let unshifted = simplify(sub(claim, pool.integer(shift), pool), pool).value;
let evidence = verify::check(unshifted, integrand, x, dist, pool)?;
let mut log = DerivationLog::new();
log.push(RewriteStep::simple(rule, x, claim));
log.push(verify::evidence_step(&evidence, claim, dist, pool));
super::stash_prob_side_conditions(super::conditions_of(&log));
Ok(DerivedExpr::with_log(claim, log))
}
pub fn skewness(dist: &Distribution, pool: &ExprPool) -> Result<DerivedExpr<ExprId>, ProbError> {
standardised_moment(dist, 3, 0, "prob_skewness", pool)
}
pub fn excess_kurtosis(
dist: &Distribution,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, ProbError> {
standardised_moment(dist, 4, -3, "prob_excess_kurtosis", pool)
}