use crate::kernel::{ExprData, ExprId, ExprPool};
use crate::primitive::PrimitiveRegistry;
use crate::simplify::engine::simplify;
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Strength {
Sampled,
Enclosure,
Symbolic,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeclineReason {
Differentiation,
NotEnoughPoints {
found: usize,
required: usize,
},
EnclosureUnavailable {
what: String,
},
BelowRequiredStrength {
reached: Strength,
required: Strength,
},
}
impl std::fmt::Display for DeclineReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeclineReason::Differentiation => {
write!(f, "the candidate could not be differentiated")
}
DeclineReason::NotEnoughPoints { found, required } => write!(
f,
"only {found} in-domain sample points were evaluable, {required} required"
),
DeclineReason::EnclosureUnavailable { what } => {
write!(f, "no rigorous enclosure could be established: {what}")
}
DeclineReason::BelowRequiredStrength { reached, required } => write!(
f,
"verification reached {reached:?} but {required:?} was required"
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VerifiedBox {
pub lo: f64,
pub hi: f64,
pub residual_bound: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Verdict {
Proven,
EnclosureVerified {
boxes: Vec<VerifiedBox>,
residual_bound: f64,
points: usize,
tolerance: f64,
},
SampledOnly {
points: usize,
tolerance: f64,
},
Failed {
at: f64,
residual: f64,
tolerance: f64,
},
Declined {
reason: DeclineReason,
},
}
impl Verdict {
pub fn strength(&self) -> Option<Strength> {
match self {
Verdict::Proven => Some(Strength::Symbolic),
Verdict::EnclosureVerified { .. } => Some(Strength::Enclosure),
Verdict::SampledOnly { .. } => Some(Strength::Sampled),
Verdict::Failed { .. } | Verdict::Declined { .. } => None,
}
}
pub fn meets(&self, min: Strength) -> bool {
self.strength().is_some_and(|s| s >= min)
}
pub fn is_verified(&self) -> bool {
self.strength().is_some()
}
}
pub struct Domain<'a> {
samples: Vec<f64>,
predicate: Option<Box<dyn Fn(f64) -> bool + 'a>>,
boxes: Vec<(f64, f64)>,
}
impl std::fmt::Debug for Domain<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Domain")
.field("samples", &self.samples.len())
.field("predicate", &self.predicate.is_some())
.field("boxes", &self.boxes)
.finish()
}
}
impl<'a> Domain<'a> {
pub fn from_samples(samples: Vec<f64>) -> Self {
Domain {
samples,
predicate: None,
boxes: Vec::new(),
}
}
pub fn with_predicate(mut self, p: impl Fn(f64) -> bool + 'a) -> Self {
self.predicate = Some(Box::new(p));
self
}
pub fn with_boxes(mut self, boxes: Vec<(f64, f64)>) -> Self {
self.boxes = boxes
.into_iter()
.filter(|(lo, hi)| lo.is_finite() && hi.is_finite() && hi > lo)
.collect();
self
}
pub fn contains(&self, x: f64) -> bool {
x.is_finite() && self.predicate.as_ref().map_or(true, |p| p(x))
}
pub fn samples(&self) -> &[f64] {
&self.samples
}
pub fn boxes(&self) -> &[(f64, f64)] {
&self.boxes
}
}
#[derive(Clone, Copy, Debug)]
pub struct EnclosureBudget {
pub order: usize,
pub prec: u32,
pub tol: f64,
pub max_subdivisions: usize,
pub max_boxes: usize,
}
impl Default for EnclosureBudget {
fn default() -> Self {
EnclosureBudget {
order: 6,
prec: 128,
tol: 1e-7,
max_subdivisions: 64,
max_boxes: 4,
}
}
}
impl EnclosureBudget {
pub fn cheap() -> Self {
EnclosureBudget {
order: 6,
prec: 96,
tol: 1e-7,
max_subdivisions: 16,
max_boxes: 2,
}
}
pub fn thorough() -> Self {
EnclosureBudget {
order: 8,
prec: 128,
tol: 1e-8,
max_subdivisions: 256,
max_boxes: 12,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum EnclosurePolicy {
Skip,
BestEffort(EnclosureBudget),
Required(EnclosureBudget),
}
#[derive(Clone, Copy, Debug)]
pub struct GateOptions {
pub tolerance: f64,
pub min_points: usize,
pub symbolic: bool,
pub egraph: bool,
pub enclosure: EnclosurePolicy,
pub min_strength: Strength,
}
impl Default for GateOptions {
fn default() -> Self {
GateOptions {
tolerance: 1e-7,
min_points: 3,
symbolic: true,
egraph: false,
enclosure: EnclosurePolicy::BestEffort(EnclosureBudget::cheap()),
min_strength: Strength::Sampled,
}
}
}
impl GateOptions {
pub fn sampled_only() -> Self {
GateOptions {
enclosure: EnclosurePolicy::Skip,
..GateOptions::default()
}
}
pub fn rigorous(budget: EnclosureBudget) -> Self {
GateOptions {
enclosure: EnclosurePolicy::Required(budget),
min_strength: Strength::Enclosure,
..GateOptions::default()
}
}
}
pub struct Target<'a> {
pub integrand: ExprId,
pub numeric: Option<&'a dyn Fn(f64) -> Option<f64>>,
}
impl<'a> Target<'a> {
pub fn symbolic(integrand: ExprId) -> Self {
Target {
integrand,
numeric: None,
}
}
pub fn with_numeric(mut self, f: &'a dyn Fn(f64) -> Option<f64>) -> Self {
self.numeric = Some(f);
self
}
fn eval(&self, x: f64, var: ExprId, pool: &ExprPool) -> Option<f64> {
match self.numeric {
Some(f) => f(x),
None => eval_at(self.integrand, var, x, pool),
}
}
}
pub fn verify(
candidate: ExprId,
target: &Target<'_>,
var: ExprId,
domain: &Domain<'_>,
opts: &GateOptions,
pool: &ExprPool,
) -> Verdict {
let Ok(df) = crate::diff::diff(candidate, var, pool) else {
return Verdict::Declined {
reason: DeclineReason::Differentiation,
};
};
let dfs = simplify(df.value, pool).value;
let residual = pool.add(vec![dfs, negate(target.integrand, pool)]);
if opts.symbolic {
let r = simplify(residual, pool).value;
if is_syntactic_zero(r, pool) {
return finalize(Verdict::Proven, opts);
}
#[cfg(feature = "egraph")]
if opts.egraph {
let r2 = crate::simplify::simplify_egraph(r, pool).value;
if is_syntactic_zero(r2, pool) {
return finalize(Verdict::Proven, opts);
}
}
}
let mut checked = 0usize;
for &x in domain.samples() {
if !domain.contains(x) {
continue;
}
let Some(rhs) = target.eval(x, var, pool) else {
continue;
};
let Some(lhs) = eval_at(dfs, var, x, pool) else {
continue;
};
if !rhs.is_finite() {
continue;
}
if !lhs.is_finite() {
return Verdict::Failed {
at: x,
residual: f64::INFINITY,
tolerance: opts.tolerance,
};
}
let err = (lhs - rhs).abs();
if err > opts.tolerance * (1.0 + rhs.abs()) {
return Verdict::Failed {
at: x,
residual: err,
tolerance: opts.tolerance,
};
}
checked += 1;
}
if checked < opts.min_points {
return Verdict::Declined {
reason: DeclineReason::NotEnoughPoints {
found: checked,
required: opts.min_points,
},
};
}
let sampled = Verdict::SampledOnly {
points: checked,
tolerance: opts.tolerance,
};
let budget = match opts.enclosure {
EnclosurePolicy::Skip => return finalize(sampled, opts),
EnclosurePolicy::BestEffort(b) | EnclosurePolicy::Required(b) => b,
};
let required = matches!(opts.enclosure, EnclosurePolicy::Required(_));
let resid = simplify(residual, pool).value;
match enclose(resid, var, domain, &budget, pool) {
Ok((boxes, residual_bound)) => finalize(
Verdict::EnclosureVerified {
boxes,
residual_bound,
points: checked,
tolerance: opts.tolerance,
},
opts,
),
Err(what) => {
if required {
Verdict::Declined {
reason: DeclineReason::EnclosureUnavailable { what },
}
} else {
finalize(sampled, opts)
}
}
}
}
fn finalize(v: Verdict, opts: &GateOptions) -> Verdict {
match v.strength() {
Some(s) if s >= opts.min_strength => v,
Some(s) => Verdict::Declined {
reason: DeclineReason::BelowRequiredStrength {
reached: s,
required: opts.min_strength,
},
},
None => v,
}
}
fn enclose(
resid: ExprId,
var: ExprId,
domain: &Domain<'_>,
budget: &EnclosureBudget,
pool: &ExprPool,
) -> Result<(Vec<VerifiedBox>, f64), String> {
use crate::validated::bounds::{bound_on_box, BoundOptions};
if domain.boxes().is_empty() {
return Err("the caller supplied no in-domain box".to_string());
}
let opts = BoundOptions {
order: budget.order,
prec: budget.prec,
tol: budget.tol,
max_subdivisions: budget.max_subdivisions,
};
let screened: Vec<(f64, f64)> = domain
.boxes()
.iter()
.copied()
.filter(|&(lo, hi)| {
[0.02_f64, 0.25, 0.5, 0.75, 0.98].iter().all(|f| {
eval_at(resid, var, lo + (hi - lo) * f, pool).is_some_and(|v| v.is_finite())
})
})
.collect();
const MAX_SPLIT_DEPTH: u32 = 3;
const MIN_SPLIT_WIDTH: f64 = 0.1;
let mut queue: std::collections::VecDeque<(f64, f64, u32)> =
screened.iter().map(|&(lo, hi)| (lo, hi, 0)).collect();
let mut covered: Vec<VerifiedBox> = Vec::new();
let mut worst = 0.0_f64;
let mut attempts = 0usize;
let mut last_refusal = String::from("no box survived the f64 pre-screen");
while let Some((lo, hi, depth)) = queue.pop_front() {
if attempts >= budget.max_boxes {
break;
}
attempts += 1;
let retry = |q: &mut std::collections::VecDeque<(f64, f64, u32)>| {
if depth >= MAX_SPLIT_DEPTH || (hi - lo) < 2.0 * MIN_SPLIT_WIDTH {
return;
}
let mid = 0.5 * (lo + hi);
for (a, b) in [(lo, mid), (mid, hi)] {
let evaluable = [0.02_f64, 0.5, 0.98].iter().all(|f| {
eval_at(resid, var, a + (b - a) * f, pool).is_some_and(|v| v.is_finite())
});
if evaluable {
q.push_back((a, b, depth + 1));
}
}
};
match bound_on_box(resid, pool, &[(var, lo, hi)], &opts) {
Ok(r) => {
let bound = r.lower().abs().max(r.upper().abs());
if !bound.is_finite() {
last_refusal = format!("enclosure on [{lo}, {hi}] is not finite");
retry(&mut queue);
continue;
}
if bound > budget.tol {
last_refusal = format!(
"residual bound {bound:.3e} on [{lo}, {hi}] exceeds the \
tolerance {:.3e} (budget exhausted: {})",
budget.tol, r.budget_exhausted
);
retry(&mut queue);
continue;
}
worst = worst.max(bound);
covered.push(VerifiedBox {
lo,
hi,
residual_bound: bound,
});
}
Err(e) => {
last_refusal = format!("[{lo}, {hi}]: {e}");
retry(&mut queue);
}
}
}
if covered.is_empty() {
return Err(last_refusal);
}
Ok((covered, worst))
}
fn negate(expr: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Add(args) => pool.add(args.iter().map(|&a| negate(a, pool)).collect()),
_ => pool.mul(vec![pool.integer(-1_i32), expr]),
}
}
fn is_syntactic_zero(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Integer(n) => n.0 == 0,
ExprData::Rational(r) => *r.0.numer() == 0,
ExprData::Float(f) => f.inner.to_f64() == 0.0,
_ => false,
}
}
fn registry() -> &'static PrimitiveRegistry {
static REGISTRY: OnceLock<PrimitiveRegistry> = OnceLock::new();
REGISTRY.get_or_init(PrimitiveRegistry::dispatch_registry)
}
pub fn eval_at(expr: ExprId, var: ExprId, x: f64, pool: &ExprPool) -> Option<f64> {
if expr == var {
return Some(x);
}
match pool.get(expr) {
ExprData::Integer(n) => Some(n.0.to_f64()),
ExprData::Rational(r) => Some(r.0.to_f64()),
ExprData::Float(f) => Some(f.inner.to_f64()),
ExprData::Symbol { .. } => None,
ExprData::Add(args) => args
.iter()
.try_fold(0.0, |s, &a| Some(s + eval_at(a, var, x, pool)?)),
ExprData::Mul(args) => args
.iter()
.try_fold(1.0, |s, &a| Some(s * eval_at(a, var, x, pool)?)),
ExprData::Pow { base, exp } => {
Some(eval_at(base, var, x, pool)?.powf(eval_at(exp, var, x, pool)?))
}
ExprData::Func { ref name, ref args } if !args.is_empty() => {
let mut vals = Vec::with_capacity(args.len());
for &a in args.iter() {
vals.push(eval_at(a, var, x, pool)?);
}
registry().numeric_f64(name, &vals)
}
ExprData::RootSum {
poly,
var: rvar,
body,
} => {
let env: std::collections::HashMap<ExprId, f64> = std::iter::once((var, x)).collect();
crate::eval::eval_root_sum_f64(poly, rvar, body, &env, pool)
}
_ => None,
}
}
#[derive(Clone, Copy, Debug)]
pub struct FitOptions {
pub max_residual: f64,
pub snap_window: f64,
pub drop_below: f64,
pub max_denominator: i64,
}
impl Default for FitOptions {
fn default() -> Self {
FitOptions {
max_residual: 1e-7,
snap_window: 1e-10,
drop_below: 1e-12,
max_denominator: 60,
}
}
}
pub fn fit_blocks(
blocks: &[ExprId],
var: ExprId,
samples: &[f64],
target: &dyn Fn(f64) -> Option<f64>,
opts: &FitOptions,
pool: &ExprPool,
) -> Option<Vec<f64>> {
let block_dx: Vec<ExprId> = blocks
.iter()
.map(|&blk| {
crate::diff::diff(blk, var, pool)
.ok()
.map(|d| simplify(d.value, pool).value)
})
.collect::<Option<Vec<_>>>()?;
let nblk = blocks.len();
if nblk == 0 {
return None;
}
let mut rows: Vec<Vec<f64>> = Vec::new();
let mut ys: Vec<f64> = Vec::new();
for &xv in samples {
let Some(yv) = target(xv) else { continue };
if !yv.is_finite() {
continue;
}
let mut row = Vec::with_capacity(nblk);
let mut ok = true;
for &dxi in &block_dx {
match eval_at(dxi, var, xv, pool) {
Some(v) if v.is_finite() => row.push(v),
_ => {
ok = false;
break;
}
}
}
if ok {
rows.push(row);
ys.push(yv);
}
}
if rows.len() < nblk + 1 {
return None;
}
let coeffs = lstsq(&rows, &ys, nblk)?;
let mut maxr = 0.0_f64;
for (row, &y) in rows.iter().zip(&ys) {
let pred: f64 = row.iter().zip(&coeffs).map(|(a, b)| a * b).sum();
maxr = maxr.max((pred - y).abs() / (1.0 + y.abs()));
}
if !maxr.is_finite() || maxr > opts.max_residual {
return None;
}
Some(
coeffs
.into_iter()
.map(|c| {
let snapped = snap_rational(c, opts.max_denominator);
if (snapped - c).abs() < opts.snap_window * (1.0 + c.abs()) {
snapped
} else {
c
}
})
.collect(),
)
}
pub fn assemble(
blocks: &[ExprId],
coeffs: &[f64],
opts: &FitOptions,
coeff_to_expr: &dyn Fn(f64, &ExprPool) -> ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
let mut terms: Vec<ExprId> = Vec::new();
for (&c, &blk) in coeffs.iter().zip(blocks) {
if c.abs() < opts.drop_below {
continue;
}
terms.push(pool.mul(vec![coeff_to_expr(c, pool), blk]));
}
if terms.is_empty() {
return None;
}
Some(simplify(pool.add(terms), pool).value)
}
pub fn lstsq(rows: &[Vec<f64>], ys: &[f64], n: usize) -> Option<Vec<f64>> {
if n == 0 {
return None;
}
let mut ata = vec![vec![0.0_f64; n]; n];
let mut aty = vec![0.0_f64; n];
for (row, &y) in rows.iter().zip(ys) {
if row.len() != n {
return None;
}
for i in 0..n {
aty[i] += row[i] * y;
for j in 0..n {
ata[i][j] += row[i] * row[j];
}
}
}
for col in 0..n {
let mut piv = col;
let mut best = ata[col][col].abs();
for (r, arow) in ata.iter().enumerate().take(n).skip(col + 1) {
if arow[col].abs() > best {
best = arow[col].abs();
piv = r;
}
}
if best < 1e-12 {
return None; }
ata.swap(col, piv);
aty.swap(col, piv);
let d = ata[col][col];
let pivot_row = ata[col].clone();
let pivot_y = aty[col];
for r in 0..n {
if r == col {
continue;
}
let f = ata[r][col] / d;
if f == 0.0 {
continue;
}
for (c, prc) in pivot_row.iter().enumerate().take(n).skip(col) {
ata[r][c] -= f * prc;
}
aty[r] -= f * pivot_y;
}
}
let mut out = vec![0.0; n];
for (i, oi) in out.iter_mut().enumerate() {
*oi = aty[i] / ata[i][i];
if !oi.is_finite() {
return None;
}
}
Some(out)
}
pub fn snap_rational(v: f64, max_den: i64) -> f64 {
if v.abs() < 1e-9 {
return 0.0;
}
for den in 1..=max_den.max(1) {
let num = (v * den as f64).round();
let cand = num / den as f64;
if (cand - v).abs() < 1e-9 * (1.0 + v.abs()) {
return cand;
}
}
v
}
#[derive(Clone, Debug)]
pub struct Accepted {
pub antiderivative: ExprId,
pub verdict: Verdict,
}
pub fn verify_first<I>(
proposals: I,
target: &Target<'_>,
var: ExprId,
domain: &Domain<'_>,
opts: &GateOptions,
pool: &ExprPool,
) -> Option<Accepted>
where
I: IntoIterator<Item = ExprId>,
{
for candidate in proposals {
let verdict = verify(candidate, target, var, domain, opts, pool);
if verdict.is_verified() {
return Some(Accepted {
antiderivative: candidate,
verdict,
});
}
}
None
}
#[allow(clippy::too_many_arguments)]
pub fn propose_fit_verify(
recipes: &[Vec<ExprId>],
fit_samples: &[f64],
fit_target: &dyn Fn(f64) -> Option<f64>,
coeff_to_expr: &dyn Fn(f64, &ExprPool) -> ExprId,
target: &Target<'_>,
var: ExprId,
domain: &Domain<'_>,
fit_opts: &FitOptions,
gate_opts: &GateOptions,
pool: &ExprPool,
) -> Option<Accepted> {
for blocks in recipes {
let Some(coeffs) = fit_blocks(blocks, var, fit_samples, fit_target, fit_opts, pool) else {
continue;
};
let Some(cand) = assemble(blocks, &coeffs, fit_opts, coeff_to_expr, pool) else {
continue;
};
let verdict = verify(cand, target, var, domain, gate_opts, pool);
if verdict.is_verified() {
return Some(Accepted {
antiderivative: cand,
verdict,
});
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain as SymDomain;
fn pool_and_x() -> (ExprPool, ExprId) {
let pool = ExprPool::new();
let x = pool.symbol("x", SymDomain::Real);
(pool, x)
}
fn grid(lo: f64, hi: f64, n: usize) -> Vec<f64> {
(0..=n)
.map(|i| lo + (hi - lo) * (i as f64) / (n as f64))
.collect()
}
#[test]
fn a_domain_hole_in_the_candidate_is_a_refutation() {
let (pool, x) = pool_and_x();
let abs_x = pool.func("sqrt", vec![pool.pow(x, pool.integer(2_i32))]);
let f_cand = pool.func("log", vec![pool.add(vec![x, abs_x])]);
let integrand = pool.pow(x, pool.integer(-1_i32));
let dom = Domain::from_samples(vec![-3.1, -2.3, -1.7, -0.9, 0.9, 1.7, 2.3, 3.1]);
let opts = GateOptions {
symbolic: false,
enclosure: EnclosurePolicy::Skip,
..GateOptions::default()
};
let v = verify(f_cand, &Target::symbolic(integrand), x, &dom, &opts, &pool);
match v {
Verdict::Failed { at, residual, .. } => {
assert!(at < 0.0, "refuted at {at}, expected a negative sample");
assert!(
residual.is_infinite(),
"a non-finite derivative reports an infinite residual, got {residual}"
);
}
other => panic!("expected a refutation, got {other:?}"),
}
}
#[test]
fn a_candidate_defined_beyond_the_integrand_is_not_refuted() {
let (pool, x) = pool_and_x();
let half = pool.rational(rug::Integer::from(1), rug::Integer::from(2));
let f_cand = pool.mul(vec![half, pool.pow(x, pool.integer(2_i32))]);
let root = pool.func("sqrt", vec![pool.add(vec![x, pool.integer(-1_i32)])]);
let integrand = pool.mul(vec![x, root, pool.pow(root, pool.integer(-1_i32))]);
let dom = Domain::from_samples(vec![-2.0, -1.0, 0.0, 1.5, 2.5, 3.5, 4.5]);
let opts = GateOptions {
symbolic: false,
enclosure: EnclosurePolicy::Skip,
..GateOptions::default()
};
let v = verify(f_cand, &Target::symbolic(integrand), x, &dom, &opts, &pool);
assert!(
matches!(v, Verdict::SampledOnly { .. }),
"samples below 1 are outside the integrand's domain and say nothing, got {v:?}"
);
}
#[test]
fn exact_identity_is_proven() {
let (pool, x) = pool_and_x();
let third = pool.rational(rug::Integer::from(1), rug::Integer::from(3));
let f_cand = pool.mul(vec![third, pool.pow(x, pool.integer(3_i32))]);
let integrand = pool.pow(x, pool.integer(2_i32));
let dom = Domain::from_samples(grid(-2.0, 2.0, 20));
let v = verify(
f_cand,
&Target::symbolic(integrand),
x,
&dom,
&GateOptions::default(),
&pool,
);
assert_eq!(v, Verdict::Proven, "expected a symbolic proof, got {v:?}");
assert_eq!(v.strength(), Some(Strength::Symbolic));
assert!(v.meets(Strength::Enclosure));
}
#[test]
fn a_wrong_candidate_is_refuted_not_declined() {
let (pool, x) = pool_and_x();
let f_cand = pool.pow(x, pool.integer(2_i32));
let integrand = pool.pow(x, pool.integer(2_i32));
let dom = Domain::from_samples(grid(1.0, 3.0, 20));
let v = verify(
f_cand,
&Target::symbolic(integrand),
x,
&dom,
&GateOptions::sampled_only(),
&pool,
);
assert!(matches!(v, Verdict::Failed { .. }), "got {v:?}");
assert!(!v.is_verified());
}
#[test]
fn enclosure_tier_upgrades_a_float_coefficient_candidate() {
let (pool, x) = pool_and_x();
let half = pool.rational(rug::Integer::from(1), rug::Integer::from(2));
let tiny = pool.rational(
rug::Integer::from(1),
rug::Integer::from(1_000_000_000_000_i64),
);
let f_cand = pool.add(vec![
pool.mul(vec![half, pool.pow(x, pool.integer(2_i32))]),
pool.mul(vec![tiny, x]),
]);
let dom = Domain::from_samples(grid(-2.0, 2.0, 20)).with_boxes(vec![(-2.0, 2.0)]);
let opts = GateOptions::rigorous(EnclosureBudget::thorough());
let v = verify(f_cand, &Target::symbolic(x), x, &dom, &opts, &pool);
match &v {
Verdict::EnclosureVerified {
boxes,
residual_bound,
..
} => {
assert_eq!(boxes.len(), 1);
assert_eq!((boxes[0].lo, boxes[0].hi), (-2.0, 2.0));
assert!(
*residual_bound < 1e-8 && *residual_bound > 0.0,
"bound {residual_bound:e}"
);
}
other => panic!("expected an enclosure verdict, got {other:?}"),
}
assert_eq!(v.strength(), Some(Strength::Enclosure));
}
#[test]
fn required_enclosure_declines_when_no_box_is_given() {
let (pool, x) = pool_and_x();
let half = pool.rational(rug::Integer::from(1), rug::Integer::from(2));
let tiny = pool.rational(
rug::Integer::from(1),
rug::Integer::from(1_000_000_000_000_i64),
);
let f_cand = pool.add(vec![
pool.mul(vec![half, pool.pow(x, pool.integer(2_i32))]),
pool.mul(vec![tiny, x]),
]);
let dom = Domain::from_samples(grid(-2.0, 2.0, 20)); let opts = GateOptions::rigorous(EnclosureBudget::cheap());
let v = verify(f_cand, &Target::symbolic(x), x, &dom, &opts, &pool);
assert!(
matches!(
v,
Verdict::Declined {
reason: DeclineReason::EnclosureUnavailable { .. }
}
),
"got {v:?}"
);
}
#[test]
fn domain_predicate_skips_out_of_domain_points() {
let (pool, x) = pool_and_x();
let sqrt_x = pool.func("sqrt", vec![x]);
let f_cand = pool.mul(vec![pool.integer(2_i32), sqrt_x]);
let integrand = pool.pow(sqrt_x, pool.integer(-1_i32));
let dom = Domain::from_samples(grid(-3.0, 3.0, 24)).with_predicate(|v: f64| v > 1e-6);
let v = verify(
f_cand,
&Target::symbolic(integrand),
x,
&dom,
&GateOptions::sampled_only(),
&pool,
);
assert!(v.is_verified(), "got {v:?}");
}
#[test]
fn too_few_evaluable_points_declines() {
let (pool, x) = pool_and_x();
let sqrt_x = pool.func("sqrt", vec![x]);
let f_cand = pool.mul(vec![pool.integer(2_i32), sqrt_x]);
let integrand = pool.pow(sqrt_x, pool.integer(-1_i32));
let dom = Domain::from_samples(vec![-1.0, 1.0, 2.0]).with_predicate(|v: f64| v > 1e-6);
let v = verify(
f_cand,
&Target::symbolic(integrand),
x,
&dom,
&GateOptions {
symbolic: false,
..GateOptions::sampled_only()
},
&pool,
);
assert!(
matches!(
v,
Verdict::Declined {
reason: DeclineReason::NotEnoughPoints { found: 2, .. }
}
),
"got {v:?}"
);
}
#[test]
fn min_strength_downgrades_a_sampled_pass_to_a_decline() {
let (pool, x) = pool_and_x();
let sqrt_x = pool.func("sqrt", vec![x]);
let f_cand = pool.mul(vec![pool.integer(2_i32), sqrt_x]);
let integrand = pool.pow(sqrt_x, pool.integer(-1_i32));
let dom = Domain::from_samples(grid(0.5, 3.0, 20)).with_predicate(|v: f64| v > 1e-6);
let opts = GateOptions {
symbolic: false,
enclosure: EnclosurePolicy::Skip,
min_strength: Strength::Enclosure,
..GateOptions::default()
};
let v = verify(f_cand, &Target::symbolic(integrand), x, &dom, &opts, &pool);
assert!(
matches!(
v,
Verdict::Declined {
reason: DeclineReason::BelowRequiredStrength {
reached: Strength::Sampled,
required: Strength::Enclosure,
}
}
),
"got {v:?}"
);
}
#[test]
fn eval_at_dispatches_through_the_primitive_registry() {
let (pool, x) = pool_and_x();
let asin = pool.func("asin", vec![x]);
assert!((eval_at(asin, x, 0.5, &pool).unwrap() - 0.5_f64.asin()).abs() < 1e-15);
let m = pool.rational(rug::Integer::from(1), rug::Integer::from(2));
let ef = pool.func("EllipticF", vec![x, m]);
let v = eval_at(ef, x, std::f64::consts::FRAC_PI_4, &pool).expect("EllipticF evaluates");
assert!((v - 0.826_017_876).abs() < 1e-6, "{v}");
let bogus = pool.func("no_such_primitive", vec![x]);
assert_eq!(eval_at(bogus, x, 1.0, &pool), None);
}
#[test]
fn fit_then_verify_recovers_an_exact_ansatz() {
let (pool, x) = pool_and_x();
let blocks = vec![
pool.pow(x, pool.integer(3_i32)),
pool.pow(x, pool.integer(2_i32)),
x,
];
let integrand = pool.add(vec![
pool.mul(vec![pool.integer(3_i32), pool.pow(x, pool.integer(2_i32))]),
pool.mul(vec![pool.integer(2_i32), x]),
]);
let samples = grid(-2.0, 2.0, 40);
let target_num = |v: f64| Some(3.0 * v * v + 2.0 * v);
let to_expr = |c: f64, p: &ExprPool| -> ExprId {
match rug::Rational::from_f64(c) {
Some(r) => {
let (n, d) = r.into_numer_denom();
p.rational(n, d)
}
None => p.integer(0_i32),
}
};
let got = propose_fit_verify(
&[blocks],
&samples,
&target_num,
&to_expr,
&Target::symbolic(integrand),
x,
&Domain::from_samples(samples.clone()),
&FitOptions::default(),
&GateOptions::default(),
&pool,
)
.expect("the ansatz spans the antiderivative");
assert_eq!(got.verdict, Verdict::Proven);
let s = pool.display(got.antiderivative).to_string();
assert!(s.contains("x^3") && s.contains("x^2"), "{s}");
}
#[test]
fn a_deficient_ansatz_declines_rather_than_emitting() {
let (pool, x) = pool_and_x();
let blocks = vec![x, pool.pow(x, pool.integer(2_i32))];
let integrand = pool.pow(x, pool.integer(-1_i32));
let samples = grid(0.5, 3.0, 40);
let target_num = |v: f64| Some(1.0 / v);
let to_expr = |c: f64, p: &ExprPool| -> ExprId {
match rug::Rational::from_f64(c) {
Some(r) => {
let (n, d) = r.into_numer_denom();
p.rational(n, d)
}
None => p.integer(0_i32),
}
};
let got = propose_fit_verify(
&[blocks],
&samples,
&target_num,
&to_expr,
&Target::symbolic(integrand),
x,
&Domain::from_samples(samples.clone()),
&FitOptions::default(),
&GateOptions::default(),
&pool,
);
assert!(got.is_none(), "a deficient ansatz must decline");
}
#[test]
fn snap_rational_is_cosmetic_and_bounded() {
assert_eq!(snap_rational(0.5000000000001, 60), 0.5);
assert_eq!(snap_rational(1e-12, 60), 0.0);
let s = snap_rational(std::f64::consts::SQRT_2, 60);
assert_eq!(s, std::f64::consts::SQRT_2);
}
}