use rug::Float;
use crate::ball::IntervalEval;
use crate::deriv::{RewriteStep, SideCondition};
use crate::kernel::expr::PredicateKind;
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use super::dists;
use super::quad::{
eval_ball, fl, locate, pi_ball, quadrature, quadrature_at, QuadOutcome, Region, Sample,
VERIFY_PREC,
};
use super::{Distribution, ProbError, Support, UnverifiedReason};
#[derive(Clone, Debug, PartialEq)]
pub struct Evidence {
pub parameter_points: usize,
pub worst_relative_error: f64,
pub precision_bits: u32,
}
pub(crate) const REL_TOL: f64 = 1e-12;
const MAX_FREE: usize = 4;
const MIN_POINTS: usize = 2;
fn required_points(available: usize) -> usize {
available.clamp(1, MIN_POINTS)
}
const MAX_POINTS: usize = 3;
const MAX_DISCRETE_TERMS: u32 = 2000;
pub(crate) fn check(
claim: ExprId,
integrand: ExprId,
x: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
check_with_breaks(claim, integrand, x, dist, &[], pool)
}
pub(crate) fn check_with_breaks(
claim: ExprId,
integrand: ExprId,
x: ExprId,
dist: &Distribution,
breaks: &[ExprId],
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
let points = sample_points(claim, integrand, x, dist, None, &[], pool);
compare_against_definition(&points, claim, integrand, x, dist, breaks, pool)
}
pub(crate) fn check_pair(
claim: ExprId,
integrand: ExprId,
x: ExprId,
p: &Distribution,
q: &Distribution,
extra: &[ExprId],
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
let points = sample_points(claim, integrand, x, p, Some(q), extra, pool);
compare_against_definition(&points, claim, integrand, x, p, &[], pool)
}
#[allow(clippy::too_many_arguments)]
fn compare_against_definition(
points: &[Vec<(ExprId, Float)>],
claim: ExprId,
integrand: ExprId,
x: ExprId,
dist: &Distribution,
breaks: &[ExprId],
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
if points.is_empty() {
return Err(ProbError::Unverified(UnverifiedReason::NoAdmissiblePoint));
}
let required = required_points(points.len());
let integrand = ballable(integrand, pool);
let density = ballable(dists::pdf(dist, x, pool), pool);
let support = dist.support(pool);
let mut compared = 0usize;
let mut worst = 0.0f64;
let mut claim_ever_evaluable = false;
let mut divergences = 0usize;
for binding in points {
let rule = if support.is_discrete() {
ValidatedRule {
region: Region::Real,
density_window: None,
}
} else {
match prepare_rule(density, x, &support, binding, pool) {
Some(r) => r,
None => continue,
}
};
let cuts = evaluate_breaks(breaks, binding, pool);
let outcome = integrate_definition(integrand, x, &support, &rule, &cuts, binding, pool);
let (value, est_err) = match outcome {
QuadOutcome::Value { value, est_err } => (value, est_err),
QuadOutcome::Divergent => {
divergences += 1;
continue;
}
QuadOutcome::Inconclusive => continue,
};
let Some(claim_ball) = eval_ball(claim, binding, pool, VERIFY_PREC) else {
continue;
};
claim_ever_evaluable = true;
let rel = agree(&claim_ball, &value, &est_err)?;
if rel > worst {
worst = rel;
}
compared += 1;
}
if divergences > 0 && compared == 0 {
return Err(ProbError::Divergent(
"the defining integral does not converge: the transformed integrand fails to \
decay at the ends of the integration range at every admissible parameter point"
.to_string(),
));
}
if !claim_ever_evaluable {
return Err(ProbError::Unverified(UnverifiedReason::ClaimNotEvaluable));
}
if compared < required {
return Err(ProbError::Unverified(
UnverifiedReason::QuadratureInconclusive,
));
}
Ok(Evidence {
parameter_points: compared,
worst_relative_error: worst,
precision_bits: VERIFY_PREC,
})
}
pub(crate) fn convergence_probe(
integrand: ExprId,
x: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Option<ProbError> {
let points = sample_points(integrand, integrand, x, dist, None, &[], pool);
if points.is_empty() {
return None;
}
let integrand_b = ballable(integrand, pool);
let density = ballable(dists::pdf(dist, x, pool), pool);
let support = dist.support(pool);
let mut divergent = 0usize;
let mut decided = 0usize;
for binding in &points {
let rule = if support.is_discrete() {
ValidatedRule {
region: Region::Real,
density_window: None,
}
} else {
match prepare_rule(density, x, &support, binding, pool) {
Some(r) => r,
None => continue,
}
};
match integrate_definition(integrand_b, x, &support, &rule, &[], binding, pool) {
QuadOutcome::Divergent => {
divergent += 1;
decided += 1;
}
QuadOutcome::Value { .. } => decided += 1,
QuadOutcome::Inconclusive => {}
}
}
if decided > 0 && divergent == decided {
return Some(ProbError::Divergent(format!(
"E[f(X)] over {} does not converge: the integrand fails to decay at every \
admissible parameter point",
dist.kind().name()
)));
}
None
}
fn fmt(v: &Float) -> String {
format!("{:.17e}", v.to_f64())
}
struct ValidatedRule {
region: Region,
density_window: Option<crate::prob::quad::Window>,
}
fn prepare_rule(
density: ExprId,
x: ExprId,
support: &Support,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> Option<ValidatedRule> {
let region = match support {
Support::Real => Region::Real,
Support::Positive => Region::Positive,
Support::Interval(lo, hi) => Region::Interval(
eval_ball(*lo, binding, pool, VERIFY_PREC)?.mid,
eval_ball(*hi, binding, pool, VERIFY_PREC)?.mid,
),
_ => return None,
};
let window = match region {
Region::Interval(_, _) => None,
Region::Real => {
let mut g = integrand_closure(density, x, binding, pool);
Some(locate(&mut g, VERIFY_PREC, false)?)
}
Region::Positive => {
let mut g = integrand_closure(density, x, binding, pool);
Some(locate(&mut g, VERIFY_PREC, true)?)
}
};
let mut g = integrand_closure(density, x, binding, pool);
let QuadOutcome::Value { value, .. } =
quadrature_at(&mut g, region.clone(), window.as_ref(), VERIFY_PREC)
else {
return None;
};
if rel(&value, &fl(VERIFY_PREC, 1.0)) > NORMALISATION_TOL {
return None;
}
Some(ValidatedRule {
region,
density_window: window,
})
}
const NORMALISATION_TOL: f64 = 1e-10;
fn evaluate_breaks(breaks: &[ExprId], binding: &[(ExprId, Float)], pool: &ExprPool) -> Vec<Float> {
let mut cuts: Vec<Float> = breaks
.iter()
.filter_map(|&b| eval_ball(b, binding, pool, VERIFY_PREC))
.filter(|b| b.mid.is_finite())
.map(|b| b.mid)
.collect();
cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
cuts.dedup();
cuts
}
fn integrate_definition(
integrand: ExprId,
x: ExprId,
support: &Support,
rule: &ValidatedRule,
cuts: &[Float],
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> QuadOutcome {
if support.is_discrete() {
return discrete_sum(integrand, x, support, binding, pool);
}
if !cuts.is_empty() {
return integrate_pieces(integrand, x, &rule.region, cuts, binding, pool);
}
let own = match rule.region {
Region::Interval(_, _) => None,
Region::Real => {
let mut g = integrand_closure(integrand, x, binding, pool);
locate(&mut g, VERIFY_PREC, false)
}
Region::Positive => {
let mut g = integrand_closure(integrand, x, binding, pool);
locate(&mut g, VERIFY_PREC, true)
}
};
let window = match (&rule.density_window, &own) {
(Some(a), Some(b)) => Some(a.union(b, VERIFY_PREC)),
(Some(a), None) => Some(a.clone()),
(None, w) => w.clone(),
};
let mut g = integrand_closure(integrand, x, binding, pool);
quadrature_at(&mut g, rule.region.clone(), window.as_ref(), VERIFY_PREC)
}
fn integrate_pieces(
integrand: ExprId,
x: ExprId,
region: &Region,
cuts: &[Float],
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> QuadOutcome {
let (lo, hi): (Option<Float>, Option<Float>) = match region {
Region::Real => (None, None),
Region::Positive => (Some(fl(VERIFY_PREC, 0.0)), None),
Region::Interval(a, b) => (Some(a.clone()), Some(b.clone())),
};
let inside: Vec<Float> = cuts
.iter()
.filter(|c| lo.as_ref().map_or(true, |l| *c > l) && hi.as_ref().map_or(true, |h| *c < h))
.cloned()
.collect();
let mut edges: Vec<Option<Float>> = Vec::with_capacity(inside.len() + 2);
edges.push(lo);
edges.extend(inside.into_iter().map(Some));
edges.push(hi);
let mut total = fl(VERIFY_PREC, 0.0);
let mut err = fl(VERIFY_PREC, 0.0);
for w in edges.windows(2) {
let outcome = match (&w[0], &w[1]) {
(Some(a), Some(b)) => {
let mut g = integrand_closure(integrand, x, binding, pool);
quadrature(&mut g, Region::Interval(a.clone(), b.clone()), VERIFY_PREC)
}
(Some(a), None) => {
let a = a.clone();
let mut inner = integrand_closure(integrand, x, binding, pool);
let mut g = |y: &Float| inner(&Float::with_val(VERIFY_PREC, &a + y));
quadrature(&mut g, Region::Positive, VERIFY_PREC)
}
(None, Some(b)) => {
let b = b.clone();
let mut inner = integrand_closure(integrand, x, binding, pool);
let mut g = |y: &Float| inner(&Float::with_val(VERIFY_PREC, &b - y));
quadrature(&mut g, Region::Positive, VERIFY_PREC)
}
(None, None) => {
let mut g = integrand_closure(integrand, x, binding, pool);
quadrature(&mut g, Region::Real, VERIFY_PREC)
}
};
match outcome {
QuadOutcome::Value { value, est_err } => {
total += value;
err += est_err;
}
other => return other,
}
}
QuadOutcome::Value {
value: total,
est_err: err,
}
}
fn discrete_sum(
integrand: ExprId,
k: ExprId,
support: &Support,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> QuadOutcome {
let upper = match support {
Support::IntegersUpTo(n) => Some(*n),
Support::NonNegativeIntegers => None,
_ => unreachable!("only discrete supports reach here"),
};
let limit = upper.unwrap_or(MAX_DISCRETE_TERMS);
if limit > MAX_DISCRETE_TERMS {
return QuadOutcome::Inconclusive;
}
let mut env: Vec<(ExprId, Float)> = binding.to_vec();
env.push((k, fl(VERIFY_PREC, 0.0)));
let last = env.len() - 1;
let mut sum = fl(VERIFY_PREC, 0.0);
let mut max_term = fl(VERIFY_PREC, 0.0);
let mut small_run = 0u32;
let mut last_term = fl(VERIFY_PREC, 0.0);
for i in 0..=limit {
env[last].1 = fl(VERIFY_PREC, f64::from(i));
let Some(b) = eval_ball(integrand, &env, pool, VERIFY_PREC) else {
return QuadOutcome::Inconclusive;
};
if !b.mid.is_finite() {
return QuadOutcome::Divergent;
}
let mut mag = b.mid.clone();
mag.abs_mut();
if mag > max_term {
max_term = mag.clone();
}
sum += &b.mid;
let tiny = Float::with_val(VERIFY_PREC, &max_term * fl(VERIFY_PREC, 1e-40));
if mag < tiny && mag <= last_term {
small_run += 1;
} else {
small_run = 0;
}
last_term = mag;
if upper.is_none() && small_run >= 8 {
return QuadOutcome::Value {
value: sum,
est_err: Float::with_val(VERIFY_PREC, last_term.clone() * 2u32),
};
}
}
match upper {
Some(_) => QuadOutcome::Value {
value: sum,
est_err: fl(VERIFY_PREC, 0.0),
},
None => QuadOutcome::Inconclusive,
}
}
fn ballable(expr: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Func { name, args } if args.len() == 2 && (name == "max" || name == "min") => {
let a = ballable(args[0], pool);
let b = ballable(args[1], pool);
let (first, second) = if name == "max" { (a, b) } else { (b, a) };
let cond = pool.pred_ge(first, second);
pool.piecewise(vec![(cond, first)], second)
}
ExprData::Add(args) => pool.add(args.iter().map(|&a| ballable(a, pool)).collect()),
ExprData::Mul(args) => pool.mul(args.iter().map(|&a| ballable(a, pool)).collect()),
ExprData::Pow { base, exp } => pool.pow(ballable(base, pool), ballable(exp, pool)),
ExprData::Func { name, args } => {
pool.func(name, args.iter().map(|&a| ballable(a, pool)).collect())
}
_ => expr,
}
}
fn role_rows(dist: &Distribution) -> &'static [&'static [f64]] {
use super::DistKind::*;
match dist.kind() {
Normal => &[&[0.0, 1.0], &[2.0, 0.5], &[-1.5, 2.0], &[0.25, 3.0]],
LogNormal => &[&[0.0, 0.3], &[0.5, 1.0], &[-1.0, 0.75], &[0.2, 0.6]],
Uniform => &[&[0.0, 1.0], &[-2.0, 3.0], &[1.5, 4.25], &[-0.5, 0.25]],
Exponential | Poisson => &[&[1.0], &[0.4], &[2.5], &[1.75]],
Gamma => &[&[2.0, 1.0], &[0.5, 2.0], &[3.5, 0.7], &[1.0, 3.0]],
Beta => &[&[2.0, 3.0], &[0.5, 0.5], &[1.0, 4.0], &[3.2, 1.4]],
Bernoulli => &[&[0.3], &[0.5], &[0.8], &[0.125]],
Binomial => &[&[0.0, 0.3], &[0.0, 0.5], &[0.0, 0.8], &[0.0, 0.125]],
}
}
const EXTRA_ROWS: [f64; 4] = [0.6, 1.7, 1.2, 0.35];
fn sample_points(
claim: ExprId,
integrand: ExprId,
x: ExprId,
dist: &Distribution,
other: Option<&Distribution>,
extra: &[ExprId],
pool: &ExprPool,
) -> Vec<Vec<(ExprId, Float)>> {
use crate::eval::symbols::collect_free_symbols;
let mut free: Vec<ExprId> = Vec::new();
collect_free_symbols(claim, pool, &mut free);
collect_free_symbols(integrand, pool, &mut free);
for &p in dist.params() {
collect_free_symbols(p, pool, &mut free);
}
if let Some(o) = other {
for &p in o.params() {
collect_free_symbols(p, pool, &mut free);
}
}
free.retain(|&s| s != x && !is_infinity(s, pool));
if free.len() > MAX_FREE {
return Vec::new();
}
let mut role: Vec<Option<usize>> = vec![None; free.len()];
let mut co_role: Vec<Option<usize>> = vec![None; free.len()];
for (slot, &p) in dist.params().iter().enumerate() {
if matches!(pool.get(p), ExprData::Symbol { .. }) {
if let Some(i) = free.iter().position(|&s| s == p) {
role[i] = Some(slot);
}
}
}
if let Some(o) = other {
for (slot, &p) in o.params().iter().enumerate() {
if matches!(pool.get(p), ExprData::Symbol { .. }) {
if let Some(i) = free.iter().position(|&s| s == p) {
if role[i].is_none() {
co_role[i] = Some(slot);
}
}
}
}
}
let rows = role_rows(dist);
let co_rows = other.map(role_rows);
let mut constraints = dist.constraints(pool);
constraints.extend_from_slice(extra);
if free.is_empty() {
return if admissible(&constraints, &[], pool) {
vec![Vec::new()]
} else {
Vec::new()
};
}
let mut out = Vec::new();
for (r, c) in candidate_rows(rows.len(), co_rows.map(<[&[f64]]>::len)) {
let mut binding: Vec<(ExprId, Float)> = Vec::with_capacity(free.len());
for (i, &sym) in free.iter().enumerate() {
let v = match (role[i], co_role[i]) {
(Some(slot), _) => rows
.get(r)
.and_then(|row| row.get(slot))
.copied()
.unwrap_or(EXTRA_ROWS[r % EXTRA_ROWS.len()]),
(None, Some(slot)) => co_rows
.and_then(|cr| cr.get(c))
.and_then(|row| row.get(slot))
.copied()
.unwrap_or(EXTRA_ROWS[c % EXTRA_ROWS.len()]),
(None, None) => EXTRA_ROWS[(r + i) % EXTRA_ROWS.len()],
};
binding.push((sym, fl(VERIFY_PREC, v)));
}
if admissible(&constraints, &binding, pool) {
out.push(binding);
}
if out.len() == MAX_POINTS {
break;
}
}
out
}
fn candidate_rows(rows: usize, co_rows: Option<usize>) -> Vec<(usize, usize)> {
match co_rows {
None => (0..EXTRA_ROWS.len().max(rows)).map(|r| (r, 0)).collect(),
Some(cr) => (0..rows)
.flat_map(|r| (0..cr).map(move |c| (r, c)))
.filter(|(r, c)| r != c)
.collect(),
}
}
fn is_infinity(sym: ExprId, pool: &ExprPool) -> bool {
sym == pool.pos_infinity()
}
fn admissible(constraints: &[ExprId], binding: &[(ExprId, Float)], pool: &ExprPool) -> bool {
constraints.iter().all(|&c| {
eval_ball(c, binding, pool, VERIFY_PREC)
.map(|b| b.mid > 0.5)
.unwrap_or(false)
})
}
pub(crate) fn definition_integrand(
f: ExprId,
x: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> ExprId {
pool.mul(vec![f, dists::pdf(dist, x, pool)])
}
pub(crate) fn evidence_step(
evidence: &Evidence,
claim: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> RewriteStep {
let mut conditions = Vec::new();
for c in dist.constraints(pool) {
if let Some(side) = undischarged(c, pool) {
conditions.push(side);
}
}
let _ = evidence;
RewriteStep::with_conditions("prob_verified_against_quadrature", claim, claim, conditions)
}
pub(crate) fn undischarged(pred: ExprId, pool: &ExprPool) -> Option<SideCondition> {
let ExprData::Predicate { kind, args } = pool.get(pred) else {
return None;
};
let [lhs, rhs] = args.as_slice() else {
return None;
};
let (lhs, rhs) = (*lhs, *rhs);
let gap = crate::simplify::simplify(
match kind {
PredicateKind::Gt | PredicateKind::Ge => super::sub(lhs, rhs, pool),
PredicateKind::Lt | PredicateKind::Le => super::sub(rhs, lhs, pool),
_ => return None,
},
pool,
)
.value;
if !matches!(dists::sign_of(gap, pool), dists::Sign::Unknown) {
return None;
}
match kind {
PredicateKind::Gt | PredicateKind::Lt => Some(SideCondition::Positive(gap)),
_ => Some(SideCondition::InDomain(gap, Domain::NonNegative)),
}
}
const CDF_OFFSETS: [f64; 5] = [-2.0, -0.7, 0.0, 0.8, 2.5];
const CDF_FRACTIONS: [f64; 5] = [0.05, 0.3, 0.5, 0.75, 0.95];
const QUANTILE_LADDER: [f64; 4] = [0.25, 0.5, 0.75, 0.9];
pub(crate) fn check_cdf(
claim: ExprId,
x: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
let density = dists::pdf(dist, x, pool);
let points = sample_points(claim, density, x, dist, None, &[], pool);
if points.is_empty() {
return Err(ProbError::Unverified(UnverifiedReason::NoAdmissiblePoint));
}
let support = dist.support(pool);
if support.is_discrete() {
return Err(ProbError::Unsupported(
"a discrete CDF has no symbolic form here to check".to_string(),
));
}
let density = ballable(density, pool);
let mut compared = 0usize;
let mut worst = 0.0f64;
let mut claim_ever_evaluable = false;
for binding in &points {
let Some(abscissae) = cdf_abscissae(density, x, &support, binding, pool) else {
continue;
};
let mut levels = Vec::with_capacity(abscissae.len());
let mut ok = true;
for t in &abscissae {
match eval_at(claim, x, t, binding, pool) {
Some(v) => levels.push(v),
None => {
ok = false;
break;
}
}
}
if !ok {
continue;
}
claim_ever_evaluable = true;
let QuadOutcome::Value { value: tail, .. } =
tail_below(density, x, &support, &abscissae[0], binding, pool)
else {
continue;
};
compare(&levels[0], &tail, "CDF level at the left sample")?;
worst = worst.max(rel(&levels[0], &tail));
compared += 1;
for i in 0..abscissae.len() - 1 {
let mut g = integrand_closure(density, x, binding, pool);
let QuadOutcome::Value { value: mass, .. } = quadrature(
&mut g,
Region::Interval(abscissae[i].clone(), abscissae[i + 1].clone()),
VERIFY_PREC,
) else {
continue;
};
let step = Float::with_val(VERIFY_PREC, &levels[i + 1] - &levels[i]);
compare(&step, &mass, "CDF increment")?;
worst = worst.max(rel(&step, &mass));
compared += 1;
}
}
finish(
compared,
worst,
claim_ever_evaluable,
required_points(points.len()),
)
}
pub(crate) fn check_quantile(
claim: ExprId,
p_arg: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
let x = dists::fresh_var(&[claim, p_arg], pool);
let density = dists::pdf(dist, x, pool);
let points = sample_points(claim, density, x, dist, None, &[], pool);
if points.is_empty() {
return Err(ProbError::Unverified(UnverifiedReason::NoAdmissiblePoint));
}
let support = dist.support(pool);
if support.is_discrete() {
return Err(ProbError::Unsupported(
"a discrete quantile has no symbolic form here to check".to_string(),
));
}
let density = ballable(density, pool);
let p_sym = match pool.get(p_arg) {
ExprData::Symbol { .. } => Some(p_arg),
_ => None,
};
let mut compared = 0usize;
let mut worst = 0.0f64;
let mut claim_ever_evaluable = false;
for (i, binding) in points.iter().enumerate() {
for (j, &pv) in QUANTILE_LADDER.iter().enumerate() {
if (i + j) % 2 == 1 {
continue; }
let mut env = binding.clone();
let target = match p_sym {
Some(sym) => {
match env.iter_mut().find(|(s, _)| *s == sym) {
Some(slot) => slot.1 = fl(VERIFY_PREC, pv),
None => env.push((sym, fl(VERIFY_PREC, pv))),
}
fl(VERIFY_PREC, pv)
}
None => match eval_ball(p_arg, &env, pool, VERIFY_PREC) {
Some(b) if b.mid > 0 && b.mid < 1 => b.mid,
_ => continue,
},
};
let Some(q) = eval_ball(claim, &env, pool, VERIFY_PREC) else {
continue;
};
claim_ever_evaluable = true;
let QuadOutcome::Value { value: mass, .. } =
tail_below(density, x, &support, &q.mid, &env, pool)
else {
continue;
};
compare(&mass, &target, "quantile round-trip F(Q(p)) = p")?;
worst = worst.max(rel(&mass, &target));
compared += 1;
if p_sym.is_none() {
break;
}
}
}
finish(
compared,
worst,
claim_ever_evaluable,
required_points(points.len()),
)
}
fn finish(
compared: usize,
worst: f64,
claim_ever_evaluable: bool,
minimum: usize,
) -> Result<Evidence, ProbError> {
if !claim_ever_evaluable {
return Err(ProbError::Unverified(UnverifiedReason::ClaimNotEvaluable));
}
if compared < minimum {
return Err(ProbError::Unverified(
UnverifiedReason::QuadratureInconclusive,
));
}
Ok(Evidence {
parameter_points: compared,
worst_relative_error: worst,
precision_bits: VERIFY_PREC,
})
}
fn rel(a: &Float, b: &Float) -> f64 {
let mut scale = b.clone();
scale.abs_mut();
if scale < 1 {
scale = fl(VERIFY_PREC, 1.0);
}
Float::with_val(
VERIFY_PREC,
Float::with_val(VERIFY_PREC, a - b).abs() / scale,
)
.to_f64()
}
fn agree(claim: &crate::ball::ArbBall, value: &Float, est_err: &Float) -> Result<f64, ProbError> {
let diff = Float::with_val(VERIFY_PREC, &claim.mid - value).abs();
let scale = {
let mut s = value.clone();
s.abs_mut();
if s < 1 {
s = fl(VERIFY_PREC, 1.0);
}
s
};
let allowed = {
let base = Float::with_val(VERIFY_PREC, &scale * fl(VERIFY_PREC, REL_TOL));
let slack = Float::with_val(
VERIFY_PREC,
Float::with_val(VERIFY_PREC, est_err + &claim.rad) * 16u32,
);
if slack > base {
slack
} else {
base
}
};
if diff > allowed {
return Err(ProbError::Unverified(UnverifiedReason::Disagreement(
format!(
"closed form {} vs quadrature {} (allowed {})",
fmt(&claim.mid),
fmt(value),
fmt(&allowed)
),
)));
}
Ok(Float::with_val(VERIFY_PREC, &diff / &scale).to_f64())
}
fn compare(claim: &Float, truth: &Float, what: &str) -> Result<(), ProbError> {
if rel(claim, truth) > REL_TOL {
return Err(ProbError::Unverified(UnverifiedReason::Disagreement(
format!(
"{what}: closed form {} vs quadrature {}",
fmt(claim),
fmt(truth)
),
)));
}
Ok(())
}
fn eval_at(
expr: ExprId,
x: ExprId,
at: &Float,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> Option<Float> {
let mut env = binding.to_vec();
env.push((x, at.clone()));
eval_ball(expr, &env, pool, VERIFY_PREC).map(|b| b.mid)
}
fn integrand_closure<'a>(
density: ExprId,
x: ExprId,
binding: &'a [(ExprId, Float)],
pool: &'a ExprPool,
) -> impl FnMut(&Float) -> Sample + 'a {
let (folded, consts) = fold_constants(density, x, binding, pool);
let mut ev = IntervalEval::new(VERIFY_PREC);
ev.bind(super::pi(pool), pi_ball(VERIFY_PREC));
for (sym, val) in binding {
ev.bind(*sym, point_ball(val));
}
for (sym, ball) in consts {
ev.bind(sym, ball);
}
move |v: &Float| {
ev.bind(x, point_ball(v));
match ev.eval(folded, pool) {
Some(b) if b.mid.is_finite() && b.rad.is_finite() => Sample::Value(b.mid),
_ => Sample::Unevaluable,
}
}
}
fn point_ball(v: &Float) -> crate::ball::ArbBall {
crate::ball::ArbBall {
mid: v.clone(),
rad: fl(VERIFY_PREC, 0.0),
prec: VERIFY_PREC,
}
}
fn fold_constants(
expr: ExprId,
x: ExprId,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> (ExprId, Vec<(ExprId, crate::ball::ArbBall)>) {
let mut consts = Vec::new();
let folded = fold_walk(expr, x, binding, pool, &mut consts, 0);
(folded, consts)
}
fn fold_walk(
expr: ExprId,
x: ExprId,
binding: &[(ExprId, Float)],
pool: &ExprPool,
consts: &mut Vec<(ExprId, crate::ball::ArbBall)>,
depth: u32,
) -> ExprId {
if depth > 32 {
return expr;
}
if !dists::contains(expr, x, pool) {
if matches!(
pool.get(expr),
ExprData::Integer(_)
| ExprData::Rational(_)
| ExprData::Float(_)
| ExprData::Symbol { .. }
) {
return expr;
}
if let Some(ball) = eval_ball(expr, binding, pool, VERIFY_PREC) {
let sym = pool.symbol(format!("_prob_k{}", consts.len()), Domain::Real);
consts.push((sym, ball));
return sym;
}
return expr;
}
match pool.get(expr) {
ExprData::Add(args) => pool.add(
args.iter()
.map(|&a| fold_walk(a, x, binding, pool, consts, depth + 1))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&a| fold_walk(a, x, binding, pool, consts, depth + 1))
.collect(),
),
ExprData::Pow { base, exp } => pool.pow(
fold_walk(base, x, binding, pool, consts, depth + 1),
fold_walk(exp, x, binding, pool, consts, depth + 1),
),
ExprData::Func { name, args } => pool.func(
name,
args.iter()
.map(|&a| fold_walk(a, x, binding, pool, consts, depth + 1))
.collect(),
),
ExprData::Piecewise { branches, default } => pool.piecewise(
branches
.iter()
.map(|(c, v)| {
(
fold_walk(*c, x, binding, pool, consts, depth + 1),
fold_walk(*v, x, binding, pool, consts, depth + 1),
)
})
.collect(),
fold_walk(default, x, binding, pool, consts, depth + 1),
),
ExprData::Predicate { kind, args } => pool.predicate(
kind,
args.iter()
.map(|&a| fold_walk(a, x, binding, pool, consts, depth + 1))
.collect(),
),
_ => expr,
}
}
fn cdf_abscissae(
density: ExprId,
x: ExprId,
support: &Support,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> Option<Vec<Float>> {
match support {
Support::Interval(lo, hi) => {
let lo = eval_ball(*lo, binding, pool, VERIFY_PREC)?.mid;
let hi = eval_ball(*hi, binding, pool, VERIFY_PREC)?.mid;
let width = Float::with_val(VERIFY_PREC, &hi - &lo);
Some(
CDF_FRACTIONS
.iter()
.map(|&f| Float::with_val(VERIFY_PREC, &lo + &width * fl(VERIFY_PREC, f)))
.collect(),
)
}
Support::Real => {
let mut g = integrand_closure(density, x, binding, pool);
let w = locate(&mut g, VERIFY_PREC, false)?;
Some(
CDF_OFFSETS
.iter()
.map(|&o| {
Float::with_val(VERIFY_PREC, &w.centre + &w.scale * fl(VERIFY_PREC, o))
})
.collect(),
)
}
Support::Positive => {
let mut g = integrand_closure(density, x, binding, pool);
let w = locate(&mut g, VERIFY_PREC, true)?;
Some(
CDF_OFFSETS
.iter()
.map(|&o| {
Float::with_val(
VERIFY_PREC,
Float::with_val(VERIFY_PREC, &w.centre + &w.scale * fl(VERIFY_PREC, o))
.exp(),
)
})
.collect(),
)
}
_ => None,
}
}
fn tail_below(
density: ExprId,
x: ExprId,
support: &Support,
t: &Float,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> QuadOutcome {
match support {
Support::Real => {
let t = t.clone();
let mut inner = integrand_closure(density, x, binding, pool);
let mut g = |y: &Float| inner(&Float::with_val(VERIFY_PREC, &t - y));
quadrature(&mut g, Region::Positive, VERIFY_PREC)
}
Support::Positive => {
let mut g = integrand_closure(density, x, binding, pool);
quadrature(
&mut g,
Region::Interval(fl(VERIFY_PREC, 0.0), t.clone()),
VERIFY_PREC,
)
}
Support::Interval(lo, _) => {
let Some(lo) = eval_ball(*lo, binding, pool, VERIFY_PREC) else {
return QuadOutcome::Inconclusive;
};
let mut g = integrand_closure(density, x, binding, pool);
quadrature(&mut g, Region::Interval(lo.mid, t.clone()), VERIFY_PREC)
}
_ => QuadOutcome::Inconclusive,
}
}
const T_LADDER: [f64; 3] = [0.4, -0.9, 1.3];
pub(crate) fn check_characteristic(
claim: ExprId,
t: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Result<(Evidence, bool), ProbError> {
use super::cplx::eval_complex;
let x = dists::fresh_var(&[claim, t], pool);
let density_expr = dists::pdf(dist, x, pool);
let points = sample_points(claim, density_expr, x, dist, None, &[], pool);
if points.is_empty() {
return Err(ProbError::Unverified(UnverifiedReason::NoAdmissiblePoint));
}
let support = dist.support(pool);
let density = ballable(density_expr, pool);
let fourier_phi = fourier_route(dist, x, t, pool);
let t_sym = match pool.get(t) {
ExprData::Symbol { .. } => Some(t),
_ => None,
};
let tx = pool.mul(vec![t, x]);
let cos_part = ballable(
pool.mul(vec![density_expr, pool.func("cos", vec![tx])]),
pool,
);
let sin_part = ballable(
pool.mul(vec![density_expr, pool.func("sin", vec![tx])]),
pool,
);
let mut compared = 0usize;
let mut worst = 0.0f64;
let mut claim_ever_evaluable = false;
let mut fourier_agreed = false;
for (i, binding) in points.iter().enumerate() {
let rule = if support.is_discrete() {
ValidatedRule {
region: Region::Real,
density_window: None,
}
} else {
match prepare_rule(density, x, &support, binding, pool) {
Some(r) => r,
None => continue,
}
};
for (j, &tv) in T_LADDER.iter().enumerate() {
if (i + j) % 2 == 1 {
continue; }
let mut env = binding.clone();
match t_sym {
Some(sym) => match env.iter_mut().find(|(s, _)| *s == sym) {
Some(slot) => slot.1 = fl(VERIFY_PREC, tv),
None => env.push((sym, fl(VERIFY_PREC, tv))),
},
None => {
if j > 0 {
continue;
}
}
}
let Some(value) = eval_complex(claim, &env, pool, VERIFY_PREC) else {
continue;
};
claim_ever_evaluable = true;
let (re, im, tail) = if support.is_discrete() {
let (QuadOutcome::Value { value: re, .. }, QuadOutcome::Value { value: im, .. }) = (
integrate_definition(cos_part, x, &support, &rule, &[], &env, pool),
integrate_definition(sin_part, x, &support, &rule, &[], &env, pool),
) else {
continue;
};
(re, im, fl(VERIFY_PREC, 0.0))
} else {
let Some((core, tail)) = oscillatory_core(density, x, &rule, &env, pool) else {
continue;
};
let Some(re) = integrate_core(cos_part, x, &core, &env, pool) else {
continue;
};
let Some(im) = integrate_core(sin_part, x, &core, &env, pool) else {
continue;
};
(re, im, tail)
};
compare_within(&value.re, &re, &tail, "Re φ(t) against ∫p(x)cos(tx)dx")?;
compare_within(&value.im, &im, &tail, "Im φ(t) against ∫p(x)sin(tx)dx")?;
worst = worst.max(rel(&value.re, &re)).max(rel(&value.im, &im));
compared += 2;
if let Some(phi_f) = fourier_phi {
if let Some(vf) = eval_complex(phi_f, &env, pool, VERIFY_PREC) {
compare(&value.re, &vf.re, "Re φ(t) against F{p}(-t/2π)")?;
compare(&value.im, &vf.im, "Im φ(t) against F{p}(-t/2π)")?;
fourier_agreed = true;
compared += 2;
}
}
}
}
let evidence = finish(
compared,
worst,
claim_ever_evaluable,
2 * required_points(points.len()),
)?;
Ok((evidence, fourier_agreed))
}
fn fourier_route(dist: &Distribution, x: ExprId, t: ExprId, pool: &ExprPool) -> Option<ExprId> {
use std::collections::HashMap;
let p = super::charfun::fourier_ready_density(dist, x, pool)?;
let xi = dists::fresh_var(&[p, t, x], pool);
let transformed = crate::transform::fourier_transform(p, x, xi, pool).ok()?;
let two_pi = pool.mul(vec![pool.integer(2), super::pi(pool)]);
let sub_xi = super::div(pool.mul(vec![pool.integer(-1), t]), two_pi, pool);
let mut m = HashMap::new();
m.insert(xi, sub_xi);
Some(crate::kernel::subs(transformed, &m, pool))
}
const CORE_WIDENINGS: u32 = 8;
const CORE_TAIL_TOL: f64 = 1e-22;
fn oscillatory_core(
density: ExprId,
x: ExprId,
rule: &ValidatedRule,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> Option<(Region, Float)> {
if let Region::Interval(a, b) = &rule.region {
return Some((Region::Interval(a.clone(), b.clone()), fl(VERIFY_PREC, 0.0)));
}
let w = rule.density_window.as_ref()?;
let log_scale = matches!(rule.region, Region::Positive);
let mut reach = fl(VERIFY_PREC, 1.0);
for _ in 0..CORE_WIDENINGS {
let span = Float::with_val(VERIFY_PREC, &w.scale * &reach);
let (a, b) = if log_scale {
(
fl(VERIFY_PREC, 0.0),
Float::with_val(
VERIFY_PREC,
Float::with_val(VERIFY_PREC, &w.centre + &span).exp(),
),
)
} else {
(
Float::with_val(VERIFY_PREC, &w.centre - &span),
Float::with_val(VERIFY_PREC, &w.centre + &span),
)
};
if a.is_finite() && b.is_finite() && a < b {
let region = Region::Interval(a, b);
let mut g = integrand_closure(density, x, binding, pool);
if let QuadOutcome::Value { value, .. } =
quadrature(&mut g, region.clone(), VERIFY_PREC)
{
let tail = Float::with_val(VERIFY_PREC, fl(VERIFY_PREC, 1.0) - &value).abs();
if tail <= fl(VERIFY_PREC, CORE_TAIL_TOL) {
return Some((region, tail));
}
}
}
reach *= 2u32;
}
None
}
fn integrate_core(
integrand: ExprId,
x: ExprId,
core: &Region,
binding: &[(ExprId, Float)],
pool: &ExprPool,
) -> Option<Float> {
let mut g = integrand_closure(integrand, x, binding, pool);
match quadrature(&mut g, core.clone(), VERIFY_PREC) {
QuadOutcome::Value { value, .. } => Some(value),
_ => None,
}
}
fn compare_within(
claim: &Float,
truth: &Float,
slack: &Float,
what: &str,
) -> Result<(), ProbError> {
let diff = Float::with_val(VERIFY_PREC, claim - truth).abs();
let allowed = {
let base = fl(VERIFY_PREC, REL_TOL);
let s = Float::with_val(VERIFY_PREC, slack * 4u32);
if s > base {
s
} else {
base
}
};
if diff > allowed {
return Err(ProbError::Unverified(UnverifiedReason::Disagreement(
format!(
"{what}: closed form {} vs quadrature {}",
fmt(claim),
fmt(truth)
),
)));
}
Ok(())
}
pub(crate) fn check_generating(
claim: ExprId,
integrand: ExprId,
x: ExprId,
arg: ExprId,
ladder: &[ExprId],
dist: &Distribution,
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
let points = sample_points(claim, integrand, x, dist, None, &[], pool);
if points.is_empty() {
return Err(ProbError::Unverified(UnverifiedReason::NoAdmissiblePoint));
}
let arg_sym = match pool.get(arg) {
ExprData::Symbol { .. } => Some(arg),
_ => None,
};
let per_point = match arg_sym {
Some(_) => ladder.len().min(2),
None => 1,
};
if per_point == 0 {
return Err(ProbError::Unverified(UnverifiedReason::NoAdmissiblePoint));
}
let integrand_b = ballable(integrand, pool);
let density = ballable(dists::pdf(dist, x, pool), pool);
let support = dist.support(pool);
let mut compared = 0usize;
let mut worst = 0.0f64;
let mut claim_ever_evaluable = false;
let mut divergences = 0usize;
for binding in &points {
let rule = if support.is_discrete() {
ValidatedRule {
region: Region::Real,
density_window: None,
}
} else {
match prepare_rule(density, x, &support, binding, pool) {
Some(r) => r,
None => continue,
}
};
for env in argument_envs(arg_sym, ladder, binding, per_point, pool) {
let outcome = integrate_definition(integrand_b, x, &support, &rule, &[], &env, pool);
let (value, est_err) = match outcome {
QuadOutcome::Value { value, est_err } => (value, est_err),
QuadOutcome::Divergent => {
divergences += 1;
continue;
}
QuadOutcome::Inconclusive => continue,
};
let Some(claim_ball) = eval_ball(claim, &env, pool, VERIFY_PREC) else {
continue;
};
claim_ever_evaluable = true;
let rel = agree(&claim_ball, &value, &est_err)?;
if rel > worst {
worst = rel;
}
compared += 1;
}
}
if divergences > 0 && compared == 0 {
return Err(ProbError::Divergent(
"E[g(X)] does not converge at any argument this gate could check: the integrand \
fails to decay at the ends of the support"
.to_string(),
));
}
finish(
compared,
worst,
claim_ever_evaluable,
per_point * required_points(points.len()),
)
}
fn argument_envs(
arg_sym: Option<ExprId>,
ladder: &[ExprId],
binding: &[(ExprId, Float)],
per_point: usize,
pool: &ExprPool,
) -> Vec<Vec<(ExprId, Float)>> {
let Some(sym) = arg_sym else {
return vec![binding.to_vec()];
};
let mut out = Vec::with_capacity(per_point);
for &l in ladder {
let Some(v) = eval_ball(l, binding, pool, VERIFY_PREC) else {
continue;
};
if !v.mid.is_finite() {
continue;
}
let mut env = binding.to_vec();
match env.iter_mut().find(|(s, _)| *s == sym) {
Some(slot) => slot.1 = v.mid,
None => env.push((sym, v.mid)),
}
out.push(env);
if out.len() == per_point {
break;
}
}
out
}
pub(crate) type CentralTerm = (i32, &'static [(u32, u32)]);
pub(crate) fn check_cumulant(
claim: ExprId,
terms: &[CentralTerm],
mean: ExprId,
x: ExprId,
dist: &Distribution,
pool: &ExprPool,
) -> Result<Evidence, ProbError> {
let density_expr = dists::pdf(dist, x, pool);
let points = sample_points(claim, mean, x, dist, None, &[], pool);
if points.is_empty() {
return Err(ProbError::Unverified(UnverifiedReason::NoAdmissiblePoint));
}
let support = dist.support(pool);
let density = ballable(density_expr, pool);
let mut orders: Vec<u32> = terms
.iter()
.flat_map(|(_, f)| f.iter().map(|&(k, _)| k))
.collect();
orders.sort_unstable();
orders.dedup();
let centred: Vec<(u32, ExprId)> = orders
.iter()
.map(|&k| {
let dev = super::sub(x, mean, pool);
let power = pool.pow(dev, pool.integer(k));
(k, ballable(pool.mul(vec![power, density_expr]), pool))
})
.collect();
let mut compared = 0usize;
let mut worst = 0.0f64;
let mut claim_ever_evaluable = false;
'points: for binding in &points {
let rule = if support.is_discrete() {
ValidatedRule {
region: Region::Real,
density_window: None,
}
} else {
match prepare_rule(density, x, &support, binding, pool) {
Some(r) => r,
None => continue,
}
};
let mut mu: Vec<(u32, Float, Float)> = Vec::with_capacity(centred.len());
for &(k, integrand) in ¢red {
match integrate_definition(integrand, x, &support, &rule, &[], binding, pool) {
QuadOutcome::Value { value, est_err } => mu.push((k, value, est_err)),
_ => continue 'points,
}
}
let mut truth = fl(VERIFY_PREC, 0.0);
let mut err = fl(VERIFY_PREC, 0.0);
for (coeff, factors) in terms {
let mut term = fl(VERIFY_PREC, f64::from(*coeff));
let mut rel_err = fl(VERIFY_PREC, 0.0);
for &(k, power) in factors.iter() {
let (_, v, e) = mu
.iter()
.find(|(kk, _, _)| *kk == k)
.expect("every order was quadratured above");
for _ in 0..power {
term = Float::with_val(VERIFY_PREC, &term * v);
}
let mut scale = v.clone();
scale.abs_mut();
if scale > 0 {
rel_err += Float::with_val(VERIFY_PREC, e / &scale) * power;
}
}
let mut mag = term.clone();
mag.abs_mut();
err += Float::with_val(VERIFY_PREC, mag * rel_err);
truth += term;
}
let Some(claim_ball) = eval_ball(claim, binding, pool, VERIFY_PREC) else {
continue;
};
claim_ever_evaluable = true;
let rel = agree(&claim_ball, &truth, &err)?;
if rel > worst {
worst = rel;
}
compared += 1;
}
finish(
compared,
worst,
claim_ever_evaluable,
required_points(points.len()),
)
}