use std::any::Any;
use std::cmp::Ordering;
use std::fmt;
use std::sync::Arc;
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::api::poly_ex::Poly;
use crate::base::errors::SymplexError;
use crate::base::interval::Interval;
use super::sample::Rng;
use super::support::{Kind, Piece, Support, is_neg_inf, is_pos_inf};
pub type Sampler = Box<dyn FnMut(&mut Rng) -> f64 + Send>;
const FAR_TAIL: f64 = 1.0 / 4_294_967_296.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Tail {
Lower,
Upper,
Neither,
}
const TIE_BAND: f64 = 1.0 / 17_592_186_044_416.0;
#[derive(Clone, Copy, Debug)]
enum Level {
Lower(f64),
Upper(f64),
}
impl Level {
fn nearest(self) -> Self {
match self {
Level::Lower(p) if p > 0.5 => Level::Upper(1.0 - p),
Level::Upper(q) if q > 0.5 => Level::Lower(1.0 - q),
level => level,
}
}
fn reflected(self) -> Self {
match self {
Level::Lower(p) => Level::Upper(p),
Level::Upper(q) => Level::Lower(q),
}
}
fn lower_ex(self, ctx: &Context) -> Result<Ex, SymplexError> {
match self {
Level::Lower(p) => ctx.from_f64(p),
Level::Upper(q) => Ok(ctx.one() - ctx.from_f64(q)?),
}
}
fn lower_f64(self) -> f64 {
match self {
Level::Lower(p) => p,
Level::Upper(q) => 1.0 - q,
}
}
fn pick<R>(self, ppf: impl FnOnce(f64) -> R, isf: impl FnOnce(f64) -> R) -> R {
match self {
Level::Lower(p) => ppf(p),
Level::Upper(q) => isf(q),
}
}
fn crossed(self, tail: &Ex) -> Result<bool, SymplexError> {
let (level, lower) = match self {
Level::Lower(p) => (p, true),
Level::Upper(q) => (q, false),
};
let at = tail.context().from_f64(level)?;
let d = ((tail - &at) / &at).eval_f64()?;
if d.is_nan() {
return Err(SymplexError::computation_failed(
"quantile_f64",
"the distribution function is not a number",
));
}
Ok(if lower { d >= 0.0 } else { d <= 0.0 })
}
fn crossed_f64(self, t: f64) -> Option<bool> {
let (level, lower) = match self {
Level::Lower(p) => (p, true),
Level::Upper(q) => (q, false),
};
if level.is_nan() || level < f64::MIN_POSITIVE || t.is_nan() {
return None;
}
if t > level * (1.0 + TIE_BAND) {
Some(lower)
} else if t < level * (1.0 - TIE_BAND) {
Some(!lower)
} else {
None
}
}
}
fn grow_bracket(
g: &impl Fn(f64) -> f64,
x0: Option<f64>,
lo: f64,
hi: f64,
) -> Result<(f64, f64), SymplexError> {
let fail = |why: String| SymplexError::computation_failed("quantile_f64", why);
let inside = |v: f64| v.is_finite() && v > lo && v < hi;
let s = match x0.filter(|&v| inside(v)) {
Some(v) => v,
None => match (lo.is_finite(), hi.is_finite()) {
(true, true) => lo + 0.5 * (hi - lo),
(true, false) => lo + 1.0_f64.max(lo.abs()),
(false, true) => hi - 1.0_f64.max(hi.abs()),
(false, false) => 0.0,
},
};
let gs = g(s);
if gs.is_nan() {
return Err(fail(format!(
"the distribution function is not a number at {s}"
)));
}
if gs == 0.0 {
return Ok((s, s));
}
let up = gs < 0.0;
let mut near = s;
let mut d = if s == 0.0 { 1.0 } else { s.abs() } * 2.0_f64.powi(-26);
for _ in 0..600 {
let far = if up { (s + d).min(hi) } else { (s - d).max(lo) };
let gf = g(far);
if gf.is_nan() {
return Err(fail(format!(
"the distribution function is not a number at {far}"
)));
}
if (gf >= 0.0) == up {
return Ok(if up { (near, far) } else { (far, near) });
}
if !far.is_finite() {
break;
}
near = far;
d *= 4.0;
}
Err(fail(format!(
"no bracket found for the level starting from {s}"
)))
}
#[derive(Default)]
struct CompensatedSum {
sum: f64,
carry: f64,
}
impl CompensatedSum {
fn add(&mut self, x: f64) {
let t = self.sum + x;
self.carry += if self.sum.abs() >= x.abs() {
(self.sum - t) + x
} else {
(x - t) + self.sum
};
self.sum = t;
}
fn value(&self) -> f64 {
self.sum + self.carry
}
}
pub(crate) fn sign_of(e: &Ex) -> Option<Ordering> {
if e.is_zero() == Some(true) {
return Some(Ordering::Equal);
}
if e.is_positive() == Some(true) {
return Some(Ordering::Greater);
}
if e.is_negative() == Some(true) {
return Some(Ordering::Less);
}
if !e.free_symbols().is_empty() {
return None;
}
let digits = e.eval_decimal(5).ok()?;
let magnitude = digits.trim_start_matches('-');
if digits.contains('I')
|| !magnitude.starts_with(|c: char| c.is_ascii_digit())
|| magnitude.trim_start_matches(['0', '.']).is_empty()
{
return None;
}
Some(if digits.starts_with('-') {
Ordering::Less
} else {
Ordering::Greater
})
}
fn lattice_end(e: &Ex) -> Result<f64, SymplexError> {
if is_neg_inf(e) {
Ok(f64::NEG_INFINITY)
} else if is_pos_inf(e) {
Ok(f64::INFINITY)
} else {
e.eval_f64()
}
}
pub trait Family: Any + Send + Sync + fmt::Debug {
fn name(&self) -> &str;
fn context(&self) -> Context;
fn support(&self) -> Support;
fn density(&self, x: &Ex) -> Ex;
fn parameters(&self) -> Vec<(&'static str, Ex)>;
fn eq_family(&self, other: &dyn Family) -> bool;
fn mean(&self) -> Option<Ex> {
None
}
fn variance(&self) -> Option<Ex> {
None
}
fn raw_moment(&self, _n: u32) -> Option<Ex> {
None
}
fn cdf(&self, _x: &Ex) -> Option<Ex> {
None
}
fn cdf_lower(&self, _x: &Ex) -> Option<Ex> {
None
}
fn sf(&self, _x: &Ex) -> Option<Ex> {
None
}
fn mgf(&self, _t: &Ex) -> Option<Ex> {
None
}
fn quantile(&self, _p: &Ex) -> Option<Ex> {
None
}
fn entropy(&self) -> Option<Ex> {
None
}
fn probability_of(&self, _region: &Support) -> Option<Result<Ex, SymplexError>> {
None
}
fn expectation_over(
&self,
_g: &Ex,
_x: &Ex,
_region: &Support,
) -> Option<Result<Ex, SymplexError>> {
None
}
fn sampler(&self) -> Option<Result<Sampler, SymplexError>> {
None
}
fn fmt_display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}(", self.name())?;
for (i, (_, p)) in self.parameters().iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{p}")?;
}
f.write_str(")")
}
}
pub fn same_family<T: Family + PartialEq>(a: &T, other: &dyn Family) -> bool {
(other as &dyn Any)
.downcast_ref::<T>()
.is_some_and(|b| a == b)
}
macro_rules! family_boilerplate {
($ty:ident, $name:literal, [$first:ident $(, $field:ident)*]) => {
fn name(&self) -> &str {
$name
}
fn context(&self) -> $crate::api::context::Context {
self.$first.context()
}
fn parameters(&self) -> Vec<(&'static str, $crate::api::expr::Ex)> {
vec![(stringify!($first), self.$first.clone()) $(, (stringify!($field), self.$field.clone()))*]
}
fn eq_family(&self, other: &dyn $crate::stats::Family) -> bool {
$crate::stats::same_family(self, other)
}
};
($ty:ident, $name:literal) => {
fn name(&self) -> &str {
$name
}
fn eq_family(&self, other: &dyn $crate::stats::Family) -> bool {
$crate::stats::same_family(self, other)
}
};
}
pub(crate) use family_boilerplate;
#[derive(Clone)]
pub struct Distribution(Arc<dyn Family>);
impl PartialEq for Distribution {
fn eq(&self, other: &Self) -> bool {
self.0.eq_family(other.0.as_ref())
}
}
impl fmt::Debug for Distribution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&*self.0, f)
}
}
impl fmt::Display for Distribution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt_display(f)
}
}
pub(crate) fn fresh_symbol(ctx: &Context, prefix: &str, avoid: &[&Ex]) -> Ex {
let mut n = 0u32;
loop {
let name = if n == 0 {
format!("_{prefix}")
} else {
format!("_{prefix}{n}")
};
let s = ctx.symbol(&name);
if !avoid.iter().any(|e| e.contains(&s)) {
return s;
}
n += 1;
}
}
fn not_implemented(msg: impl Into<String>) -> SymplexError {
SymplexError::NotImplemented(msg.into())
}
impl Distribution {
pub fn from_family(family: impl Family) -> Self {
Distribution(Arc::new(family))
}
pub fn family(&self) -> &dyn Family {
self.0.as_ref()
}
pub fn downcast_ref<T: Family>(&self) -> Option<&T> {
(self.0.as_ref() as &dyn Any).downcast_ref::<T>()
}
pub fn context(&self) -> Context {
self.0.context()
}
pub fn name(&self) -> &str {
self.0.name()
}
pub fn parameters(&self) -> Vec<(&'static str, Ex)> {
self.0.parameters()
}
pub fn support(&self) -> Support {
self.0.support()
}
pub fn kind(&self) -> Kind {
self.0.support().kind()
}
pub fn is_continuous(&self) -> bool {
self.kind() == Kind::Continuous
}
pub fn density(&self, x: &Ex) -> Ex {
self.0.density(x)
}
fn param_exprs(&self) -> Vec<Ex> {
self.0.parameters().into_iter().map(|(_, e)| e).collect()
}
pub(crate) fn fresh_var(&self, prefix: &str, also: &[&Ex]) -> Ex {
let params = self.param_exprs();
let mut avoid: Vec<&Ex> = params.iter().collect();
avoid.extend_from_slice(also);
fresh_symbol(&self.context(), prefix, &avoid)
}
pub fn mean(&self) -> Ex {
match self.0.mean() {
Some(m) => m,
None => {
let x = self.fresh_var("x", &[]);
self.expectation(&x, &x)
}
}
}
pub fn variance(&self) -> Ex {
match self.0.variance() {
Some(v) => v,
None => {
let m = self.mean();
(self.moment(2) - m.powi(2)).simplify()
}
}
}
pub fn std(&self) -> Ex {
self.variance().sqrt()
}
pub fn moment(&self, n: u32) -> Ex {
if n == 0 {
return self.context().one();
}
match self.0.raw_moment(n) {
Some(m) => m,
None => {
let x = self.fresh_var("x", &[]);
let integrand = x.powi(i64::from(n)) * self.0.density(&x);
self.integrate_over(&integrand, &x, &self.support())
}
}
}
pub fn central_moment(&self, n: u32) -> Ex {
let mu = self.mean();
let x = self.fresh_var("x", &[&mu]);
self.expectation(&(&x - mu).powi(i64::from(n)), &x)
}
pub fn skewness(&self) -> Ex {
(self.central_moment(3) / self.std().powi(3)).simplify()
}
pub fn kurtosis(&self) -> Ex {
(self.central_moment(4) / self.variance().powi(2)).simplify()
}
fn tail_of(&self, x: &Ex) -> Tail {
if !x.free_symbols().is_empty() {
return Tail::Neither;
}
let Some(classic) = self.0.cdf(x) else {
return Tail::Neither;
};
match classic.eval_f64() {
Ok(p) if p.is_finite() && p < FAR_TAIL => Tail::Lower,
Ok(p) if p.is_finite() && p > 1.0 - FAR_TAIL => Tail::Upper,
Ok(_) => Tail::Neither,
Err(_) => {
let far = |form: Option<Ex>| {
form.and_then(|f| f.eval_f64().ok())
.is_some_and(|v| v.is_finite() && v < FAR_TAIL)
};
if far(self.0.cdf_lower(x)) {
Tail::Lower
} else if far(self.0.sf(x)) {
Tail::Upper
} else {
Tail::Neither
}
}
}
}
pub(crate) fn in_far_upper_tail(&self, x: &Ex) -> bool {
self.tail_of(x) == Tail::Upper
}
pub(crate) fn cdf_on_support(&self, x: &Ex) -> Ex {
if let Some(c) = self.0.cdf(x) {
if self.tail_of(x) == Tail::Lower
&& let Some(lower) = self.0.cdf_lower(x)
{
return lower;
}
return c;
}
let support = self.support();
if let Some(values) = support.as_points() {
let mut acc = self.context().zero();
for v in &values {
let p = self.0.density(v);
match (x - v).is_nonnegative() {
Some(true) => acc += p,
Some(false) => {}
None => acc += p * (x - v).heaviside(),
}
}
return acc.simplify();
}
let t = self.fresh_var("t", &[x]);
let dens = self.0.density(&t);
let ctx = self.context();
let lo = match support.as_interval() {
Some(iv) => iv.lower.clone(),
None => ctx.neg_infinity(),
};
let hi = match support.kind() {
Kind::Continuous => x.clone(),
Kind::Discrete => x.floor().eval(),
};
support.accumulate(&dens, &t, &lo, &hi)
}
pub(crate) fn mass_below(&self, lo: &Ex) -> Option<Ex> {
let ctx = self.context();
if is_neg_inf(lo) {
return Some(ctx.zero());
}
let support = self.support();
if let Some(s) = support.as_interval()
&& !is_neg_inf(&s.lower)
&& (lo - &s.lower).is_zero() == Some(true)
{
return Some(ctx.zero());
}
let at = match support.kind() {
Kind::Continuous => lo.clone(),
Kind::Discrete => lo - ctx.one(),
};
self.0.cdf(&at)?;
Some(self.cdf_on_support(&at))
}
pub(crate) fn sf_on_support(&self, x: &Ex) -> Ex {
let ctx = self.context();
match self.0.cdf(x) {
Some(c) => {
if self.tail_of(x) == Tail::Upper
&& let Some(s) = self.0.sf(x)
{
return s;
}
ctx.one() - c
}
None => self
.0
.sf(x)
.unwrap_or_else(|| ctx.one() - self.cdf_on_support(x)),
}
}
pub(crate) fn mass_above(&self, lo: &Ex) -> Option<Ex> {
let ctx = self.context();
if is_neg_inf(lo) {
return Some(ctx.one());
}
let support = self.support();
if let Some(s) = support.as_interval()
&& !is_neg_inf(&s.lower)
&& (lo - &s.lower).is_zero() == Some(true)
{
return Some(ctx.one());
}
let at = match support.kind() {
Kind::Continuous => lo.clone(),
Kind::Discrete => lo - ctx.one(),
};
if self.0.cdf(&at).is_none() && self.0.sf(&at).is_none() {
return None;
}
Some(self.sf_on_support(&at))
}
pub fn cdf(&self, x: &Ex) -> Ex {
let ctx = self.context();
let support = self.support();
let Some(iv) = support.as_interval() else {
return self.cdf_on_support(x);
};
let (lo, hi) = (&iv.lower, &iv.upper);
let lo_finite = !is_neg_inf(lo);
let hi_finite = !is_pos_inf(hi);
let from_lo = if lo_finite { sign_of(&(x - lo)) } else { None };
let to_hi = if hi_finite { sign_of(&(hi - x)) } else { None };
if from_lo == Some(Ordering::Less) {
return ctx.zero();
}
if support.kind() == Kind::Continuous && from_lo == Some(Ordering::Equal) {
return ctx.zero();
}
if matches!(to_hi, Some(Ordering::Less | Ordering::Equal)) {
return ctx.one();
}
let on = self.cdf_on_support(x);
if (!lo_finite || matches!(from_lo, Some(Ordering::Greater | Ordering::Equal)))
&& (!hi_finite || to_hi == Some(Ordering::Greater))
{
return if x.free_symbols().is_empty() {
on.eval()
} else {
on
};
}
let mut pairs: Vec<(Ex, crate::api::expr::BoolEx)> = Vec::new();
if lo_finite {
pairs.push((ctx.zero(), x.lt(lo)));
}
if hi_finite {
pairs.push((on, x.lt(hi)));
pairs.push((ctx.one(), ctx.bool_true()));
} else {
pairs.push((on, ctx.bool_true()));
}
let refs: Vec<(&Ex, &crate::api::expr::BoolEx)> =
pairs.iter().map(|(a, b)| (a, b)).collect();
Ex::piecewise(&refs)
}
pub fn sf(&self, x: &Ex) -> Ex {
let ctx = self.context();
let support = self.support();
let Some(iv) = support.as_interval() else {
return self.sf_on_support(x);
};
let (lo, hi) = (&iv.lower, &iv.upper);
let lo_finite = !is_neg_inf(lo);
let hi_finite = !is_pos_inf(hi);
let from_lo = if lo_finite { sign_of(&(x - lo)) } else { None };
let to_hi = if hi_finite { sign_of(&(hi - x)) } else { None };
if from_lo == Some(Ordering::Less) {
return ctx.one();
}
if support.kind() == Kind::Continuous && from_lo == Some(Ordering::Equal) {
return ctx.one();
}
if matches!(to_hi, Some(Ordering::Less | Ordering::Equal)) {
return ctx.zero();
}
let on = self.sf_on_support(x);
if (!lo_finite || matches!(from_lo, Some(Ordering::Greater | Ordering::Equal)))
&& (!hi_finite || to_hi == Some(Ordering::Greater))
{
return if x.free_symbols().is_empty() {
on.eval()
} else {
on
};
}
let mut pairs: Vec<(Ex, crate::api::expr::BoolEx)> = Vec::new();
if lo_finite {
pairs.push((ctx.one(), x.lt(lo)));
}
if hi_finite {
pairs.push((on, x.lt(hi)));
pairs.push((ctx.zero(), ctx.bool_true()));
} else {
pairs.push((on, ctx.bool_true()));
}
let refs: Vec<(&Ex, &crate::api::expr::BoolEx)> =
pairs.iter().map(|(a, b)| (a, b)).collect();
Ex::piecewise(&refs)
}
pub fn mgf(&self, t: &Ex) -> Ex {
match self.0.mgf(t) {
Some(m) => m,
None => {
let x = self.fresh_var("x", &[t]);
self.expectation(&(t * &x).exp(), &x)
}
}
}
pub fn characteristic_function(&self, t: &Ex) -> Ex {
let it = self.context().i_unit() * t;
match self.0.mgf(&it) {
Some(m) => m,
None => {
let x = self.fresh_var("x", &[t]);
self.expectation(&(&it * &x).exp(), &x)
}
}
}
pub fn quantile(&self, p: &Ex) -> Option<Ex> {
self.0.quantile(p)
}
pub fn median(&self) -> Option<Ex> {
self.quantile(&self.context().rational(1, 2))
}
pub fn quantile_f64(&self, p: f64) -> Result<f64, SymplexError> {
self.quantile_at(Level::Lower(p))
}
pub fn isf_f64(&self, q: f64) -> Result<f64, SymplexError> {
self.quantile_at(Level::Upper(q)).map_err(|e| match e {
SymplexError::ComputationFailed {
operation: "quantile_f64",
reason,
} => SymplexError::computation_failed("isf_f64", reason),
other => other,
})
}
fn quantile_at(&self, level: Level) -> Result<f64, SymplexError> {
let (op, name, v) = match level {
Level::Lower(p) => ("quantile_f64", "p", p),
Level::Upper(q) => ("isf_f64", "q", q),
};
if !(v > 0.0 && v < 1.0) {
return Err(SymplexError::invalid_argument(
op,
format!("{name} must lie strictly between 0 and 1, got {v}"),
));
}
if let Some(x) = self.kernel_quantile_f64(level) {
return x;
}
let support = self.support();
if support.kind() == Kind::Discrete {
return self.discrete_quantile_f64(level, &support);
}
let ctx = self.context();
if let Some(x) = self.0.quantile(&level.lower_ex(&ctx)?) {
return x.eval_f64();
}
self.continuous_quantile_f64(level.nearest(), &support)
}
fn continuous_quantile_f64(
&self,
level: Level,
support: &Support,
) -> Result<f64, SymplexError> {
if let Some((name, _)) = self
.parameters()
.into_iter()
.find(|(_, e)| !e.free_symbols().is_empty())
{
return Err(SymplexError::Unevaluable {
reason: format!("quantile_f64: the parameter {name} is symbolic"),
});
}
let ctx = self.context();
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for piece in support.pieces() {
let (a, b) = match piece {
Piece::Interval(iv) => (lattice_end(&iv.lower)?, lattice_end(&iv.upper)?),
Piece::Point(v) => {
let v = v.eval_f64()?;
(v, v)
}
};
lo = lo.min(a);
hi = hi.max(b);
}
if support.is_empty() || lo.is_nan() || hi.is_nan() {
return Err(SymplexError::computation_failed(
"quantile_f64",
"unsupported support shape",
));
}
let (lvl, sign) = match level {
Level::Lower(p) => (p, 1.0),
Level::Upper(q) => (q, -1.0),
};
let subnormal = lvl < f64::MIN_POSITIVE;
let lvl_ex = ctx.from_f64(lvl)?;
let g = |v: f64| -> f64 {
let miss = if v.is_nan() {
f64::NAN
} else if v <= lo || v >= hi {
let below = v <= lo;
let t = match level {
Level::Lower(_) => f64::from(u8::from(!below)),
Level::Upper(_) => f64::from(u8::from(below)),
};
(t - lvl) / lvl
} else {
let Ok(at) = ctx.from_f64(v) else {
return f64::NAN;
};
let tail = match level {
Level::Lower(_) => self.cdf_on_support(&at),
Level::Upper(_) => self.sf_on_support(&at),
};
if subnormal {
((tail - &lvl_ex) / &lvl_ex).eval_f64().unwrap_or(f64::NAN)
} else {
(tail.eval_f64().unwrap_or(f64::NAN) - lvl) / lvl
}
};
(sign * miss).clamp(-f64::MAX, f64::MAX)
};
let x0 = self.classic_quantile_estimate(level.lower_f64(), lo, hi);
let (a, b) = grow_bracket(&g, x0, lo, hi)?;
if a == b {
return Ok(a);
}
let opts = crate::domains::optimize::RootOpts {
xtol: f64::MIN_POSITIVE,
rtol: 4.0 * f64::EPSILON,
max_iter: 1200,
};
crate::domains::optimize::brent_root(g, a, b, &opts)
.map_err(|e| SymplexError::computation_failed("quantile_f64", e.to_string()))
}
fn classic_quantile_estimate(&self, p: f64, lo: f64, hi: f64) -> Option<f64> {
let x = self.fresh_var("x", &[]);
let name = x.to_string();
let compiled = self.cdf(&x).compile(&[name.as_str()]).ok()?;
let g = |v: f64| compiled.call(&[v]) - p;
let (mut a, mut b) = match (lo.is_finite(), hi.is_finite()) {
(true, true) => (lo, hi),
(true, false) => (lo, lo + 1.0),
(false, true) => (hi - 1.0, hi),
(false, false) => (-1.0, 1.0),
};
let mut step = 1.0;
for _ in 0..1100 {
let (ga, gb) = (g(a), g(b));
if ga <= 0.0 && gb >= 0.0 {
let opts = crate::domains::optimize::RootOpts {
xtol: f64::MIN_POSITIVE,
..Default::default()
};
return crate::domains::optimize::brent_root(g, a, b, &opts)
.ok()
.filter(|v| v.is_finite());
}
if (lo.is_finite() && ga > 0.0) || (hi.is_finite() && gb < 0.0) || ga.is_nan() {
return None;
}
step *= 2.0;
if ga > 0.0 {
a -= step;
}
if gb < 0.0 {
b += step;
}
if !(a.is_finite() && b.is_finite()) {
return None;
}
}
None
}
fn kernel_quantile_f64(&self, level: Level) -> Option<Result<f64, SymplexError>> {
use super::continuous::{Beta, ChiSquared, FDistribution, Gamma, Normal, StudentT};
use super::discrete::{Binomial, Poisson};
use super::numdist;
let num = |e: &Ex| -> Option<f64> {
if e.free_symbols().is_empty() {
e.eval_f64().ok().filter(|v| v.is_finite())
} else {
None
}
};
if let Some(d) = self.downcast_ref::<Normal>() {
let (mean, std) = (num(&d.mean)?, num(&d.std)?);
if std <= 0.0 {
return None;
}
let z = level.pick(numdist::norm::ppf, numdist::norm::isf);
return Some(z.map(|z| mean + std * z));
}
if let Some(d) = self.downcast_ref::<StudentT>() {
let dof = num(&d.dof)?;
return Some(level.pick(|p| numdist::t::ppf(p, dof), |q| numdist::t::isf(q, dof)));
}
if let Some(d) = self.downcast_ref::<ChiSquared>() {
let dof = num(&d.dof)?;
return Some(level.pick(
|p| numdist::chi2::ppf(p, dof),
|q| numdist::chi2::isf(q, dof),
));
}
if let Some(d) = self.downcast_ref::<FDistribution>() {
let (d1, d2) = (num(&d.d1)?, num(&d.d2)?);
return Some(level.pick(
|p| numdist::f::ppf(p, d1, d2),
|q| numdist::f::isf(q, d1, d2),
));
}
if let Some(d) = self.downcast_ref::<Beta>() {
let (a, b) = (num(&d.alpha)?, num(&d.beta)?);
return Some(level.pick(
|p| numdist::beta::ppf(p, a, b),
|q| numdist::beta::isf(q, a, b),
));
}
if let Some(d) = self.downcast_ref::<Gamma>() {
let (shape, scale) = (num(&d.shape)?, num(&d.scale)?);
return Some(level.pick(
|p| numdist::gamma::ppf(p, shape, scale),
|q| numdist::gamma::isf(q, shape, scale),
));
}
if let Some(d) = self.downcast_ref::<Binomial>() {
let (n, prob) = (num(&d.n)?, num(&d.p)?);
if n.fract() != 0.0 {
return None;
}
return Some(level.pick(
|p| numdist::binom::ppf(p, n, prob),
|q| numdist::binom::isf(q, n, prob),
));
}
if let Some(d) = self.downcast_ref::<Poisson>() {
let rate = num(&d.rate)?;
return Some(level.pick(
|p| numdist::poisson::ppf(p, rate),
|q| numdist::poisson::isf(q, rate),
));
}
if let Some(d) = self.downcast_ref::<super::wrappers::Affine>()
&& d.inner.kind() == Kind::Continuous
{
let (a, b) = (num(&d.a)?, num(&d.b)?);
let at = if d.increasing {
level
} else {
level.reflected()
};
return Some(d.inner.quantile_at(at).map(|x| a * x + b));
}
None
}
fn discrete_quantile_f64(&self, asked: Level, support: &Support) -> Result<f64, SymplexError> {
let level = asked.nearest();
if let Some(values) = support.as_points() {
return self.table_quantile_f64(level, &values);
}
let probe = self.fresh_var("x", &[]);
let closed = self.0.cdf(&probe).is_some()
|| (matches!(level, Level::Upper(_)) && self.0.sf(&probe).is_some());
let lattice = support.normalize_lattice();
let Some(iv) = lattice.as_interval() else {
return self.pieces_quantile_f64(level, &lattice);
};
let (lo, hi) = (lattice_end(&iv.lower)?, lattice_end(&iv.upper)?);
if closed {
let k0 = self.lattice_guess(asked, level, lo, hi)?;
return self.lattice_search(level, k0, lo, hi);
}
if let Some(k) = self.lattice_walk(level, lo, hi, false)? {
return Ok(k);
}
self.lattice_search(level, self.mean_f64().unwrap_or(f64::NAN), lo, hi)
}
fn mean_f64(&self) -> Option<f64> {
self.0
.mean()
.and_then(|m| m.eval_f64().ok())
.filter(|v| v.is_finite())
}
fn tail_crossed(&self, level: Level, x: &Ex) -> Result<bool, SymplexError> {
let tail = match level {
Level::Lower(_) => self.cdf_on_support(x),
Level::Upper(_) => self.sf_on_support(x),
};
level.crossed(&tail)
}
fn lattice_search(&self, level: Level, k0: f64, lo: f64, hi: f64) -> Result<f64, SymplexError> {
if lo.is_nan() || hi.is_nan() || lo > hi {
return Err(SymplexError::computation_failed(
"quantile_f64",
format!("empty lattice {lo}..={hi}"),
));
}
let ctx = self.context();
let failure = std::cell::RefCell::new(None);
let crossed = |k: f64| match ctx.from_f64(k).and_then(|kx| self.tail_crossed(level, &kx)) {
Ok(c) => Some(c),
Err(e) => {
failure.borrow_mut().get_or_insert(e);
None
}
};
let k0 = [k0, lo, hi]
.into_iter()
.find(|v| v.is_finite())
.unwrap_or(0.0);
super::numdist::discrete_search("quantile_f64", crossed, k0, lo, hi)
.map_err(|e| failure.into_inner().unwrap_or(e))
}
fn lattice_guess(
&self,
asked: Level,
level: Level,
lo: f64,
hi: f64,
) -> Result<f64, SymplexError> {
let ctx = self.context();
if let Some(q) = self.0.quantile(&asked.lower_ex(&ctx)?)
&& let Ok(v) = q.eval_f64()
&& v.is_finite()
{
return Ok(v);
}
if let Ok(Some(k)) = self.lattice_walk(level, lo, hi, true) {
return Ok(k);
}
Ok(self.mean_f64().unwrap_or(f64::NAN))
}
fn lattice_walk(
&self,
level: Level,
lo: f64,
hi: f64,
guess: bool,
) -> Result<Option<f64>, SymplexError> {
const LATTICE_WALK_LIMIT: usize = 100_000;
let ctx = self.context();
let k_var = self.fresh_var("k", &[]);
let compiled = self
.0
.density(&k_var)
.compile(&[k_var.to_string().as_str()])
.ok();
if compiled.is_none() && guess {
return Ok(None);
}
let exact_pmf =
|k: f64| -> Result<Ex, SymplexError> { Ok(self.0.density(&ctx.from_f64(k)?).eval()) };
let pmf = |k: f64| -> Result<f64, SymplexError> {
match compiled.as_ref().map(|f| f.call(&[k])) {
Some(v) if v.is_finite() => Ok(v),
_ => exact_pmf(k)?.eval_f64(),
}
};
let decide = |t: f64, a: f64, b: f64| -> Result<bool, SymplexError> {
if let Some(c) = level.crossed_f64(t) {
return Ok(c);
}
if guess {
return Ok(match level {
Level::Lower(p) => t >= p,
Level::Upper(q) => t <= q,
});
}
let mut acc = ctx.zero();
let mut j = a;
while j <= b {
acc += exact_pmf(j)?;
j += 1.0;
}
level.crossed(&acc)
};
let mut acc = CompensatedSum::default();
match level {
Level::Lower(_) if lo.is_finite() => {
let mut k = lo;
for _ in 0..LATTICE_WALK_LIMIT {
acc.add(pmf(k)?);
if decide(acc.value(), lo, k)? || k >= hi {
return Ok(Some(k));
}
k += 1.0;
}
Ok(None)
}
Level::Upper(_) if hi.is_finite() => {
let mut k = hi;
for _ in 0..LATTICE_WALK_LIMIT {
if k <= lo {
return Ok(Some(k));
}
acc.add(pmf(k)?);
if !decide(acc.value(), k, hi)? {
return Ok(Some(k));
}
k -= 1.0;
}
Ok(None)
}
Level::Upper(q) if lo.is_finite() => {
let target = (1.0 - q).min(1.0 - 4.0 * f64::EPSILON);
let mut k = lo;
for _ in 0..LATTICE_WALK_LIMIT {
acc.add(pmf(k)?);
if acc.value() >= target {
return Ok(Some(k));
}
k += 1.0;
}
Ok(None)
}
_ => Ok(None),
}
}
fn table_quantile_f64(&self, level: Level, values: &[Ex]) -> Result<f64, SymplexError> {
let mut seen: Vec<&Ex> = Vec::with_capacity(values.len());
let mut pts: Vec<(f64, Ex, f64)> = Vec::with_capacity(values.len());
for v in values {
if seen.contains(&v) {
continue;
}
seen.push(v);
let mass = self.0.density(v).eval();
let m = mass.eval_f64()?;
pts.push((v.eval_f64()?, mass, m));
}
pts.sort_by(|a, b| a.0.total_cmp(&b.0));
let ctx = self.context();
let exact_sum = |range: &[(f64, Ex, f64)]| -> Ex {
range
.iter()
.fold(ctx.zero(), |acc, (_, mass, _)| acc + mass)
};
let mut acc = CompensatedSum::default();
match level {
Level::Lower(_) => {
for (i, (v, _, m)) in pts.iter().enumerate() {
acc.add(*m);
let crossed = match level.crossed_f64(acc.value()) {
Some(c) => c,
None => level.crossed(&exact_sum(&pts[..=i]))?,
};
if crossed {
return Ok(*v);
}
}
Err(SymplexError::computation_failed(
"quantile_f64",
"the masses do not reach p",
))
}
Level::Upper(_) => {
let Some(&(mut answer, _, _)) = pts.last() else {
return Err(SymplexError::computation_failed(
"quantile_f64",
"no values listed",
));
};
for i in (1..pts.len()).rev() {
acc.add(pts[i].2);
let crossed = match level.crossed_f64(acc.value()) {
Some(c) => c,
None => level.crossed(&exact_sum(&pts[i..]))?,
};
if !crossed {
break;
}
answer = pts[i - 1].0;
}
Ok(answer)
}
}
}
fn pieces_quantile_f64(&self, level: Level, lattice: &Support) -> Result<f64, SymplexError> {
let mut best: Option<f64> = None;
for piece in lattice.pieces() {
let found = match piece {
Piece::Point(v) => {
if self.tail_crossed(level, v)? {
Some(v.eval_f64()?)
} else {
None
}
}
Piece::Interval(iv) => {
let (a, b) = (lattice_end(&iv.lower)?, lattice_end(&iv.upper)?);
if b.is_finite() && !self.tail_crossed(level, &iv.upper)? {
None
} else {
Some(self.lattice_search(level, f64::NAN, a, b)?)
}
}
};
if let Some(k) = found {
best = Some(best.map_or(k, |b| b.min(k)));
}
}
best.ok_or_else(|| {
SymplexError::computation_failed("quantile_f64", "the masses do not reach p")
})
}
pub fn entropy(&self) -> Ex {
if let Some(h) = self.0.entropy() {
return h;
}
let x = self.fresh_var("x", &[]);
let neg_log_density = (-self.0.density(&x).ln().expand_log()).simplify();
self.expectation(&neg_log_density, &x)
}
pub fn expectation(&self, g: &Ex, x: &Ex) -> Ex {
if let Some(by_moments) = self.expectation_by_moments(g, x) {
return by_moments;
}
let integrand = g * self.0.density(x);
self.integrate_over(&integrand, x, &self.support())
}
fn expectation_by_moments(&self, g: &Ex, x: &Ex) -> Option<Ex> {
let poly = Poly::new(&g.expand(), &[x])?;
let mut acc = self.context().zero();
for (exps, coeff) in poly.terms() {
let n = *exps.first()?;
let m = if n == 0 {
self.context().one()
} else {
self.0.raw_moment(n)?
};
acc += coeff * m;
}
Some(acc.simplify())
}
pub fn probability_of(&self, region: &Support) -> Result<Ex, SymplexError> {
if let Some(direct) = self.0.probability_of(region) {
return direct;
}
let ctx = self.context();
let support = self.support();
let clipped = support.intersect(region).ok_or_else(|| {
not_implemented(format!(
"probability on {support}: cannot decide which listed values lie in {region}"
))
})?;
let mut total = ctx.zero();
for piece in clipped.pieces() {
total += match piece {
Piece::Interval(iv) => self.interval_mass(iv, &support),
Piece::Point(v) => match support.kind() {
Kind::Continuous => ctx.zero(),
Kind::Discrete => self.0.density(v).eval(),
},
};
}
Ok(total.simplify())
}
fn interval_mass(&self, iv: &Interval<Ex>, support: &Support) -> Ex {
let ctx = self.context();
let (lo, hi) = (&iv.lower, &iv.upper);
if support.kind() == Kind::Continuous && (hi - lo).is_zero() == Some(true) {
return ctx.zero();
}
let at_upper_end = is_pos_inf(hi)
|| support
.as_interval()
.is_some_and(|s| (hi - &s.upper).is_zero() == Some(true));
let probe = self.fresh_var("t", &[lo, hi]);
let has_cdf = self.0.cdf(&probe).is_some();
let has_sf = self.0.sf(&probe).is_some();
if at_upper_end
&& (has_cdf || has_sf)
&& let Some(above) = self.mass_above(lo)
{
return above.simplify();
}
if has_cdf {
if has_sf
&& self.tail_of(lo) == Tail::Upper
&& let Some(above) = self.mass_above(lo)
{
return (above - self.sf_on_support(hi)).simplify();
}
if let Some(below) = self.mass_below(lo) {
return (self.cdf_on_support(hi) - below).simplify();
}
}
let t = probe;
let dens = self.0.density(&t);
support.accumulate(&dens, &t, lo, hi).simplify()
}
pub fn expectation_over(&self, g: &Ex, x: &Ex, region: &Support) -> Result<Ex, SymplexError> {
if let Some(direct) = self.0.expectation_over(g, x, region) {
return direct;
}
let support = self.support();
let clipped = support.intersect(region).ok_or_else(|| {
not_implemented(format!(
"expectation on {support}: cannot decide which listed values lie in {region}"
))
})?;
let integrand = g * self.0.density(x);
Ok(self.integrate_over(&integrand, x, &clipped))
}
pub(crate) fn integrate_over(&self, integrand: &Ex, x: &Ex, region: &Support) -> Ex {
let ctx = self.context();
let integrand = integrand.simplify();
let region = region.normalize_lattice();
let over = |iv: &Interval<Ex>, f: &Ex| region.accumulate(f, x, &iv.lower, &iv.upper);
let mut acc = ctx.zero();
for piece in region.pieces() {
acc += match piece {
Piece::Interval(iv) => {
let first = over(iv, &integrand);
if first.has_unevaluated() {
let expanded = integrand.expand();
if expanded != integrand {
let second = over(iv, &expanded);
if !second.has_unevaluated() {
acc += second;
continue;
}
}
}
first
}
Piece::Point(v) => integrand.subs(x, v).eval(),
};
}
acc.simplify()
}
pub fn sample(&self, n: usize, rng: &mut Rng) -> Result<Vec<f64>, SymplexError> {
let mut sampler = self.sampler()?;
let mut out = Vec::new();
out.try_reserve_exact(n).map_err(|_| {
SymplexError::invalid_argument("sample", format!("cannot allocate {n} samples"))
})?;
out.extend((0..n).map(|_| sampler(rng)));
Ok(out)
}
pub fn sample_one(&self, rng: &mut Rng) -> Result<f64, SymplexError> {
Ok(self.sampler()?(rng))
}
pub fn sampler(&self) -> Result<Sampler, SymplexError> {
if let Some(own) = self.0.sampler() {
return own;
}
let ctx = self.context();
let support = self.support();
match support.kind() {
Kind::Continuous => {
let p = self.fresh_var("p", &[]);
let q = self.0.quantile(&p).ok_or_else(|| {
not_implemented(format!(
"sampling {}: no closed-form quantile function",
self.name()
))
})?;
let name = p.to_string();
let q = q.compile(&[name.as_str()])?;
Ok(Box::new(move |rng: &mut Rng| q.call(&[rng.next_f64()])))
}
Kind::Discrete => {
if let Some(values) = support.as_points() {
let mut cumulative = Vec::with_capacity(values.len());
let mut points = Vec::with_capacity(values.len());
let mut acc = 0.0;
for v in &values {
acc += self.0.density(v).eval_f64()?;
cumulative.push(acc);
points.push(v.eval_f64()?);
}
return Ok(Box::new(move |rng: &mut Rng| {
let u = rng.next_f64() * acc;
let idx = cumulative.partition_point(|c| *c < u);
points[idx.min(points.len().saturating_sub(1))]
}));
}
let Some(iv) = support.as_interval() else {
return Err(not_implemented(format!(
"sampling {}: the support is not a single integer range",
self.name()
)));
};
let (lo, hi) = (&iv.lower, &iv.upper);
if is_neg_inf(lo) || is_pos_inf(hi) {
return Err(not_implemented(format!(
"sampling {}: no sampling route for an infinite discrete support",
self.name()
)));
}
let lo_v = lo.eval_f64()?;
let hi_v = hi.eval_f64()?;
if !(lo_v.is_finite() && hi_v.is_finite() && hi_v >= lo_v) {
return Err(not_implemented(format!(
"sampling {}: the support is not a finite integer range",
self.name()
)));
}
let x = self.fresh_var("x", &[]);
let name = x.to_string();
let pmf = self.0.density(&x).compile(&[name.as_str()])?;
let mut values = Vec::new();
let mut k = lo_v;
while k <= hi_v && values.len() < 1_000_000 {
values.push(k);
k += 1.0;
}
let mut cumulative = Vec::with_capacity(values.len());
let mut acc = 0.0;
for &k in &values {
acc += pmf.call(&[k]);
cumulative.push(acc);
}
let _ = ctx;
Ok(Box::new(move |rng: &mut Rng| {
let u = rng.next_f64() * acc;
let idx = cumulative.partition_point(|c| *c < u);
values[idx.min(values.len().saturating_sub(1))]
}))
}
}
}
}