use rug::Float;
use super::quad::{eval_ball, VERIFY_PREC};
use super::*;
use crate::errors::AlkahestError;
use crate::kernel::{Domain, ExprId, ExprPool};
use crate::simplify::simplify;
fn pool() -> ExprPool {
ExprPool::new()
}
fn sym(p: &ExprPool, name: &str) -> ExprId {
p.symbol(name, Domain::Real)
}
fn at(p: &ExprPool, e: ExprId, env: &[(&str, f64)]) -> f64 {
let bindings: Vec<(ExprId, Float)> = env
.iter()
.map(|(n, v)| (sym(p, n), Float::with_val(VERIFY_PREC, *v)))
.collect();
eval_ball(e, &bindings, p, VERIFY_PREC)
.unwrap_or_else(|| panic!("{} did not evaluate", p.display(e)))
.mid
.to_f64()
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() <= 1e-11 * a.abs().max(b.abs()).max(1.0)
}
macro_rules! assert_close {
($a:expr, $b:expr) => {{
let (a, b) = ($a, $b);
assert!(close(a, b), "{a} != {b}");
}};
}
#[test]
fn a_numeric_parameter_outside_its_constraint_is_refused() {
let p = pool();
let mu = sym(&p, "mu");
for bad in [p.integer(0), p.integer(-1), p.rational(-1, 2)] {
let e = Distribution::normal(mu, bad, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-001");
}
assert_eq!(
Distribution::uniform(p.integer(1), p.integer(0), &p)
.unwrap_err()
.code(),
"E-PROB-001"
);
assert_eq!(
Distribution::beta(p.integer(-1), p.integer(2), &p)
.unwrap_err()
.code(),
"E-PROB-001"
);
assert_eq!(
Distribution::bernoulli(p.rational(3, 2), &p)
.unwrap_err()
.code(),
"E-PROB-001"
);
assert_eq!(
Distribution::binomial(sym(&p, "n"), p.rational(1, 2), &p)
.unwrap_err()
.code(),
"E-PROB-001"
);
}
#[test]
fn a_symbolic_parameter_is_carried_not_decided() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let d = Distribution::normal(mu, sigma, &p).expect("symbolic sigma is not decidable");
let cs = d.constraints(&p);
assert_eq!(cs.len(), 1);
assert_eq!(format!("{}", p.display(cs[0])), "(sigma > 0)");
}
#[test]
fn normal_mean_and_variance() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let d = Distribution::normal(mu, sigma, &p).unwrap();
assert_eq!(d.mean(&p).unwrap().value, mu);
let v = d.variance(&p).unwrap().value;
assert_close!(at(&p, v, &[("sigma", 1.7), ("mu", 0.4)]), 1.7 * 1.7);
}
#[test]
fn log_normal_mean_is_exp_mu_plus_half_sigma_squared() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let d = Distribution::log_normal(mu, sigma, &p).unwrap();
let m = d.mean(&p).unwrap().value;
assert_close!(
at(&p, m, &[("mu", 0.3), ("sigma", 0.8)]),
(0.3f64 + 0.5 * 0.64).exp()
);
let v = d.variance(&p).unwrap().value;
let expected = ((0.64f64).exp() - 1.0) * (2.0 * 0.3 + 0.64f64).exp();
assert_close!(at(&p, v, &[("mu", 0.3), ("sigma", 0.8)]), expected);
}
#[test]
fn uniform_exponential_gamma_beta_means() {
let p = pool();
let (a, b) = (sym(&p, "a"), sym(&p, "b"));
let u = Distribution::uniform(a, b, &p).unwrap();
assert_close!(
at(&p, u.mean(&p).unwrap().value, &[("a", -2.0), ("b", 3.0)]),
0.5
);
assert_close!(
at(
&p,
u.variance(&p).unwrap().value,
&[("a", -2.0), ("b", 3.0)]
),
25.0 / 12.0
);
let lam = sym(&p, "lambda");
let e = Distribution::exponential(lam, &p).unwrap();
assert_close!(at(&p, e.mean(&p).unwrap().value, &[("lambda", 2.5)]), 0.4);
assert_close!(
at(&p, e.variance(&p).unwrap().value, &[("lambda", 2.5)]),
0.16
);
let (k, theta) = (sym(&p, "k"), sym(&p, "theta"));
let g = Distribution::gamma(k, theta, &p).unwrap();
assert_close!(
at(&p, g.mean(&p).unwrap().value, &[("k", 3.5), ("theta", 0.7)]),
3.5 * 0.7
);
assert_close!(
at(
&p,
g.variance(&p).unwrap().value,
&[("k", 3.5), ("theta", 0.7)]
),
3.5 * 0.49
);
let (al, be) = (sym(&p, "alpha"), sym(&p, "beta"));
let bd = Distribution::beta(al, be, &p).unwrap();
assert_close!(
at(
&p,
bd.mean(&p).unwrap().value,
&[("alpha", 2.0), ("beta", 3.0)]
),
0.4
);
assert_close!(
at(
&p,
bd.variance(&p).unwrap().value,
&[("alpha", 2.0), ("beta", 3.0)]
),
6.0 / (25.0 * 6.0)
);
}
#[test]
fn discrete_means_and_variances() {
let p = pool();
let prob = sym(&p, "p");
let b = Distribution::bernoulli(prob, &p).unwrap();
assert_eq!(b.mean(&p).unwrap().value, prob);
assert_close!(at(&p, b.variance(&p).unwrap().value, &[("p", 0.3)]), 0.21);
let bin = Distribution::binomial(p.integer(5), prob, &p).unwrap();
assert_close!(at(&p, bin.mean(&p).unwrap().value, &[("p", 0.3)]), 1.5);
assert_close!(at(&p, bin.variance(&p).unwrap().value, &[("p", 0.3)]), 1.05);
let lam = sym(&p, "lambda");
let po = Distribution::poisson(lam, &p).unwrap();
assert_close!(at(&p, po.mean(&p).unwrap().value, &[("lambda", 2.5)]), 2.5);
assert_close!(
at(&p, po.variance(&p).unwrap().value, &[("lambda", 2.5)]),
2.5
);
}
#[test]
fn standard_normal_moments_are_the_double_factorials() {
let p = pool();
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
for (n, expected) in [
(1u32, 0.0),
(2, 1.0),
(3, 0.0),
(4, 3.0),
(6, 15.0),
(8, 105.0),
] {
let m = d.moment(n, &p).unwrap().value;
assert_close!(at(&p, m, &[]), expected);
}
}
#[test]
fn non_central_normal_second_moment_is_mu_squared_plus_sigma_squared() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let d = Distribution::normal(mu, sigma, &p).unwrap();
let m2 = d.moment(2, &p).unwrap().value;
assert_close!(at(&p, m2, &[("mu", 1.3), ("sigma", 0.7)]), 1.3 * 1.3 + 0.49);
let m3 = d.moment(3, &p).unwrap().value;
assert_close!(
at(&p, m3, &[("mu", 1.3), ("sigma", 0.7)]),
1.3f64.powi(3) + 3.0 * 1.3 * 0.49
);
}
#[test]
fn poisson_third_moment_is_the_touchard_polynomial() {
let p = pool();
let lam = sym(&p, "lambda");
let d = Distribution::poisson(lam, &p).unwrap();
let m3 = d.moment(3, &p).unwrap().value;
let l = 2.5f64;
assert_close!(at(&p, m3, &[("lambda", l)]), l * l * l + 3.0 * l * l + l);
}
#[test]
fn exponential_and_gamma_moments() {
let p = pool();
let lam = sym(&p, "lambda");
let e = Distribution::exponential(lam, &p).unwrap();
assert_close!(
at(&p, e.moment(4, &p).unwrap().value, &[("lambda", 1.75)]),
24.0 / 1.75f64.powi(4)
);
let (k, theta) = (sym(&p, "k"), sym(&p, "theta"));
let g = Distribution::gamma(k, theta, &p).unwrap();
assert_close!(
at(
&p,
g.moment(3, &p).unwrap().value,
&[("k", 2.0), ("theta", 1.5)]
),
1.5f64.powi(3) * 2.0 * 3.0 * 4.0
);
}
#[test]
fn moment_order_past_the_verifiable_range_is_refused() {
let p = pool();
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let e = d.moment(MAX_MOMENT_ORDER + 1, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-002");
}
#[test]
fn normal_cdf_is_the_error_function_form() {
let p = pool();
let (mu, sigma, x) = (sym(&p, "mu"), sym(&p, "sigma"), sym(&p, "x"));
let d = Distribution::normal(mu, sigma, &p).unwrap();
let f = d.cdf(x, &p).unwrap().value;
assert_close!(
at(&p, f, &[("mu", 1.0), ("sigma", 2.0), ("x", 2.5)]),
0.773_372_647_623_131_8
);
assert_close!(at(&p, f, &[("mu", 0.0), ("sigma", 1.0), ("x", 0.0)]), 0.5);
}
#[test]
fn log_normal_and_exponential_cdfs() {
let p = pool();
let x = sym(&p, "x");
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let ln = Distribution::log_normal(mu, sigma, &p).unwrap();
let f = ln.cdf(x, &p).unwrap().value;
assert_close!(
at(&p, f, &[("mu", 0.2), ("sigma", 0.5), ("x", 1.5)]),
0.659_438_147_374_727
);
let lam = sym(&p, "lambda");
let e = Distribution::exponential(lam, &p).unwrap();
let f = e.cdf(x, &p).unwrap().value;
assert_close!(
at(&p, f, &[("lambda", 1.75), ("x", 0.8)]),
1.0 - (-1.4f64).exp()
);
}
#[test]
fn erlang_cdf_closes_and_the_general_gamma_cdf_refuses() {
let p = pool();
let x = sym(&p, "x");
let theta = sym(&p, "theta");
let erlang = Distribution::gamma(p.integer(3), theta, &p).unwrap();
let f = erlang.cdf(x, &p).unwrap().value;
assert_close!(
at(&p, f, &[("theta", 2.0), ("x", 5.0)]),
0.456_186_884_116_670_5
);
let k = sym(&p, "k");
let g = Distribution::gamma(k, theta, &p).unwrap();
let e = g.cdf(x, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-004");
assert!(format!("{e}").contains("incomplete gamma"));
}
#[test]
fn integer_beta_cdf_closes_and_the_general_one_refuses() {
let p = pool();
let x = sym(&p, "x");
let bd = Distribution::beta(p.integer(2), p.integer(3), &p).unwrap();
let f = bd.cdf(x, &p).unwrap().value;
assert_close!(at(&p, f, &[("x", 0.4)]), 0.5248);
let general = Distribution::beta(sym(&p, "alpha"), p.integer(3), &p).unwrap();
assert_eq!(general.cdf(x, &p).unwrap_err().code(), "E-PROB-004");
}
#[test]
fn uniform_cdf_is_linear() {
let p = pool();
let (a, b, x) = (sym(&p, "a"), sym(&p, "b"), sym(&p, "x"));
let u = Distribution::uniform(a, b, &p).unwrap();
let f = u.cdf(x, &p).unwrap().value;
assert_close!(at(&p, f, &[("a", -2.0), ("b", 3.0), ("x", 0.5)]), 0.5);
}
#[test]
fn a_cdf_below_or_above_the_support_is_zero_or_one_not_the_formula() {
let p = pool();
let g = Distribution::gamma(p.integer(3), p.rational(4, 5), &p).unwrap();
for below in [-1, -5] {
let f = g.cdf(p.integer(below), &p).unwrap().value;
assert_eq!(at(&p, f, &[]), 0.0, "Erlang CDF at x = {below}");
}
let e = Distribution::exponential(p.rational(7, 4), &p).unwrap();
assert_eq!(at(&p, e.cdf(p.integer(-3), &p).unwrap().value, &[]), 0.0);
let u = Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap();
assert_eq!(at(&p, u.cdf(p.integer(-4), &p).unwrap().value, &[]), 0.0);
assert_eq!(at(&p, u.cdf(p.integer(7), &p).unwrap().value, &[]), 1.0);
let b = Distribution::beta(p.integer(2), p.integer(3), &p).unwrap();
assert_eq!(
at(&p, b.cdf(p.rational(-1, 2), &p).unwrap().value, &[]),
0.0
);
assert_eq!(at(&p, b.cdf(p.rational(3, 2), &p).unwrap().value, &[]), 1.0);
let r = g.cdf(p.integer(-5), &p).unwrap();
assert!(r
.log
.0
.iter()
.any(|s| s.rule_name == "prob_cdf_outside_support"));
}
#[test]
fn a_cdf_at_a_literal_point_inside_the_support_is_verified_and_returned() {
let p = pool();
let g = Distribution::gamma(p.integer(3), p.rational(4, 5), &p).unwrap();
let f = g.cdf(p.rational(12, 5), &p).unwrap();
assert_close!(at(&p, f.value, &[]), 0.576_809_918_873_156_5);
assert!(f.log.0.iter().any(|s| s.rule_name == "prob_cdf"));
let u = Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap();
assert_close!(at(&p, u.cdf(p.rational(1, 2), &p).unwrap().value, &[]), 0.5);
}
#[test]
fn a_symbolic_cdf_argument_carries_the_in_support_condition() {
let p = pool();
let x = sym(&p, "x");
let e = Distribution::exponential(sym(&p, "lambda"), &p).unwrap();
let r = e.cdf(x, &p).unwrap();
assert!(r.log.0.iter().any(
|s| s.rule_name == "prob_cdf_argument_inside_support" && !s.side_conditions.is_empty()
));
let u = Distribution::uniform(sym(&p, "a"), sym(&p, "b"), &p).unwrap();
let r = u.cdf(x, &p).unwrap();
let conds: usize = r
.log
.0
.iter()
.filter(|s| s.rule_name == "prob_cdf_argument_inside_support")
.map(|s| s.side_conditions.len())
.sum();
assert_eq!(conds, 2);
let n = Distribution::normal(sym(&p, "mu"), sym(&p, "sigma"), &p).unwrap();
assert!(!n
.cdf(x, &p)
.unwrap()
.log
.0
.iter()
.any(|s| s.rule_name == "prob_cdf_argument_inside_support"));
}
#[test]
fn undischarged_hypotheses_are_readable_out_of_band() {
use crate::prob::take_prob_side_conditions;
let p = pool();
let x = sym(&p, "x");
let e = Distribution::exponential(p.rational(7, 4), &p).unwrap();
let _ = e.cdf(x, &p).unwrap();
assert_eq!(
take_prob_side_conditions().len(),
1,
"a symbolic cdf argument over a literal-rate exponential publishes x > 0"
);
assert!(take_prob_side_conditions().is_empty());
let e = Distribution::exponential(sym(&p, "lambda"), &p).unwrap();
let _ = e.cdf(x, &p).unwrap();
assert_eq!(
take_prob_side_conditions().len(),
2,
"lambda > 0 and x > 0 are both still open"
);
let u = Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap();
let _ = u.cdf(x, &p).unwrap();
assert_eq!(take_prob_side_conditions().len(), 2);
let g = Distribution::gamma(p.integer(3), p.rational(4, 5), &p).unwrap();
let _ = g.cdf(p.integer(-5), &p).unwrap();
assert!(take_prob_side_conditions().is_empty());
let _ = g.cdf(p.rational(12, 5), &p).unwrap();
assert!(take_prob_side_conditions().is_empty());
let n = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let _ = n.cdf(x, &p).unwrap();
assert!(take_prob_side_conditions().is_empty());
let q = sym(&p, "q");
let _ = u.quantile(q, &p).unwrap();
assert_eq!(take_prob_side_conditions().len(), 2);
let _ = u.cdf(x, &p).unwrap();
let _ = n
.quantile(q, &p)
.expect_err("the normal quantile needs erf-inverse");
assert!(
take_prob_side_conditions().is_empty(),
"a refusal must not leave a stale hypothesis behind"
);
let _ = u.cdf(x, &p).unwrap();
let _ = n.mean(&p).unwrap();
assert!(take_prob_side_conditions().is_empty());
}
#[test]
fn uniform_and_exponential_quantiles_close() {
let p = pool();
let (a, b, q) = (sym(&p, "a"), sym(&p, "b"), sym(&p, "q"));
let u = Distribution::uniform(a, b, &p).unwrap();
let f = u.quantile(q, &p).unwrap().value;
assert_close!(at(&p, f, &[("a", -2.0), ("b", 3.0), ("q", 0.25)]), -0.75);
let lam = sym(&p, "lambda");
let e = Distribution::exponential(lam, &p).unwrap();
let f = e.quantile(q, &p).unwrap().value;
assert_close!(at(&p, f, &[("lambda", 2.0), ("q", 0.75)]), 4f64.ln() / 2.0);
}
#[test]
fn the_normal_quantile_refuses_for_want_of_an_inverse_error_function() {
let p = pool();
let q = sym(&p, "q");
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let e = d.quantile(q, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-004");
assert!(format!("{e}").contains("erf"));
let g = Distribution::gamma(p.integer(2), p.integer(1), &p).unwrap();
assert_eq!(g.quantile(q, &p).unwrap_err().code(), "E-PROB-004");
let po = Distribution::poisson(p.integer(2), &p).unwrap();
assert_eq!(po.quantile(q, &p).unwrap_err().code(), "E-PROB-004");
}
#[test]
fn a_quantile_argument_outside_zero_one_is_refused() {
let p = pool();
let u = Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap();
for bad in [p.integer(2), p.rational(-1, 2), p.integer(-3)] {
let e = u.quantile(bad, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-001");
}
let e = Distribution::exponential(p.rational(7, 4), &p).unwrap();
assert_eq!(
e.quantile(p.rational(3, 2), &p).unwrap_err().code(),
"E-PROB-001"
);
assert_close!(
at(&p, e.quantile(p.rational(9, 10), &p).unwrap().value, &[]),
1.315_762_910_282_311_8
);
assert_close!(
at(&p, u.quantile(p.rational(1, 4), &p).unwrap().value, &[]),
-0.75
);
let q = sym(&p, "q");
let r = u.quantile(q, &p).unwrap();
assert!(r
.log
.0
.iter()
.any(|s| s.rule_name == "prob_quantile_argument_is_a_probability"
&& s.side_conditions.len() == 2));
}
#[test]
fn expectation_of_a_polynomial_matches_the_moment_table() {
let p = pool();
let x = sym(&p, "x");
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let f = p.add(vec![
p.mul(vec![p.integer(3), p.pow(x, p.integer(2))]),
p.mul(vec![p.integer(2), x]),
p.integer(1),
]);
let r = expectation(f, x, &d, &p).unwrap();
assert_close!(at(&p, r.value, &[]), 4.0);
}
#[test]
fn expectation_of_an_exponential_is_the_moment_generating_function() {
let p = pool();
let x = sym(&p, "x");
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let f = p.func("exp", vec![x]);
let r = expectation(f, x, &d, &p).unwrap();
assert_close!(at(&p, r.value, &[]), 0.5f64.exp());
}
#[test]
fn expectation_over_a_finite_discrete_support_is_the_sum() {
let p = pool();
let x = sym(&p, "x");
let prob = sym(&p, "p");
let d = Distribution::binomial(p.integer(4), prob, &p).unwrap();
let f = p.pow(x, p.integer(2));
let r = expectation(f, x, &d, &p).unwrap();
let pv = 0.3f64;
assert_close!(
at(&p, r.value, &[("p", pv)]),
4.0 * pv * (1.0 - pv) + (4.0 * pv).powi(2)
);
}
#[test]
fn expectation_over_poisson_needs_a_polynomial_and_says_so() {
let p = pool();
let x = sym(&p, "x");
let lam = sym(&p, "lambda");
let d = Distribution::poisson(lam, &p).unwrap();
let f = p.pow(x, p.integer(2));
let r = expectation(f, x, &d, &p).unwrap();
let l = 1.75f64;
assert_close!(at(&p, r.value, &[("lambda", l)]), l * l + l);
let g = p.func("exp", vec![p.func("sin", vec![x])]);
let e = expectation(g, x, &d, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-002");
}
#[test]
fn expectation_over_a_bounded_support_uses_the_upper_endpoint() {
let p = pool();
let x = sym(&p, "x");
let u = Distribution::uniform(p.integer(0), p.integer(2), &p).unwrap();
assert_close!(at(&p, expectation(x, x, &u, &p).unwrap().value, &[]), 1.0);
assert_close!(
at(
&p,
expectation(p.pow(x, p.integer(2)), x, &u, &p)
.unwrap()
.value,
&[]
),
4.0 / 3.0
);
let f = p.add(vec![
p.mul(vec![p.integer(3), p.pow(x, p.integer(2))]),
p.mul(vec![p.integer(2), x]),
p.integer(1),
]);
assert_close!(at(&p, expectation(f, x, &u, &p).unwrap().value, &[]), 7.0);
assert_close!(
at(
&p,
expectation(p.func("exp", vec![x]), x, &u, &p)
.unwrap()
.value,
&[]
),
(std::f64::consts::E.powi(2) - 1.0) / 2.0
);
let b = Distribution::beta(p.integer(2), p.integer(2), &p).unwrap();
assert_close!(
at(
&p,
expectation(p.pow(x, p.integer(2)), x, &b, &p)
.unwrap()
.value,
&[]
),
0.3
);
}
#[test]
fn a_call_payoff_over_a_uniform_uses_the_upper_endpoint() {
let p = pool();
let x = sym(&p, "x");
let u = Distribution::uniform(p.integer(0), p.integer(2), &p).unwrap();
for (k_num, k_den, want) in [(1_i64, 2_i64, 0.5625_f64), (1, 1, 0.25), (3, 2, 0.0625)] {
let k = p.rational(k_num, k_den);
let payoff = p.func("max", vec![sub(x, k, &p), p.integer(0)]);
let r = expectation(payoff, x, &u, &p).unwrap();
assert_close!(at(&p, r.value, &[]), want);
}
}
#[test]
fn a_divergent_expectation_is_reported_as_divergent() {
let p = pool();
let x = sym(&p, "x");
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let f = p.func("exp", vec![p.pow(x, p.integer(2))]);
let e = expectation(f, x, &d, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-006");
}
#[test]
fn expectation_of_a_call_payoff_under_a_log_normal_is_black_scholes() {
let p = pool();
let s = sym(&p, "S");
let (mu, sigma, k) = (sym(&p, "mu"), sym(&p, "sigma"), sym(&p, "K"));
let d = Distribution::log_normal(mu, sigma, &p).unwrap();
let payoff = p.func("max", vec![sub(s, k, &p), p.integer(0)]);
let r = expectation(payoff, s, &d, &p).expect("the call payoff should derive");
let value = at(&p, r.value, &[("mu", 0.05), ("sigma", 0.3), ("K", 1.1)]);
assert_close!(value, 0.130_968_082_077_972_3);
let has_condition = r
.log
.0
.iter()
.any(|s| s.rule_name == "prob_kink_inside_support");
assert!(has_condition, "K > 0 should travel with the answer");
}
#[test]
fn a_put_payoff_derives_too_and_satisfies_put_call_parity() {
let p = pool();
let s = sym(&p, "S");
let (mu, sigma, k) = (sym(&p, "mu"), sym(&p, "sigma"), sym(&p, "K"));
let d = Distribution::log_normal(mu, sigma, &p).unwrap();
let call = expectation(p.func("max", vec![sub(s, k, &p), p.integer(0)]), s, &d, &p)
.unwrap()
.value;
let put = expectation(p.func("max", vec![sub(k, s, &p), p.integer(0)]), s, &d, &p)
.unwrap()
.value;
let env = [("mu", 0.05), ("sigma", 0.3), ("K", 1.1)];
let parity = at(&p, call, &env) - at(&p, put, &env);
let forward = (0.05f64 + 0.5 * 0.09).exp() - 1.1;
assert_close!(parity, forward);
}
#[test]
fn a_normal_call_payoff_is_the_bachelier_formula() {
let p = pool();
let x = sym(&p, "x");
let (mu, sigma, k) = (sym(&p, "mu"), sym(&p, "sigma"), sym(&p, "K"));
let d = Distribution::normal(mu, sigma, &p).unwrap();
let payoff = p.func("max", vec![sub(x, k, &p), p.integer(0)]);
let r = expectation(payoff, x, &d, &p).unwrap();
let v = at(&p, r.value, &[("mu", 0.2), ("sigma", 0.9), ("K", 0.5)]);
assert_close!(v, 0.228_812_502_869_164_76);
}
#[test]
fn linearity_of_expectation_needs_no_independence() {
let p = pool();
let (x, y) = (sym(&p, "X"), sym(&p, "Y"));
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let lam = sym(&p, "lambda");
let nd = Distribution::normal(mu, sigma, &p).unwrap();
let ed = Distribution::exponential(lam, &p).unwrap();
let expr = p.add(vec![
p.mul(vec![p.integer(2), x]),
p.mul(vec![p.integer(-3), y]),
p.integer(5),
]);
let r = expectation_affine(expr, &[(x, nd), (y, ed)], &p).unwrap();
assert_close!(
at(&p, r.value, &[("mu", 1.5), ("sigma", 0.5), ("lambda", 2.0)]),
2.0 * 1.5 - 3.0 / 2.0 + 5.0
);
}
#[test]
fn variance_of_an_independent_sum_adds_the_squares() {
let p = pool();
let (x, y) = (sym(&p, "X"), sym(&p, "Y"));
let nd = Distribution::normal(p.integer(0), sym(&p, "sigma"), &p).unwrap();
let ud = Distribution::uniform(p.integer(0), p.integer(1), &p).unwrap();
let expr = p.add(vec![p.mul(vec![p.integer(2), x]), y]);
let r = variance_affine_independent(expr, &[(x, nd), (y, ud)], &p).unwrap();
assert_close!(at(&p, r.value, &[("sigma", 1.5)]), 4.0 * 2.25 + 1.0 / 12.0);
}
#[test]
fn a_product_of_two_variates_is_refused_rather_than_guessed() {
let p = pool();
let (x, y) = (sym(&p, "X"), sym(&p, "Y"));
let nd = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let nd2 = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let expr = p.mul(vec![x, y]);
let e = expectation_affine(expr, &[(x, nd), (y, nd2)], &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-002");
assert!(format!("{e}").contains("joint law"));
}
#[test]
fn a_nonlinear_function_of_one_variate_is_refused_by_the_affine_route() {
let p = pool();
let x = sym(&p, "X");
let nd = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let expr = p.func("sin", vec![x]);
let e = expectation_affine(expr, &[(x, nd)], &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-002");
}
#[test]
fn the_numeric_gate_rejects_a_plausible_wrong_closed_form() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let d = Distribution::normal(mu, sigma, &p).unwrap();
let x = super::dists::fresh_var(&[mu, sigma], &p);
let centred = p.pow(sub(x, mu, &p), p.integer(2));
let integrand = super::verify::definition_integrand(centred, x, &d, &p);
let wrong = sigma;
let e = super::verify::check(wrong, integrand, x, &d, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-005");
let right = p.pow(sigma, p.integer(2));
let ev = super::verify::check(right, integrand, x, &d, &p).unwrap();
assert!(ev.parameter_points >= 2);
assert!(ev.worst_relative_error < 1e-12);
}
#[test]
fn the_numeric_gate_rejects_a_form_that_is_right_only_at_sigma_one() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let d = Distribution::normal(mu, sigma, &p).unwrap();
let x = super::dists::fresh_var(&[mu, sigma], &p);
let integrand = super::verify::definition_integrand(p.pow(x, p.integer(2)), x, &d, &p);
let wrong = p.add(vec![p.pow(mu, p.integer(2)), p.integer(1)]);
assert_eq!(
super::verify::check(wrong, integrand, x, &d, &p)
.unwrap_err()
.code(),
"E-PROB-005"
);
}
#[test]
fn every_density_integrates_to_one() {
let p = pool();
let one = p.integer(1);
let cases: Vec<Distribution> = vec![
Distribution::normal(sym(&p, "mu"), sym(&p, "sigma"), &p).unwrap(),
Distribution::log_normal(sym(&p, "mu"), sym(&p, "sigma"), &p).unwrap(),
Distribution::uniform(sym(&p, "a"), sym(&p, "b"), &p).unwrap(),
Distribution::exponential(sym(&p, "lambda"), &p).unwrap(),
Distribution::gamma(sym(&p, "k"), sym(&p, "theta"), &p).unwrap(),
Distribution::beta(sym(&p, "alpha"), sym(&p, "beta"), &p).unwrap(),
Distribution::bernoulli(sym(&p, "p"), &p).unwrap(),
Distribution::binomial(p.integer(4), sym(&p, "p"), &p).unwrap(),
Distribution::poisson(sym(&p, "lambda"), &p).unwrap(),
];
for d in &cases {
let x = super::dists::fresh_var(d.params(), &p);
let integrand = super::verify::definition_integrand(one, x, d, &p);
let ev = super::verify::check(one, integrand, x, d, &p)
.unwrap_or_else(|e| panic!("{}: {e}", d.kind().name()));
assert!(
ev.parameter_points >= 2,
"{}: only {} points",
d.kind().name(),
ev.parameter_points
);
}
}
#[test]
fn supports_are_what_they_say() {
let p = pool();
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
assert_eq!(d.support(&p), Support::Real);
let d = Distribution::log_normal(p.integer(0), p.integer(1), &p).unwrap();
assert_eq!(d.support(&p), Support::Positive);
let d = Distribution::binomial(p.integer(7), p.rational(1, 2), &p).unwrap();
assert_eq!(d.support(&p), Support::IntegersUpTo(7));
assert!(d.support(&p).is_discrete());
let d = Distribution::poisson(p.integer(1), &p).unwrap();
assert_eq!(d.support(&p), Support::NonNegativeIntegers);
}
#[test]
fn the_pdf_is_the_textbook_density() {
let p = pool();
let x = sym(&p, "x");
let d = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let f = simplify(d.pdf(x, &p), &p).value;
assert_close!(
at(&p, f, &[("x", 0.0)]),
1.0 / (2.0 * std::f64::consts::PI).sqrt()
);
let d = Distribution::poisson(p.integer(2), &p).unwrap();
let k = sym(&p, "k");
let f = d.pdf(k, &p);
assert_close!(at(&p, f, &[("k", 3.0)]), 8.0 * (-2.0f64).exp() / 6.0);
}
fn phi_at(p: &ExprPool, e: ExprId, env: &[(&str, f64)]) -> (f64, f64) {
let bindings: Vec<(ExprId, Float)> = env
.iter()
.map(|(n, v)| (sym(p, n), Float::with_val(VERIFY_PREC, *v)))
.collect();
let v = super::cplx::eval_complex(e, &bindings, p, VERIFY_PREC)
.unwrap_or_else(|| panic!("{} did not evaluate in C", p.display(e)));
(v.re.to_f64(), v.im.to_f64())
}
#[test]
fn the_normal_characteristic_function_pins_the_convention() {
let p = pool();
let t = sym(&p, "t");
let (mu, sigma) = (sym(&p, "mu"), sym(&p, "sigma"));
let d = Distribution::normal(mu, sigma, &p).unwrap();
let r = d.characteristic_function(t, &p).unwrap();
let (re, im) = phi_at(&p, r.value, &[("mu", 0.7), ("sigma", 1.1), ("t", 1.3)]);
let m = (-(1.1f64 * 1.1) * (1.3 * 1.3) / 2.0).exp();
let ph = 0.7f64 * 1.3;
assert_close!(re, m * ph.cos());
assert_close!(im, m * ph.sin());
let (re0, im0) = phi_at(&p, r.value, &[("mu", 0.7), ("sigma", 1.1), ("t", 0.0)]);
assert_close!(re0, 1.0);
assert_close!(im0, 0.0);
assert!(r
.log
.0
.iter()
.any(|s| s.rule_name == "prob_charfun_matches_fourier_transform_at_minus_t_over_two_pi"));
}
#[test]
fn exponential_gamma_uniform_characteristic_functions() {
let p = pool();
let t = sym(&p, "t");
let lam = sym(&p, "lambda");
let e = Distribution::exponential(lam, &p).unwrap();
let phi = e.characteristic_function(t, &p).unwrap();
let (lv, tv) = (1.6f64, 0.9f64);
let den = lv * lv + tv * tv;
let (re, im) = phi_at(&p, phi.value, &[("lambda", lv), ("t", tv)]);
assert_close!(re, lv * lv / den);
assert_close!(im, lv * tv / den);
let (k, theta) = (sym(&p, "k"), sym(&p, "theta"));
let g = Distribution::gamma(k, theta, &p).unwrap();
let phi = g.characteristic_function(t, &p).unwrap();
let (re, im) = phi_at(&p, phi.value, &[("k", 2.0), ("theta", 0.5), ("t", 1.2)]);
let d2 = 0.64f64 * 0.64 + 1.2 * 1.2;
assert_close!(re, 0.64 / d2);
assert_close!(im, 1.2 / d2);
let (a, b) = (sym(&p, "a"), sym(&p, "b"));
let u = Distribution::uniform(a, b, &p).unwrap();
let phi = u.characteristic_function(t, &p).unwrap();
let (re, im) = phi_at(&p, phi.value, &[("a", 0.0), ("b", 1.0), ("t", 1.0)]);
assert_close!(re, 1f64.sin());
assert_close!(im, 1.0 - 1f64.cos());
}
#[test]
fn discrete_characteristic_functions() {
let p = pool();
let t = sym(&p, "t");
let prob = sym(&p, "p");
let b = Distribution::bernoulli(prob, &p).unwrap();
let phi = b.characteristic_function(t, &p).unwrap();
let (pv, tv) = (0.3f64, 1.1f64);
let (re, im) = phi_at(&p, phi.value, &[("p", pv), ("t", tv)]);
assert_close!(re, 1.0 - pv + pv * tv.cos());
assert_close!(im, pv * tv.sin());
let bin = Distribution::binomial(p.integer(4), prob, &p).unwrap();
let phi = bin.characteristic_function(t, &p).unwrap();
let (re, im) = phi_at(&p, phi.value, &[("p", pv), ("t", tv)]);
let (br, bi) = (1.0 - pv + pv * tv.cos(), pv * tv.sin());
let (mut ar, mut ai) = (1.0f64, 0.0f64);
for _ in 0..4 {
let nr = ar * br - ai * bi;
ai = ar * bi + ai * br;
ar = nr;
}
assert_close!(re, ar);
assert_close!(im, ai);
let lam = sym(&p, "lambda");
let po = Distribution::poisson(lam, &p).unwrap();
let phi = po.characteristic_function(t, &p).unwrap();
let lv = 2.2f64;
let (re, im) = phi_at(&p, phi.value, &[("lambda", lv), ("t", tv)]);
let m = (lv * (tv.cos() - 1.0)).exp();
let ph = lv * tv.sin();
assert_close!(re, m * ph.cos());
assert_close!(im, m * ph.sin());
}
#[test]
fn the_log_normal_and_beta_characteristic_functions_refuse() {
let p = pool();
let t = sym(&p, "t");
let d = Distribution::log_normal(p.integer(0), p.integer(1), &p).unwrap();
let e = d.characteristic_function(t, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-004");
assert!(format!("{e}").contains("no closed form"));
let b = Distribution::beta(p.integer(2), p.integer(3), &p).unwrap();
let e = b.characteristic_function(t, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-004");
assert!(format!("{e}").contains("hypergeometric"));
}
#[test]
fn moments_from_the_characteristic_function_agree_with_the_moment_table() {
let p = pool();
let t = sym(&p, "t");
let cases: Vec<(Distribution, Vec<(&str, f64)>)> = vec![
(
Distribution::normal(sym(&p, "mu"), sym(&p, "sigma"), &p).unwrap(),
vec![("mu", 0.6), ("sigma", 1.3)],
),
(
Distribution::exponential(sym(&p, "lambda"), &p).unwrap(),
vec![("lambda", 1.7)],
),
(
Distribution::gamma(p.integer(3), sym(&p, "theta"), &p).unwrap(),
vec![("theta", 0.8)],
),
(
Distribution::bernoulli(sym(&p, "p"), &p).unwrap(),
vec![("p", 0.42)],
),
(
Distribution::poisson(sym(&p, "lambda"), &p).unwrap(),
vec![("lambda", 2.4)],
),
(
Distribution::binomial(p.integer(5), sym(&p, "p"), &p).unwrap(),
vec![("p", 0.35)],
),
];
for (d, env) in &cases {
let phi = d.characteristic_function(t, &p).unwrap().value;
let mut deriv = phi;
for n in 1..=3u32 {
deriv = simplify(crate::diff::diff(deriv, t, &p).unwrap().value, &p).value;
let mut at_zero: Vec<(&str, f64)> = env.clone();
at_zero.push(("t", 0.0));
let (re, im) = phi_at(&p, deriv, &at_zero);
let from_phi = match n % 4 {
0 => re,
1 => im,
2 => -re,
_ => -im,
};
let from_table = at(&p, d.moment(n, &p).unwrap().value, env);
assert!(
close(from_phi, from_table),
"{} moment {n}: phi route {from_phi} vs table {from_table}",
d.kind().name()
);
}
}
}
fn at_complex(p: &ExprPool, e: ExprId, env: &[(&str, f64)]) -> (f64, f64) {
let bindings: Vec<(ExprId, Float)> = env
.iter()
.map(|(n, v)| (sym(p, n), Float::with_val(VERIFY_PREC, *v)))
.collect();
let v = super::cplx::eval_complex(e, &bindings, p, VERIFY_PREC)
.unwrap_or_else(|| panic!("{} did not evaluate in C", p.display(e)));
(v.re.to_f64(), v.im.to_f64())
}
fn subs1(p: &ExprPool, expr: ExprId, var: ExprId, by: ExprId) -> ExprId {
let mut m = std::collections::HashMap::new();
m.insert(var, by);
crate::kernel::subs(expr, &m, p)
}
fn nth_derivative(p: &ExprPool, expr: ExprId, var: ExprId, n: u32) -> ExprId {
let mut e = expr;
for _ in 0..n {
e = simplify(
crate::diff::diff(e, var, p).expect("differentiable").value,
p,
)
.value;
}
e
}
#[test]
fn the_mgf_at_an_imaginary_argument_is_the_characteristic_function() {
let p = pool();
let t = sym(&p, "t");
let s = sym(&p, "s");
let i_t = p.mul(vec![p.imaginary_unit(), t]);
let dists = [
Distribution::normal(p.rational(7, 10), p.rational(11, 10), &p).unwrap(),
Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap(),
Distribution::exponential(p.rational(7, 4), &p).unwrap(),
Distribution::gamma(p.integer(3), p.rational(4, 5), &p).unwrap(),
Distribution::bernoulli(p.rational(3, 10), &p).unwrap(),
Distribution::binomial(p.integer(5), p.rational(7, 20), &p).unwrap(),
Distribution::poisson(p.rational(12, 5), &p).unwrap(),
];
for d in &dists {
let m = moment_generating_function(d, s, &p).unwrap().value;
let phi = d.characteristic_function(t, &p).unwrap().value;
for tv in [0.4, -0.9, 1.3] {
let (mr, mi) = at_complex(&p, subs1(&p, m, s, i_t), &[("t", tv)]);
let (pr, pi_) = at_complex(&p, phi, &[("t", tv)]);
assert_close!(mr, pr);
assert_close!(mi, pi_);
}
}
}
#[test]
fn derivatives_of_the_mgf_at_zero_are_the_raw_moments() {
let p = pool();
let t = sym(&p, "t");
let cases = [
Distribution::normal(p.rational(1, 2), p.rational(3, 2), &p).unwrap(),
Distribution::exponential(p.rational(7, 4), &p).unwrap(),
Distribution::gamma(p.integer(3), p.rational(4, 5), &p).unwrap(),
Distribution::poisson(p.rational(12, 5), &p).unwrap(),
Distribution::binomial(p.integer(5), p.rational(7, 20), &p).unwrap(),
];
for d in &cases {
let m = moment_generating_function(d, t, &p).unwrap().value;
for n in 1..=4u32 {
let dn = subs1(&p, nth_derivative(&p, m, t, n), t, p.integer(0));
let table = d.moment(n, &p).unwrap().value;
assert_close!(at(&p, dn, &[]), at(&p, table, &[]));
}
}
}
#[test]
fn derivatives_of_the_pgf_at_one_are_the_factorial_moments() {
let p = pool();
let z = sym(&p, "z");
let cases = [
Distribution::bernoulli(p.rational(3, 10), &p).unwrap(),
Distribution::binomial(p.integer(5), p.rational(7, 20), &p).unwrap(),
Distribution::poisson(p.rational(12, 5), &p).unwrap(),
];
for d in &cases {
let g = probability_generating_function(d, z, &p).unwrap().value;
for n in 1..=4u32 {
let dn = subs1(&p, nth_derivative(&p, g, z, n), z, p.integer(1));
let fm = factorial_moment(d, n, &p).unwrap().value;
assert_close!(at(&p, dn, &[]), at(&p, fm, &[]));
}
}
}
#[test]
fn the_pgf_at_e_to_the_t_is_the_mgf() {
let p = pool();
let t = sym(&p, "t");
let z = sym(&p, "z");
let e_t = p.func("exp", vec![t]);
let cases = [
Distribution::bernoulli(p.rational(3, 10), &p).unwrap(),
Distribution::binomial(p.integer(5), p.rational(7, 20), &p).unwrap(),
Distribution::poisson(p.rational(12, 5), &p).unwrap(),
];
for d in &cases {
let g = probability_generating_function(d, z, &p).unwrap().value;
let m = moment_generating_function(d, t, &p).unwrap().value;
for tv in [0.4, -0.9, 1.3] {
assert_close!(
at(&p, subs1(&p, g, z, e_t), &[("t", tv)]),
at(&p, m, &[("t", tv)])
);
}
}
}
#[test]
fn the_first_two_cumulants_are_the_mean_and_the_variance() {
let p = pool();
let cases = [
Distribution::normal(p.rational(1, 2), p.rational(3, 2), &p).unwrap(),
Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap(),
Distribution::exponential(p.rational(7, 4), &p).unwrap(),
Distribution::gamma(p.integer(3), p.rational(4, 5), &p).unwrap(),
Distribution::beta(p.integer(2), p.integer(3), &p).unwrap(),
Distribution::bernoulli(p.rational(3, 10), &p).unwrap(),
Distribution::binomial(p.integer(5), p.rational(7, 20), &p).unwrap(),
Distribution::poisson(p.rational(12, 5), &p).unwrap(),
];
for d in &cases {
assert_close!(
at(&p, cumulant(d, 1, &p).unwrap().value, &[]),
at(&p, d.mean(&p).unwrap().value, &[])
);
assert_close!(
at(&p, cumulant(d, 2, &p).unwrap().value, &[]),
at(&p, d.variance(&p).unwrap().value, &[])
);
}
}
#[test]
fn every_normal_cumulant_past_the_second_is_zero() {
let p = pool();
let mu = sym(&p, "mu");
let sigma = sym(&p, "sigma");
let d = Distribution::normal(mu, sigma, &p).unwrap();
for n in 3..=MAX_CUMULANT_ORDER {
let k = cumulant(&d, n, &p).unwrap().value;
for env in [
[("mu", 0.0), ("sigma", 1.0)],
[("mu", -1.5), ("sigma", 2.0)],
[("mu", 0.25), ("sigma", 3.0)],
] {
assert!(
at(&p, k, &env).abs() < 1e-9,
"kappa_{n} of a normal is {} at {env:?}, not 0",
at(&p, k, &env)
);
}
}
}
#[test]
fn every_poisson_cumulant_is_lambda() {
let p = pool();
let lam = sym(&p, "lambda");
let d = Distribution::poisson(lam, &p).unwrap();
for n in 1..=MAX_CUMULANT_ORDER {
let k = cumulant(&d, n, &p).unwrap().value;
for lv in [1.0, 0.4, 2.5] {
assert_close!(at(&p, k, &[("lambda", lv)]), lv);
}
}
}
#[test]
fn skewness_and_excess_kurtosis_agree_with_the_cumulants() {
let p = pool();
let cases = [
Distribution::normal(p.rational(1, 2), p.rational(3, 2), &p).unwrap(),
Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap(),
Distribution::exponential(p.rational(7, 4), &p).unwrap(),
Distribution::gamma(p.integer(3), p.rational(4, 5), &p).unwrap(),
Distribution::beta(p.integer(2), p.integer(3), &p).unwrap(),
Distribution::bernoulli(p.rational(3, 10), &p).unwrap(),
Distribution::poisson(p.rational(12, 5), &p).unwrap(),
];
for d in &cases {
let var = at(&p, d.variance(&p).unwrap().value, &[]);
let k3 = at(&p, cumulant(d, 3, &p).unwrap().value, &[]);
let k4 = at(&p, cumulant(d, 4, &p).unwrap().value, &[]);
assert_close!(
at(&p, skewness(d, &p).unwrap().value, &[]),
k3 / var.powf(1.5)
);
assert_close!(
at(&p, excess_kurtosis(d, &p).unwrap().value, &[]),
k4 / (var * var)
);
}
}
#[test]
fn known_shape_statistics_match_their_textbook_values() {
let p = pool();
let n = Distribution::normal(p.rational(1, 2), p.rational(3, 2), &p).unwrap();
assert_close!(at(&p, skewness(&n, &p).unwrap().value, &[]), 0.0);
assert_close!(at(&p, excess_kurtosis(&n, &p).unwrap().value, &[]), 0.0);
let u = Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap();
assert_close!(at(&p, skewness(&u, &p).unwrap().value, &[]), 0.0);
assert_close!(at(&p, excess_kurtosis(&u, &p).unwrap().value, &[]), -1.2);
let lam = sym(&p, "lambda");
let e = Distribution::exponential(lam, &p).unwrap();
let (sk, ek) = (
skewness(&e, &p).unwrap().value,
excess_kurtosis(&e, &p).unwrap().value,
);
for lv in [1.0, 0.4, 2.5] {
assert_close!(at(&p, sk, &[("lambda", lv)]), 2.0);
assert_close!(at(&p, ek, &[("lambda", lv)]), 6.0);
}
let po = Distribution::poisson(lam, &p).unwrap();
let (sk, ek) = (
skewness(&po, &p).unwrap().value,
excess_kurtosis(&po, &p).unwrap().value,
);
for lv in [1.0, 0.4, 2.5] {
assert_close!(at(&p, sk, &[("lambda", lv)]), lv.powf(-0.5));
assert_close!(at(&p, ek, &[("lambda", lv)]), 1.0 / lv);
}
}
#[test]
fn the_lognormal_mgf_is_refused_rather_than_completed() {
let p = pool();
let t = sym(&p, "t");
let d = Distribution::log_normal(p.integer(0), p.integer(1), &p).unwrap();
assert_eq!(
moment_generating_function(&d, t, &p).unwrap_err().code(),
"E-PROB-006"
);
assert_eq!(
moment_generating_function(&d, p.integer(1), &p)
.unwrap_err()
.code(),
"E-PROB-006"
);
assert_eq!(
cumulant_generating_function(&d, t, &p).unwrap_err().code(),
"E-PROB-006"
);
assert_eq!(cumulant(&d, 3, &p).unwrap_err().code(), "E-PROB-006");
assert_eq!(
moment_generating_function(&d, p.integer(-1), &p)
.unwrap_err()
.code(),
"E-PROB-004"
);
assert_close!(
at(&p, skewness(&d, &p).unwrap().value, &[]),
(1.0f64.exp() + 2.0) * (1.0f64.exp() - 1.0).sqrt()
);
}
#[test]
fn an_exponential_mgf_outside_its_strip_is_refused() {
let p = pool();
let d = Distribution::exponential(p.rational(7, 4), &p).unwrap();
for bad in [p.rational(7, 2), p.rational(7, 4), p.integer(2)] {
let e = moment_generating_function(&d, bad, &p).unwrap_err();
assert_eq!(e.code(), "E-PROB-006");
assert_eq!(
cumulant_generating_function(&d, bad, &p)
.unwrap_err()
.code(),
"E-PROB-006"
);
}
let good = moment_generating_function(&d, p.rational(1, 2), &p).unwrap();
assert_close!(at(&p, good.value, &[]), 1.4);
assert!(take_prob_side_conditions().is_empty());
}
#[test]
fn a_symbolic_mgf_argument_carries_its_convergence_strip() {
let p = pool();
let t = sym(&p, "t");
let lam = sym(&p, "lambda");
let d = Distribution::exponential(lam, &p).unwrap();
moment_generating_function(&d, t, &p).unwrap();
let conds = take_prob_side_conditions();
assert!(
conds
.iter()
.any(|c| matches!(c, crate::deriv::SideCondition::Positive(_))),
"expected the strip lambda - t > 0, got {conds:?}"
);
let theta = sym(&p, "theta");
let g = Distribution::gamma(p.integer(3), theta, &p).unwrap();
moment_generating_function(&g, t, &p).unwrap();
assert!(!take_prob_side_conditions().is_empty());
let n = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
moment_generating_function(&n, t, &p).unwrap();
assert!(take_prob_side_conditions().is_empty());
}
#[test]
fn the_pgf_of_a_continuous_law_is_a_category_error() {
let p = pool();
let z = sym(&p, "z");
for d in [
Distribution::normal(p.integer(0), p.integer(1), &p).unwrap(),
Distribution::log_normal(p.integer(0), p.integer(1), &p).unwrap(),
Distribution::uniform(p.integer(0), p.integer(1), &p).unwrap(),
Distribution::exponential(p.integer(1), &p).unwrap(),
Distribution::gamma(p.integer(3), p.integer(1), &p).unwrap(),
Distribution::beta(p.integer(2), p.integer(3), &p).unwrap(),
] {
assert_eq!(
probability_generating_function(&d, z, &p)
.unwrap_err()
.code(),
"E-PROB-002"
);
assert_eq!(
factorial_moment(&d, 2, &p).unwrap_err().code(),
"E-PROB-002"
);
}
let po = Distribution::poisson(p.rational(12, 5), &p).unwrap();
let g = probability_generating_function(&po, z, &p).unwrap();
assert_close!(at(&p, g.value, &[("z", 0.5)]), (-1.2f64).exp());
}
#[test]
fn the_beta_cgf_refuses_but_its_cumulants_do_not() {
let p = pool();
let t = sym(&p, "t");
let d = Distribution::beta(p.integer(2), p.integer(3), &p).unwrap();
assert_eq!(
moment_generating_function(&d, t, &p).unwrap_err().code(),
"E-PROB-004"
);
assert_eq!(
cumulant_generating_function(&d, t, &p).unwrap_err().code(),
"E-PROB-004"
);
assert_close!(at(&p, cumulant(&d, 2, &p).unwrap().value, &[]), 0.04);
assert_close!(at(&p, skewness(&d, &p).unwrap().value, &[]), 2.0 / 7.0);
}
#[test]
fn cumulant_orders_outside_the_checked_range_are_refused() {
let p = pool();
let d = Distribution::poisson(p.rational(12, 5), &p).unwrap();
assert_eq!(cumulant(&d, 0, &p).unwrap_err().code(), "E-PROB-002");
assert_eq!(
cumulant(&d, MAX_CUMULANT_ORDER + 1, &p).unwrap_err().code(),
"E-PROB-002"
);
}
#[test]
fn differential_entropy_matches_an_independent_implementation() {
let p = pool();
let cases: Vec<(Distribution, f64)> = vec![
(
Distribution::normal(p.integer(0), p.integer(1), &p).unwrap(),
1.4189385332046727,
),
(
Distribution::normal(p.integer(2), p.rational(3, 2), &p).unwrap(),
1.8244036413128368,
),
(
Distribution::log_normal(p.rational(1, 5), p.rational(1, 2), &p).unwrap(),
0.9257913526447272,
),
(
Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap(),
1.60943791243413,
),
(
Distribution::exponential(p.rational(7, 4), &p).unwrap(),
0.44038421206458056,
),
(
Distribution::gamma(p.rational(7, 2), p.rational(4, 5), &p).unwrap(),
1.7199384494197565,
),
(
Distribution::beta(p.integer(2), p.integer(3), &p).unwrap(),
-0.23490664978800016,
),
];
for (d, want) in &cases {
let h = d.entropy(None, &p).unwrap().value;
assert_close!(at(&p, h, &[]), *want);
}
}
#[test]
fn shannon_entropy_matches_the_defining_sum() {
let p = pool();
let bern = Distribution::bernoulli(p.rational(3, 10), &p).unwrap();
assert_close!(
at(&p, bern.entropy(None, &p).unwrap().value, &[]),
0.610864302054894
);
let binom = Distribution::binomial(p.integer(5), p.rational(7, 20), &p).unwrap();
assert_close!(
at(&p, binom.entropy(None, &p).unwrap().value, &[]),
1.464242936917896
);
}
#[test]
fn gaussian_entropy_is_half_log_two_pi_e_sigma_squared_and_free_of_mu() {
let p = pool();
let sigma = p.rational(3, 2);
let mut seen = None;
for mu in [
p.integer(0),
p.integer(3),
p.rational(-7, 2),
sym(&p, "mu_free"),
] {
let h = Distribution::normal(mu, sigma, &p)
.unwrap()
.entropy(None, &p)
.unwrap()
.value;
match seen {
None => seen = Some(h),
Some(first) => assert_eq!(first, h, "entropy depends on mu"),
}
}
assert_close!(at(&p, seen.unwrap(), &[]), 1.8244036413128368);
}
#[test]
fn differential_entropy_shifts_by_the_log_jacobian_under_exp() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu_j"), sym(&p, "sigma_j"));
let normal = Distribution::normal(mu, sigma, &p).unwrap();
let log_normal = Distribution::log_normal(mu, sigma, &p).unwrap();
let gap = simplify(
sub(
log_normal.entropy(None, &p).unwrap().value,
normal.entropy(None, &p).unwrap().value,
&p,
),
&p,
)
.value;
assert_eq!(gap, mu, "h(e^Y) - h(Y) should be exactly mu");
}
#[test]
fn a_divergence_from_a_law_to_itself_is_exactly_zero() {
let p = pool();
let zero = p.integer(0);
let families = [
Distribution::normal(sym(&p, "m0"), sym(&p, "s0"), &p).unwrap(),
Distribution::log_normal(sym(&p, "m0"), sym(&p, "s0"), &p).unwrap(),
Distribution::uniform(sym(&p, "ua"), sym(&p, "ub"), &p).unwrap(),
Distribution::exponential(sym(&p, "lam0"), &p).unwrap(),
Distribution::gamma(sym(&p, "k0"), sym(&p, "th0"), &p).unwrap(),
Distribution::beta(sym(&p, "al0"), sym(&p, "be0"), &p).unwrap(),
Distribution::bernoulli(sym(&p, "pp0"), &p).unwrap(),
Distribution::binomial(p.integer(5), sym(&p, "pb0"), &p).unwrap(),
Distribution::poisson(sym(&p, "lm0"), &p).unwrap(),
];
for d in &families {
let v = kl_divergence(d, d, None, &p).unwrap().value;
assert_eq!(
simplify(v, &p).value,
zero,
"D(P‖P) for {} is {}",
d.kind().name(),
p.display(v)
);
}
}
#[test]
fn cross_entropy_is_entropy_plus_divergence() {
let p = pool();
let zero = p.integer(0);
let pairs: Vec<(Distribution, Distribution)> = vec![
(
Distribution::normal(sym(&p, "m1"), sym(&p, "s1"), &p).unwrap(),
Distribution::normal(sym(&p, "m2"), sym(&p, "s2"), &p).unwrap(),
),
(
Distribution::exponential(sym(&p, "l1"), &p).unwrap(),
Distribution::exponential(sym(&p, "l2"), &p).unwrap(),
),
(
Distribution::gamma(sym(&p, "k1"), sym(&p, "t1"), &p).unwrap(),
Distribution::gamma(sym(&p, "k2"), sym(&p, "t2"), &p).unwrap(),
),
(
Distribution::beta(sym(&p, "a1"), sym(&p, "b1"), &p).unwrap(),
Distribution::beta(sym(&p, "a2"), sym(&p, "b2"), &p).unwrap(),
),
(
Distribution::bernoulli(sym(&p, "q1"), &p).unwrap(),
Distribution::bernoulli(sym(&p, "q2"), &p).unwrap(),
),
(
Distribution::binomial(p.integer(4), sym(&p, "r1"), &p).unwrap(),
Distribution::binomial(p.integer(4), sym(&p, "r2"), &p).unwrap(),
),
];
for (a, b) in &pairs {
let ce = cross_entropy(a, b, None, &p).unwrap().value;
let h = a.entropy(None, &p).unwrap().value;
let d = kl_divergence(a, b, None, &p).unwrap().value;
let gap = simplify(sub(ce, p.add(vec![h, d]), &p), &p).value;
assert_eq!(gap, zero, "{} cross-entropy identity", a.kind().name());
}
}
#[test]
fn gibbs_every_divergence_is_non_negative() {
let p = pool();
let grid = [(1, 2), (1, 1), (3, 2), (5, 2)];
let mut gammas = Vec::new();
let mut normals = Vec::new();
let mut bernoullis = Vec::new();
for (a, b) in grid {
gammas.push(Distribution::gamma(p.rational(a, 2), p.rational(b, 2), &p).unwrap());
normals.push(Distribution::normal(p.rational(a - 3, 2), p.rational(b, 2), &p).unwrap());
}
for k in 1..10 {
bernoullis.push(Distribution::bernoulli(p.rational(k, 10), &p).unwrap());
}
for family in [&gammas, &normals, &bernoullis] {
for a in family.iter() {
for b in family.iter() {
let v = at(&p, kl_divergence(a, b, None, &p).unwrap().value, &[]);
assert!(
v >= -1e-12,
"D({} ‖ {}) = {v} is negative",
a.kind().name(),
b.kind().name()
);
if a == b {
assert!(v.abs() < 1e-12, "D(P‖P) = {v}");
}
}
}
}
}
#[test]
fn divergences_match_independent_values() {
let p = pool();
let cases: Vec<(Distribution, Distribution, f64)> = vec![
(
Distribution::normal(p.integer(0), p.integer(1), &p).unwrap(),
Distribution::normal(p.integer(2), p.rational(3, 2), &p).unwrap(),
1.016576219219275,
),
(
Distribution::exponential(p.integer(1), &p).unwrap(),
Distribution::exponential(p.integer(4), &p).unwrap(),
1.613705638880109,
),
(
Distribution::exponential(p.integer(4), &p).unwrap(),
Distribution::exponential(p.integer(1), &p).unwrap(),
0.636294361119891,
),
(
Distribution::uniform(p.integer(0), p.integer(1), &p).unwrap(),
Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap(),
1.6094379124341,
),
(
Distribution::poisson(p.rational(12, 5), &p).unwrap(),
Distribution::poisson(p.integer(1), &p).unwrap(),
0.701124969649360,
),
(
Distribution::bernoulli(p.rational(1, 2), &p).unwrap(),
Distribution::bernoulli(p.rational(7, 10), &p).unwrap(),
0.087176693572389,
),
(
Distribution::bernoulli(p.rational(3, 10), &p).unwrap(),
Distribution::bernoulli(p.rational(1, 2), &p).unwrap(),
0.082282878505052,
),
];
for (a, b, want) in &cases {
let v = kl_divergence(a, b, None, &p).unwrap().value;
assert_close!(at(&p, v, &[]), *want);
}
}
#[test]
fn a_divergence_off_a_nested_support_is_infinite_not_the_formula() {
let p = pool();
let wide = Distribution::uniform(p.integer(0), p.integer(1), &p).unwrap();
let narrow = Distribution::uniform(p.integer(0), p.rational(1, 2), &p).unwrap();
assert_eq!(
kl_divergence(&wide, &narrow, None, &p).unwrap_err().code(),
"E-PROB-006"
);
let shifted = Distribution::uniform(p.rational(1, 4), p.rational(3, 4), &p).unwrap();
assert_eq!(
kl_divergence(&wide, &shifted, None, &p).unwrap_err().code(),
"E-PROB-006"
);
let fair = Distribution::bernoulli(p.rational(1, 2), &p).unwrap();
for degenerate in [p.integer(0), p.integer(1)] {
let q = Distribution::bernoulli(degenerate, &p).unwrap();
assert_eq!(
kl_divergence(&fair, &q, None, &p).unwrap_err().code(),
"E-PROB-006"
);
}
let containing = Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap();
assert_close!(
at(
&p,
kl_divergence(&wide, &containing, None, &p).unwrap().value,
&[]
),
1.6094379124341
);
}
#[test]
fn a_symbolic_containment_travels_as_a_side_condition() {
let p = pool();
let (a1, b1) = (sym(&p, "ca"), sym(&p, "cb"));
let inner = Distribution::uniform(a1, b1, &p).unwrap();
let outer = Distribution::uniform(p.integer(-2), p.integer(3), &p).unwrap();
match kl_divergence(&inner, &outer, None, &p) {
Ok(_) => {
let conds = take_prob_side_conditions();
assert!(
conds.len() >= 2,
"a symbolic [a, b] against [-2, 3] must publish both containments, got {conds:?}"
);
}
Err(e) => assert_eq!(e.code(), "E-PROB-005"),
}
}
#[test]
fn entropy_in_bits_is_entropy_in_nats_over_log_two() {
let p = pool();
let fair = Distribution::bernoulli(p.rational(1, 2), &p).unwrap();
assert_close!(
at(&p, fair.entropy(Some(p.integer(2)), &p).unwrap().value, &[]),
1.0
);
assert_close!(
at(&p, fair.entropy(None, &p).unwrap().value, &[]),
std::f64::consts::LN_2
);
for bad in [p.integer(1), p.integer(0), p.integer(-2)] {
assert_eq!(
fair.entropy(Some(bad), &p).unwrap_err().code(),
"E-PROB-001"
);
}
}
#[test]
fn information_theory_refusals_name_what_is_missing() {
let p = pool();
let pois = Distribution::poisson(p.rational(12, 5), &p).unwrap();
assert_eq!(pois.entropy(None, &p).unwrap_err().code(), "E-PROB-004");
let pois2 = Distribution::poisson(p.integer(1), &p).unwrap();
assert_close!(
at(
&p,
kl_divergence(&pois, &pois2, None, &p).unwrap().value,
&[]
),
0.701124969649360
);
let normal = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let expo = Distribution::exponential(p.integer(1), &p).unwrap();
assert_eq!(
kl_divergence(&normal, &expo, None, &p).unwrap_err().code(),
"E-PROB-002"
);
let big = Distribution::binomial(p.integer(80), p.rational(1, 3), &p).unwrap();
assert_eq!(big.entropy(None, &p).unwrap_err().code(), "E-PROB-002");
}
#[test]
fn the_entropy_of_a_point_mass_is_zero_not_nan() {
let p = pool();
let zero = p.integer(0);
for degenerate in [p.integer(0), p.integer(1)] {
let d = Distribution::bernoulli(degenerate, &p).unwrap();
assert_eq!(d.entropy(None, &p).unwrap().value, zero);
let b = Distribution::binomial(p.integer(4), degenerate, &p).unwrap();
assert_eq!(b.entropy(None, &p).unwrap().value, zero);
}
let none = Distribution::binomial(p.integer(0), p.rational(1, 3), &p).unwrap();
assert_eq!(none.entropy(None, &p).unwrap().value, zero);
}
#[test]
fn mutual_information_of_an_independent_pair_is_zero() {
let p = pool();
let x = Distribution::normal(p.integer(0), p.integer(1), &p).unwrap();
let y = Distribution::poisson(p.integer(3), &p).unwrap();
assert_eq!(
mutual_information_independent(&x, &y, None, &p)
.unwrap()
.value,
p.integer(0)
);
}
#[test]
fn the_gate_rejects_a_wrong_entropy_and_a_wrong_divergence() {
let p = pool();
let (mu, sigma) = (sym(&p, "mu_g"), sym(&p, "sigma_g"));
let normal = Distribution::normal(mu, sigma, &p).unwrap();
let x = dists::fresh_var(normal.params(), &p);
let density = dists::pdf(&normal, x, &p);
let integrand = p.mul(vec![
p.mul(vec![p.integer(-1), p.func("log", vec![density])]),
density,
]);
let wrong = p.add(vec![
p.mul(vec![
p.rational(1, 2),
p.func("log", vec![p.mul(vec![p.integer(2), pi(&p), sigma])]),
]),
p.rational(1, 2),
]);
let err = verify::check(simplify(wrong, &p).value, integrand, x, &normal, &p).unwrap_err();
assert_eq!(err.code(), "E-PROB-005");
let q = Distribution::normal(sym(&p, "mu_h"), sym(&p, "sigma_h"), &p).unwrap();
let (dp, dq) = (dists::pdf(&normal, x, &p), dists::pdf(&q, x, &p));
let kl_integrand = p.mul(vec![p.func("log", vec![div(dp, dq, &p)]), dp]);
let good = kl_divergence(&normal, &q, None, &p).unwrap().value;
let flipped = simplify(
p.add(vec![
good,
p.mul(vec![
p.integer(-2),
p.func("log", vec![div(q.params()[1], sigma, &p)]),
]),
]),
&p,
)
.value;
let err = verify::check_pair(
flipped,
kl_integrand,
x,
&normal,
&q,
&q.constraints(&p),
&p,
)
.unwrap_err();
assert_eq!(err.code(), "E-PROB-005");
}