use symplex::prelude::*;
use symplex::stats::{Distribution, RandomVariable, Rng, Support};
fn assert_exact(actual: &Ex, expected: &Ex, label: &str) {
assert_eq!(
actual.equals(expected),
Some(true),
"{label}: got `{actual}`, expected `{expected}`"
);
}
fn assert_close(actual: &Ex, expected: f64, label: &str) {
let v = actual
.eval_f64()
.unwrap_or_else(|e| panic!("{label}: `{actual}` did not evaluate: {e}"));
assert!(
(v - expected).abs() < 1e-12 * expected.abs().max(1.0),
"{label}: {v} vs {expected}"
);
}
fn open_half_line(ctx: &Context, lo: Ex) -> Support {
Support::from_pieces(
symplex::stats::Kind::Continuous,
vec![symplex::stats::Piece::Interval(Interval::open(
lo,
ctx.infinity(),
))],
)
}
fn standard_normal(ctx: &Context) -> RandomVariable {
RandomVariable::new(ctx, "N", Distribution::normal(ctx.int(0), ctx.int(1)))
}
#[test]
fn half_normal_by_conditioning() -> Result<(), SymplexError> {
let ctx = Context::new();
let n = standard_normal(&ctx);
let half = n.given(&n.symbol().gt(&ctx.int(0)))?;
assert_eq!(half.distribution().name(), "Truncated");
assert_exact(
&half.mean(),
&(ctx.int(2) / ctx.pi()).sqrt(),
"E[N | N > 0]",
);
assert_exact(
&half.variance(),
&(ctx.one() - ctx.int(2) / ctx.pi()),
"Var[N | N > 0]",
);
assert_exact(
&half.probability(&n.symbol().lt(&ctx.int(1)))?,
&(ctx.int(2).sqrt() / 2).erf(),
"P(N < 1 | N > 0)",
);
assert_eq!(half.support(), open_half_line(&ctx, ctx.int(0)));
let x = ctx.symbol("x");
assert_exact(
&half.density(&x),
&(2 * n.density(&x)),
"density doubles on the half-line",
);
assert_exact(
&half.probability(&ctx.bool_true())?,
&ctx.one(),
"P(true) = 1",
);
Ok(())
}
#[test]
fn conditioned_uniform_and_binomial() -> Result<(), SymplexError> {
let ctx = Context::new();
let u = RandomVariable::new(&ctx, "U", Distribution::uniform(ctx.int(0), ctx.int(1)));
let uh = u.given(&u.symbol().gt(&ctx.rational(1, 2)))?;
assert_exact(&uh.mean(), &ctx.rational(3, 4), "E[U | U > ½]");
assert_exact(&uh.variance(), &ctx.rational(1, 48), "Var[U | U > ½]");
assert_exact(
&uh.cdf(&ctx.rational(3, 4)),
&ctx.rational(1, 2),
"F(¾ | U > ½)",
);
assert_exact(
&uh.quantile(&ctx.rational(1, 2)).unwrap(),
&ctx.rational(3, 4),
"median of U | U > ½",
);
let b = RandomVariable::new(
&ctx,
"B",
Distribution::binomial(ctx.int(5), ctx.rational(1, 3)),
);
let bg = b.given(&b.symbol().ge(&ctx.int(2)))?;
assert_exact(&bg.mean(), &ctx.rational(325, 131), "E[B | B ≥ 2]");
assert_exact(
&bg.probability(&b.symbol().eq_expr(&ctx.int(3)))?,
&ctx.rational(40, 131),
"P(B = 3 | B ≥ 2)",
);
assert!(matches!(
standard_normal(&ctx).given(&ctx.symbol("N").eq_expr(&ctx.int(0))),
Err(SymplexError::InvalidArgument { .. })
));
Ok(())
}
#[test]
fn conditioning_on_a_non_linear_event_uses_the_set_machinery() -> Result<(), SymplexError> {
let ctx = Context::new();
let v = RandomVariable::new(&ctx, "V", Distribution::uniform(ctx.int(-1), ctx.int(1)));
let inner = v.given(&v.symbol().powi(2).lt(&ctx.rational(1, 4)))?;
assert_eq!(inner.mean(), ctx.int(0));
assert_exact(
&inner.variance(),
&ctx.rational(1, 12),
"Var of Uniform(−½, ½)",
);
let n = standard_normal(&ctx);
let inside = n.probability(&n.symbol().powi(2).lt(&ctx.int(1)))?;
let outside = n.probability(&n.symbol().gt(&ctx.int(1)).or(&n.symbol().lt(&ctx.int(-1))))?;
assert_exact(
&(inside + outside).simplify(),
&ctx.one(),
"complementary events",
);
assert_close(
&n.probability(&n.symbol().powi(2).lt(&ctx.int(1)))?,
0.682_689_492_137_085_9,
"P(N² < 1)",
);
Ok(())
}
#[test]
fn affine_map_of_a_normal_is_a_normal() -> Result<(), SymplexError> {
let ctx = Context::new();
let n = standard_normal(&ctx);
let w = n.transform("W", &(2 * n.symbol() + 1))?;
assert_eq!(w.distribution().name(), "Affine");
let x = ctx.symbol("x");
assert_exact(
&w.density(&x),
&(ctx.int(2).sqrt() * (-((&x / 2 - ctx.rational(1, 2)).powi(2)) / 2).exp()
/ (4 * ctx.pi().sqrt())),
"density of 2N + 1",
);
assert_eq!(w.mean(), ctx.int(1));
assert_eq!(w.variance(), ctx.int(4));
let direct = Distribution::normal(ctx.int(1), ctx.int(2));
let t = ctx.symbol("t");
assert_exact(&w.mgf(&t), &direct.mgf(&t), "mgf");
assert_exact(&w.cdf(&ctx.int(3)), &direct.cdf(&ctx.int(3)), "cdf(3)");
assert_exact(
&w.quantile(&ctx.rational(3, 4)).unwrap(),
&direct.quantile(&ctx.rational(3, 4)).unwrap(),
"quantile(¾)",
);
assert_exact(&w.moment(3), &direct.moment(3), "E[W³] = 1 + 3·4 = 13");
assert_eq!(w.moment(3), ctx.int(13));
let m = n.transform("M", &(-n.symbol()))?;
assert_exact(
&m.probability(&m.symbol().gt(&ctx.int(-1)))?,
&(ctx.rational(1, 2) + (ctx.int(2).sqrt() / 2).erf() / 2),
"P(−N > −1)",
);
let a = ctx.symbol("a");
assert!(matches!(
n.distribution().affine(a, ctx.int(0)),
Err(SymplexError::InvalidArgument { .. })
));
Ok(())
}
#[test]
fn square_and_absolute_value_of_a_normal() -> Result<(), SymplexError> {
let ctx = Context::new();
let n = standard_normal(&ctx);
let y = ctx.symbol("y");
let sq = n.transform("S", &n.symbol().powi(2))?;
assert_eq!(sq.distribution().name(), "Transformed");
assert_eq!(sq.support(), Support::half_line(ctx.int(0)));
assert_exact(
&sq.density(&y),
&(ctx.int(2).sqrt() * (-&y / 2).exp() / (2 * ctx.pi().sqrt() * y.sqrt())),
"χ²(1) density",
);
assert_eq!(sq.mean(), ctx.int(1));
assert_eq!(sq.variance(), ctx.int(2));
let ab = n.transform("A", &n.symbol().abs())?;
assert_exact(&ab.density(&y), &(2 * n.density(&y)), "half-normal density");
assert_exact(&ab.mean(), &(ctx.int(2) / ctx.pi()).sqrt(), "E|N|");
assert_exact(
&ab.variance(),
&(ctx.one() - ctx.int(2) / ctx.pi()),
"Var|N|",
);
Ok(())
}
#[test]
fn monotone_maps_by_change_of_variables() -> Result<(), SymplexError> {
let ctx = Context::new();
let y = ctx.symbol("y");
let n = standard_normal(&ctx);
let en = n.transform("E", &n.symbol().exp())?;
assert_exact(
&en.density(&y),
&(ctx.int(2).sqrt() * (-(y.ln().powi(2)) / 2).exp() / (2 * ctx.pi().sqrt() * &y)),
"log-normal density",
);
assert_exact(&en.mean(), &ctx.rational(1, 2).exp(), "E[eᴺ] = √e");
assert_eq!(en.support(), open_half_line(&ctx, ctx.int(0)));
let ex = RandomVariable::new(&ctx, "Ex", Distribution::exponential(ctx.int(2)));
let f = ex.transform("F", &(-ex.symbol()).exp())?;
assert_exact(&f.density(&y), &(2 * &y), "density of e^{−Ex}");
assert_eq!(f.mean(), ctx.rational(2, 3));
let u = RandomVariable::new(&ctx, "U", Distribution::uniform(ctx.int(0), ctx.int(1)));
let iu = u.transform("I", &(1 / u.symbol()))?;
assert_eq!(iu.support(), Support::half_line(ctx.int(1)));
assert_exact(&iu.density(&y), &y.powi(-2), "density of 1/U");
assert_eq!(
iu.probability(&iu.symbol().gt(&ctx.int(2)))?,
ctx.rational(1, 2)
);
assert!(matches!(
n.transform("Z", &n.symbol().sin()),
Err(SymplexError::NotImplemented(_))
));
Ok(())
}
#[test]
fn transforming_a_finite_range_maps_and_merges_values() -> Result<(), SymplexError> {
let ctx = Context::new();
let d = RandomVariable::new(&ctx, "D", Distribution::die(ctx.int(6)));
let d2 = d.transform("D2", &d.symbol().powi(2))?;
assert_eq!(d2.distribution().name(), "Finite");
assert_eq!(d2.mean(), ctx.rational(91, 6));
assert_eq!(
d2.probability(&d2.symbol().eq_expr(&ctx.int(9)))?,
ctx.rational(1, 6)
);
let m = d.transform("M", &(d.symbol() - 3).powi(2))?;
assert_eq!(
m.probability(&m.symbol().eq_expr(&ctx.int(1)))?,
ctx.rational(1, 3)
);
assert_eq!(
m.probability(&m.symbol().eq_expr(&ctx.int(0)))?,
ctx.rational(1, 6)
);
Ok(())
}
#[test]
fn mixture_of_two_normals() -> Result<(), SymplexError> {
let ctx = Context::new();
let a = Distribution::normal(ctx.int(-1), ctx.int(1));
let b = Distribution::normal(ctx.int(3), ctx.int(2));
let m = Distribution::mixture(&[
(ctx.rational(1, 4), a.clone()),
(ctx.rational(3, 4), b.clone()),
])?;
let x = RandomVariable::new(&ctx, "X", m);
assert_eq!(x.mean(), ctx.int(2));
assert_eq!(x.moment(2), ctx.rational(41, 4));
assert_eq!(x.variance(), ctx.rational(25, 4));
let zero = ctx.int(0);
let region = Support::half_line(zero.clone());
let expected = (ctx.rational(1, 4) * a.probability_of(®ion)?
+ ctx.rational(3, 4) * b.probability_of(®ion)?)
.simplify();
assert_exact(
&x.probability(&x.symbol().gt(&zero))?,
&expected,
"P(X > 0) is the weighted sum",
);
assert_close(
&x.probability(&x.symbol().gt(&zero))?,
0.25 * (1.0 - 0.8413447460685429) + 0.75 * 0.9331927987311419,
"P(X > 0)",
);
let v = ctx.symbol("v");
assert_exact(
&x.density(&v),
&(ctx.rational(1, 4) * a.density(&v) + ctx.rational(3, 4) * b.density(&v)),
"density",
);
let mut rng = Rng::new(11);
let s = x.sample(20_000, &mut rng)?;
let mean = s.iter().sum::<f64>() / s.len() as f64;
assert!((mean - 2.0).abs() < 0.06, "sample mean {mean}");
assert!(Distribution::mixture(&[]).is_err());
assert!(
Distribution::mixture(&[
(ctx.rational(1, 2), a.clone()),
(ctx.rational(1, 3), b.clone())
])
.is_err(),
"weights must sum to 1"
);
assert!(
Distribution::mixture(&[(ctx.one(), Distribution::die(ctx.int(6)))]).is_ok()
&& Distribution::mixture(&[
(ctx.rational(1, 2), a),
(ctx.rational(1, 2), Distribution::die(ctx.int(6)))
])
.is_err(),
"one kind per mixture"
);
Ok(())
}
#[test]
fn sampling_reaches_normals_truncations_and_transforms() -> Result<(), SymplexError> {
let ctx = Context::new();
let n = standard_normal(&ctx);
let mut rng = Rng::new(3);
let s = n.sample(20_000, &mut rng)?;
let var = s.iter().map(|z| z * z).sum::<f64>() / s.len() as f64;
assert!((var - 1.0).abs() < 0.05, "variance {var}");
let half = n.given(&n.symbol().gt(&ctx.int(0)))?;
let s = half.sample(20_000, &mut rng)?;
assert!(s.iter().all(|v| *v > 0.0));
let mean = s.iter().sum::<f64>() / s.len() as f64;
assert!(
(mean - 0.797_884_560_802_865_4).abs() < 0.02,
"half-normal mean {mean}"
);
let sq = n.transform("S", &n.symbol().powi(2))?;
let s = sq.sample(20_000, &mut rng)?;
let mean = s.iter().sum::<f64>() / s.len() as f64;
assert!((mean - 1.0).abs() < 0.05, "χ²(1) mean {mean}");
Ok(())
}