use super::taylor::{taylor_range, TaylorContext, MAX_ORDER};
use super::{
contains_zero, from_bounds, from_float, is_finite, lb, mag, ub, width, ValidatedError,
};
use crate::ball::ArbBall;
use crate::diff::diff;
use crate::kernel::subs::subs;
use crate::kernel::{ExprData, ExprId, ExprPool};
use crate::simplify::simplify;
use rug::float::Round;
use rug::Float;
use std::collections::{HashMap, VecDeque};
type Result<T> = std::result::Result<T, ValidatedError>;
const SINGULARITY_BISECTION_LIMIT: i32 = 60;
#[derive(Clone, Debug)]
pub struct BoundOptions {
pub order: usize,
pub prec: u32,
pub tol: f64,
pub max_subdivisions: usize,
}
impl Default for BoundOptions {
fn default() -> Self {
BoundOptions {
order: 6,
prec: 128,
tol: 1e-9,
max_subdivisions: 2048,
}
}
}
#[derive(Clone, Debug)]
pub struct BoundResult {
enclosure: ArbBall,
pub budget_exhausted: bool,
pub subdivisions: usize,
}
impl BoundResult {
pub fn enclosure(&self) -> &ArbBall {
&self.enclosure
}
pub fn lower(&self) -> f64 {
self.enclosure.lo().to_f64_round(Round::Down)
}
pub fn upper(&self) -> f64 {
self.enclosure.hi().to_f64_round(Round::Up)
}
}
type FBox = (ExprId, Float, Float);
fn max_dim_width(boxes: &[FBox], prec: u32) -> f64 {
boxes
.iter()
.map(|(_, lo, hi)| Float::with_val(prec, hi - lo).to_f64_round(Round::Up))
.fold(0.0_f64, f64::max)
}
fn midpoint(lo: &Float, hi: &Float, prec: u32) -> Float {
Float::with_val(prec, Float::with_val(prec, lo + hi) / 2u32)
}
fn split_widest(boxes: &[FBox], prec: u32) -> (Vec<FBox>, Vec<FBox>) {
let mut best = 0usize;
let mut best_w = Float::with_val(prec, 0.0);
for (i, (_, lo, hi)) in boxes.iter().enumerate() {
let w = Float::with_val(prec, hi - lo);
if i == 0 || w > best_w {
best_w = w;
best = i;
}
}
let (v, lo, hi) = &boxes[best];
let mid = midpoint(lo, hi, prec);
let mut b1 = boxes.to_vec();
let mut b2 = boxes.to_vec();
b1[best] = (*v, lo.clone(), mid.clone());
b2[best] = (*v, mid, hi.clone());
(b1, b2)
}
fn is_recoverable_domain_issue(e: &ValidatedError) -> bool {
matches!(
e,
ValidatedError::DomainViolation { .. } | ValidatedError::NotFinite { .. }
)
}
pub fn bound_on_box(
expr: ExprId,
pool: &ExprPool,
boxes: &[(ExprId, f64, f64)],
opts: &BoundOptions,
) -> Result<BoundResult> {
let prec = opts.prec;
let boxes0: Vec<FBox> = boxes
.iter()
.map(|(v, lo, hi)| (*v, Float::with_val(prec, lo), Float::with_val(prec, hi)))
.collect();
bound_on_fboxes(expr, pool, &boxes0, opts)
}
fn bound_on_fboxes(
expr: ExprId,
pool: &ExprPool,
boxes0: &[FBox],
opts: &BoundOptions,
) -> Result<BoundResult> {
if boxes0.is_empty() {
return Err(ValidatedError::InvalidInput {
what: "the box must constrain at least one variable".into(),
});
}
if opts.order == 0 || opts.order > MAX_ORDER {
return Err(ValidatedError::InvalidInput {
what: format!("Taylor order must be in 1..={MAX_ORDER}"),
});
}
let prec = opts.prec;
let (lo_bound, used_lo, exhausted_lo) = extremum_search(
expr,
pool,
boxes0,
opts,
Extremum::Min,
SearchGoal::Tolerance,
)?;
let (hi_bound, used_hi, exhausted_hi) = extremum_search(
expr,
pool,
boxes0,
opts,
Extremum::Max,
SearchGoal::Tolerance,
)?;
let enclosure = from_bounds(&lo_bound, &hi_bound, prec);
if !is_finite(&enclosure) {
return Err(ValidatedError::NotFinite {
what: "range enclosure".into(),
});
}
Ok(BoundResult {
enclosure,
budget_exhausted: exhausted_lo || exhausted_hi,
subdivisions: used_lo + used_hi,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Extremum {
Min,
Max,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum SearchGoal {
Tolerance,
DecideSign,
}
fn extremum_search(
expr: ExprId,
pool: &ExprPool,
boxes0: &[FBox],
opts: &BoundOptions,
which: Extremum,
goal: SearchGoal,
) -> Result<(Float, usize, bool)> {
let prec = opts.prec;
let floor = max_dim_width(boxes0, prec) * 2f64.powi(-SINGULARITY_BISECTION_LIMIT);
let tol_f = Float::with_val(prec, opts.tol);
let sign_lo = |r: &ArbBall| -> Float {
match which {
Extremum::Min => lb(r),
Extremum::Max => -ub(r),
}
};
let sign_hi = |r: &ArbBall| -> Float {
match which {
Extremum::Min => ub(r),
Extremum::Max => -lb(r),
}
};
let mut active: Vec<(Float, Vec<FBox>)> = Vec::new();
let mut retired: Option<Float> = None;
let mut best_ub: Option<Float> = None;
let mut subdivisions = 0usize;
let mut exhausted = false;
let keep_min = |cur: Option<Float>, k: Float| -> Option<Float> {
Some(match cur {
Some(c) if c <= k => c,
_ => k,
})
};
let seed = evaluate_box(expr, pool, boxes0, opts, floor, prec)?;
match seed {
BoxOutcome::Range(r) => {
best_ub = Some(sign_hi(&r));
active.push((sign_lo(&r), boxes0.to_vec()));
}
BoxOutcome::Refine => {
active.push((Float::with_val(prec, f64::NEG_INFINITY), boxes0.to_vec()));
}
}
while let Some(idx) = argmin_key(&active) {
let (key, b) = active.swap_remove(idx);
let overall = match &retired {
Some(r) if r < &key => r.clone(),
_ => key.clone(),
};
if goal == SearchGoal::DecideSign && overall > 0 {
active.push((key, b));
break;
}
if let Some(ub_best) = &best_ub {
if &key > ub_best {
continue;
}
if goal == SearchGoal::Tolerance {
let gap = Float::with_val(prec, ub_best - &overall);
if gap <= tol_f {
active.push((key, b));
break;
}
}
}
if subdivisions >= opts.max_subdivisions {
exhausted = true;
active.push((key, b));
break;
}
if max_dim_width(&b, prec) <= floor {
retired = keep_min(retired, key);
continue;
}
subdivisions += 1;
let (b1, b2) = split_widest(&b, prec);
for child in [b1, b2] {
match evaluate_box(expr, pool, &child, opts, floor, prec)? {
BoxOutcome::Range(r) => {
let child_ub = sign_hi(&r);
best_ub = Some(match best_ub {
Some(cur) if cur <= child_ub => cur,
_ => child_ub,
});
active.push((sign_lo(&r), child));
}
BoxOutcome::Refine => {
active.push((Float::with_val(prec, f64::NEG_INFINITY), child));
}
}
}
}
let mut bound = active
.iter()
.map(|(k, _)| k.clone())
.chain(retired.clone())
.fold(None::<Float>, keep_min)
.or_else(|| best_ub.clone())
.ok_or_else(|| ValidatedError::InvalidInput {
what: "no enclosure was produced".into(),
})?;
if active.is_empty() && retired.is_some() {
exhausted = true;
}
if let Some(ub_best) = &best_ub {
if &bound > ub_best {
bound = ub_best.clone();
}
}
if !bound.is_finite() {
return Err(ValidatedError::NotFinite {
what: "range enclosure".into(),
});
}
Ok((
match which {
Extremum::Min => bound,
Extremum::Max => -bound,
},
subdivisions,
exhausted,
))
}
enum BoxOutcome {
Range(ArbBall),
Refine,
}
fn evaluate_box(
expr: ExprId,
pool: &ExprPool,
b: &[FBox],
opts: &BoundOptions,
floor: f64,
prec: u32,
) -> Result<BoxOutcome> {
match taylor_range(expr, pool, b, opts.order, prec) {
Ok(r) => Ok(BoxOutcome::Range(r)),
Err(e) if is_recoverable_domain_issue(&e) => {
if max_dim_width(b, prec) <= floor {
Err(e)
} else {
Ok(BoxOutcome::Refine)
}
}
Err(e) => Err(e),
}
}
fn argmin_key(active: &[(Float, Vec<FBox>)]) -> Option<usize> {
let mut best: Option<(usize, &Float)> = None;
for (i, (k, _)) in active.iter().enumerate() {
match best {
Some((_, bk)) if bk <= k => {}
_ => best = Some((i, k)),
}
}
best.map(|(i, _)| i)
}
#[derive(Clone, Debug)]
pub struct IntegralOptions {
pub order: usize,
pub prec: u32,
pub tol: f64,
pub max_subdivisions: usize,
}
impl Default for IntegralOptions {
fn default() -> Self {
IntegralOptions {
order: 6,
prec: 128,
tol: 1e-9,
max_subdivisions: 2048,
}
}
}
#[derive(Clone, Debug)]
pub struct IntegralResult {
enclosure: ArbBall,
pub budget_exhausted: bool,
pub subdivisions: usize,
}
impl IntegralResult {
pub fn enclosure(&self) -> &ArbBall {
&self.enclosure
}
pub fn lower(&self) -> f64 {
self.enclosure.lo().to_f64_round(Round::Down)
}
pub fn upper(&self) -> f64 {
self.enclosure.hi().to_f64_round(Round::Up)
}
}
fn local_integral(
expr: ExprId,
pool: &ExprPool,
var: ExprId,
lo: &Float,
hi: &Float,
order: usize,
prec: u32,
) -> Result<ArbBall> {
let boxes = vec![(var, lo.clone(), hi.clone())];
let mut ctx = TaylorContext::new(pool, &boxes, order, prec)?;
let tm = ctx.eval(expr)?;
let poly_integral = tm.integrate_normalized_1d()?;
let half_width = Float::with_val(prec, Float::with_val(prec, hi - lo) / 2u32);
let r_ball = from_float(&half_width, prec);
let piece = r_ball * poly_integral;
if !is_finite(&piece) {
return Err(ValidatedError::NotFinite {
what: "integral piece".into(),
});
}
Ok(piece)
}
fn split_quotient(expr: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId)> {
let factors: Vec<ExprId> = match pool.get(expr) {
ExprData::Mul(args) => args.to_vec(),
ExprData::Pow { .. } => vec![expr],
_ => return None,
};
let mut num: Vec<ExprId> = Vec::new();
let mut den: Vec<ExprId> = Vec::new();
for f in factors {
match pool.get(f) {
ExprData::Pow { base, exp } => match pool.get(exp) {
ExprData::Integer(n) if n.0 < 0 => {
let k: rug::Integer = -n.0.clone();
den.push(pool.pow(base, pool.integer(k)));
}
_ => num.push(f),
},
_ => num.push(f),
}
}
if den.is_empty() {
return None;
}
let n = match num.len() {
0 => pool.integer(1_i32),
1 => num[0],
_ => pool.mul(num),
};
let d = if den.len() == 1 {
den[0]
} else {
pool.mul(den)
};
Some((n, d))
}
fn vanishes_exactly(expr: ExprId, pool: &ExprPool, var: ExprId, at: &Float) -> bool {
vanishes_exactly_at(expr, pool, &[(var, at.clone(), at.clone())])
}
fn vanishes_exactly_at(expr: ExprId, pool: &ExprPool, point: &[FBox]) -> bool {
let mut mapping = HashMap::new();
for (var, lo, hi) in point {
if lo != hi {
return false;
}
let Some(rational) = lo.to_rational() else {
return false;
};
let (n, d) = rational.into_numer_denom();
let value = if d == 1 {
pool.integer(n)
} else {
pool.rational(n, d)
};
mapping.insert(*var, value);
}
let substituted = subs(expr, &mapping, pool);
let reduced = simplify(substituted, pool).value;
match pool.get(reduced) {
ExprData::Integer(n) => n.0.cmp0() == std::cmp::Ordering::Equal,
ExprData::Rational(r) => r.0.cmp0() == std::cmp::Ordering::Equal,
_ => false,
}
}
fn enclosure_admits_zero(
expr: ExprId,
pool: &ExprPool,
var: ExprId,
at: &Float,
order: usize,
prec: u32,
) -> bool {
let point = vec![(var, at.clone(), at.clone())];
match taylor_range(expr, pool, &point, order, prec) {
Ok(r) => contains_zero(&r),
Err(_) => false,
}
}
fn point_value(
expr: ExprId,
pool: &ExprPool,
var: ExprId,
at: &Float,
order: usize,
prec: u32,
) -> Option<Float> {
let point = vec![(var, at.clone(), at.clone())];
let r = taylor_range(expr, pool, &point, order, prec).ok()?;
r.mid.is_finite().then(|| r.mid.clone())
}
struct RemovableQuotient {
num: ExprId,
den: ExprId,
dden: ExprId,
ratio: ExprId,
}
impl RemovableQuotient {
fn detect(expr: ExprId, pool: &ExprPool, var: ExprId) -> Option<Self> {
let (num, den) = split_quotient(expr, pool)?;
let dnum = diff(num, var, pool).ok()?.value;
let dden = diff(den, var, pool).ok()?.value;
let ratio = pool.mul(vec![dnum, pool.pow(dden, pool.integer(-1_i32))]);
Some(RemovableQuotient {
num,
den,
dden,
ratio,
})
}
fn newton_candidates(
&self,
pool: &ExprPool,
var: ExprId,
lo: &Float,
hi: &Float,
opts: &IntegralOptions,
) -> Vec<Float> {
const STEPS: usize = 6;
let (order, prec) = (opts.order, opts.prec);
let mut out = Vec::new();
let mut z = midpoint(lo, hi, prec);
for _ in 0..STEPS {
let Some(f) = point_value(self.den, pool, var, &z, order, prec) else {
break;
};
let Some(df) = point_value(self.dden, pool, var, &z, order, prec) else {
break;
};
if df.is_zero() || !df.is_finite() {
break;
}
let next = Float::with_val(prec, &z - Float::with_val(prec, &f / &df));
if !next.is_finite() || next < *lo || next > *hi || next == z {
break;
}
z = next;
out.push(z.clone());
}
out
}
fn piece(
&self,
pool: &ExprPool,
var: ExprId,
lo: &Float,
hi: &Float,
opts: &IntegralOptions,
) -> Option<ArbBall> {
let prec = opts.prec;
if lo >= hi {
return None;
}
let bopts = BoundOptions {
order: opts.order,
prec,
tol: opts.tol,
max_subdivisions: opts.max_subdivisions,
};
let mut candidates = vec![lo.clone(), hi.clone(), midpoint(lo, hi, prec)];
candidates.extend(self.newton_candidates(pool, var, lo, hi, opts));
candidates.iter().find(|p| {
vanishes_exactly(self.den, pool, var, p)
&& vanishes_exactly(self.num, pool, var, p)
&& enclosure_admits_zero(self.den, pool, var, p, opts.order, prec)
&& enclosure_admits_zero(self.num, pool, var, p, opts.order, prec)
})?;
let j = vec![(var, lo.clone(), hi.clone())];
bound_on_fboxes(self.num, pool, &j, &bopts).ok()?;
bound_on_fboxes(self.den, pool, &j, &bopts).ok()?;
let dd = bound_on_fboxes(self.dden, pool, &j, &bopts).ok()?;
if contains_zero(dd.enclosure()) {
return None;
}
let r = bound_on_fboxes(self.ratio, pool, &j, &bopts).ok()?;
let w = Float::with_val(prec, hi - lo);
let piece = from_float(&w, prec) * r.enclosure().clone();
if !is_finite(&piece) {
return None;
}
Some(piece)
}
}
fn describe_singularity(
cause: ValidatedError,
lo: &Float,
hi: &Float,
a: &Float,
b: &Float,
) -> ValidatedError {
let ValidatedError::DomainViolation { what } = &cause else {
return cause;
};
let where_ = if lo <= a {
"the left endpoint"
} else if hi >= b {
"the right endpoint"
} else {
"an interior point"
};
let at = midpoint(lo, hi, lo.prec()).to_f64();
ValidatedError::DomainViolation {
what: format!(
"the integrand is singular at {where_} x ≈ {at:e} ({what}). \
This is a statement about the *integrand*, not about the integral: \
an integrable singularity still has a finite integral, which this \
routine cannot certify. Removable singularities written as N(x)/D(x) \
with N(p) = D(p) = 0 exactly and D'(p) ≠ 0 are handled automatically \
(their continuous extension is integrated); this one is not of that form"
),
}
}
pub fn verified_integral(
expr: ExprId,
pool: &ExprPool,
var: ExprId,
a: f64,
b: f64,
opts: &IntegralOptions,
) -> Result<IntegralResult> {
if !(a.is_finite() && b.is_finite()) {
return Err(ValidatedError::InvalidInput {
what: "integration bounds must be finite; infinite-limit improper integrals are not supported".into(),
});
}
if a > b {
return Err(ValidatedError::InvalidInput {
what: "verified_integral requires a <= b".into(),
});
}
if opts.order == 0 || opts.order > MAX_ORDER {
return Err(ValidatedError::InvalidInput {
what: format!("Taylor order must be in 1..={MAX_ORDER}"),
});
}
let prec = opts.prec;
if a == b {
return Ok(IntegralResult {
enclosure: ArbBall::from_f64(0.0, prec),
budget_exhausted: false,
subdivisions: 0,
});
}
let a_f = Float::with_val(prec, a);
let b_f = Float::with_val(prec, b);
let total_width = Float::with_val(prec, &b_f - &a_f);
let floor = total_width.to_f64_round(Round::Up) * 2f64.powi(-SINGULARITY_BISECTION_LIMIT);
let tol_total = Float::with_val(prec, opts.tol);
let mut stack: Vec<(Float, Float)> = vec![(a_f.clone(), b_f.clone())];
let mut total: Option<ArbBall> = None;
let mut subdivisions = 0usize;
let mut exhausted = false;
let mut removable: Option<Option<RemovableQuotient>> = None;
while let Some((lo, hi)) = stack.pop() {
let piece_w = Float::with_val(prec, &hi - &lo);
let piece_tol = Float::with_val(
prec,
&tol_total * Float::with_val(prec, &piece_w / &total_width),
);
let outcome = match local_integral(expr, pool, var, &lo, &hi, opts.order, prec) {
Ok(piece) => Ok(piece),
Err(e) if is_recoverable_domain_issue(&e) => {
let q = removable
.get_or_insert_with(|| RemovableQuotient::detect(expr, pool, var))
.as_ref();
match q.and_then(|q| q.piece(pool, var, &lo, &hi, opts)) {
Some(piece) => Ok(piece),
None => Err(e),
}
}
Err(e) => return Err(e),
};
match outcome {
Ok(piece) => {
let w = width(&piece);
if w <= piece_tol || subdivisions >= opts.max_subdivisions {
if w > piece_tol {
exhausted = true;
}
total = Some(match total {
Some(t) => t + piece,
None => piece,
});
} else {
subdivisions += 1;
let mid = midpoint(&lo, &hi, prec);
stack.push((mid.clone(), hi));
stack.push((lo, mid));
}
}
Err(e) => {
let w = piece_w.to_f64_round(Round::Up);
if subdivisions >= opts.max_subdivisions || w <= floor {
return Err(describe_singularity(e, &lo, &hi, &a_f, &b_f));
}
subdivisions += 1;
let mid = midpoint(&lo, &hi, prec);
stack.push((mid.clone(), hi));
stack.push((lo, mid));
}
}
}
let enclosure = total.ok_or_else(|| ValidatedError::InvalidInput {
what: "no enclosure was produced".into(),
})?;
if !is_finite(&enclosure) {
return Err(ValidatedError::NotFinite {
what: "integral enclosure".into(),
});
}
Ok(IntegralResult {
enclosure,
budget_exhausted: exhausted,
subdivisions,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verdict {
True,
False,
Undecided,
}
fn determined_sign(b: &ArbBall) -> Option<bool> {
if lb(b) > 0 {
Some(true)
} else if ub(b) < 0 {
Some(false)
} else {
None
}
}
#[derive(Default)]
struct SignWitnesses {
positive: bool,
negative: bool,
}
impl SignWitnesses {
fn record(&mut self, s: Option<bool>) {
match s {
Some(true) => self.positive = true,
Some(false) => self.negative = true,
None => {}
}
}
fn both(&self) -> bool {
self.positive && self.negative
}
}
fn is_exact_zero(b: &ArbBall) -> bool {
lb(b) == 0 && ub(b) == 0
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PointOutcome {
Zero,
Sign(bool),
Unknown,
}
fn point_outcome(
expr: ExprId,
pool: &ExprPool,
point: &[FBox],
order: usize,
prec: u32,
symbolic: bool,
) -> PointOutcome {
let Ok(r) = taylor_range(expr, pool, point, order, prec) else {
return PointOutcome::Unknown;
};
if let Some(s) = determined_sign(&r) {
return PointOutcome::Sign(s);
}
if is_exact_zero(&r) {
return PointOutcome::Zero;
}
if symbolic && contains_zero(&r) && vanishes_exactly_at(expr, pool, point) {
return PointOutcome::Zero;
}
PointOutcome::Unknown
}
fn seed_points(boxes0: &[FBox], prec: u32) -> Vec<Vec<FBox>> {
let n = boxes0.len();
let centre: Vec<Float> = boxes0
.iter()
.map(|(_, lo, hi)| midpoint(lo, hi, prec))
.collect();
let mut points: Vec<Vec<Float>> = vec![centre.clone()];
for (i, (_, lo, hi)) in boxes0.iter().enumerate() {
for end in [lo, hi] {
let mut p = centre.clone();
p[i] = end.clone();
points.push(p);
}
}
if n <= 3 {
for mask in 0..(1usize << n) {
points.push(
boxes0
.iter()
.enumerate()
.map(|(i, (_, lo, hi))| {
if mask & (1 << i) == 0 {
lo.clone()
} else {
hi.clone()
}
})
.collect(),
);
}
}
points
.into_iter()
.map(|p| {
boxes0
.iter()
.zip(p)
.map(|((v, _, _), c)| (*v, c.clone(), c))
.collect()
})
.collect()
}
fn root_exists_witness(
expr: ExprId,
pool: &ExprPool,
boxes0: &[FBox],
opts: &BoundOptions,
) -> bool {
let (prec, order) = (opts.prec, opts.order);
let floor = max_dim_width(boxes0, prec) * 2f64.powi(-SINGULARITY_BISECTION_LIMIT);
let mut w = SignWitnesses::default();
for point in seed_points(boxes0, prec) {
match point_outcome(expr, pool, &point, order, prec, true) {
PointOutcome::Zero => return true,
PointOutcome::Sign(s) => {
w.record(Some(s));
if w.both() {
return true;
}
}
PointOutcome::Unknown => {}
}
}
let mut queue: VecDeque<Vec<FBox>> = VecDeque::new();
queue.push_back(boxes0.to_vec());
let mut budget = opts.max_subdivisions;
while let Some(b) = queue.pop_front() {
if budget == 0 {
break;
}
budget -= 1;
if let Ok(r) = taylor_range(expr, pool, &b, order, prec) {
if let Some(s) = determined_sign(&r) {
w.record(Some(s));
if w.both() {
return true;
}
continue;
}
if is_exact_zero(&r) {
return true;
}
}
let centre: Vec<FBox> = b
.iter()
.map(|(v, lo, hi)| {
let m = midpoint(lo, hi, prec);
(*v, m.clone(), m)
})
.collect();
match point_outcome(expr, pool, ¢re, order, prec, false) {
PointOutcome::Zero => return true,
PointOutcome::Sign(s) => {
w.record(Some(s));
if w.both() {
return true;
}
}
PointOutcome::Unknown => {}
}
if max_dim_width(&b, prec) <= floor {
continue;
}
let (b1, b2) = split_widest(&b, prec);
queue.push_back(b1);
queue.push_back(b2);
}
false
}
pub fn verified_no_roots(
expr: ExprId,
pool: &ExprPool,
boxes: &[(ExprId, f64, f64)],
opts: &BoundOptions,
) -> Result<Verdict> {
let full = bound_on_box(expr, pool, boxes, opts)?;
if !contains_zero(full.enclosure()) {
return Ok(Verdict::True);
}
let prec = opts.prec;
let boxes0: Vec<FBox> = boxes
.iter()
.map(|(v, lo, hi)| (*v, Float::with_val(prec, lo), Float::with_val(prec, hi)))
.collect();
if root_exists_witness(expr, pool, &boxes0, opts) {
return Ok(Verdict::False);
}
Ok(Verdict::Undecided)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SignPredicate {
Positive,
Negative,
NonNegative,
NonPositive,
}
pub fn verified_sign(
expr: ExprId,
pool: &ExprPool,
boxes: &[(ExprId, f64, f64)],
predicate: SignPredicate,
opts: &BoundOptions,
) -> Result<Verdict> {
let r = bound_on_box(expr, pool, boxes, opts)?;
let lo = lb(r.enclosure());
let hi = ub(r.enclosure());
let holds_everywhere = match predicate {
SignPredicate::Positive => lo > 0,
SignPredicate::Negative => hi < 0,
SignPredicate::NonNegative => lo >= 0,
SignPredicate::NonPositive => hi <= 0,
};
if holds_everywhere {
return Ok(Verdict::True);
}
let fails_everywhere = match predicate {
SignPredicate::Positive => hi <= 0,
SignPredicate::Negative => lo >= 0,
SignPredicate::NonNegative => hi < 0,
SignPredicate::NonPositive => lo > 0,
};
if fails_everywhere {
return Ok(Verdict::False);
}
if violates_at_some_sample(expr, pool, boxes, predicate, opts)? {
return Ok(Verdict::False);
}
let strictly_signed = match predicate {
SignPredicate::Positive | SignPredicate::NonNegative => {
sign_targeted_bound(expr, pool, boxes, opts, Extremum::Min)? > 0
}
SignPredicate::Negative | SignPredicate::NonPositive => {
sign_targeted_bound(expr, pool, boxes, opts, Extremum::Max)? < 0
}
};
if strictly_signed {
return Ok(Verdict::True);
}
if let Some(v) = endpoint_series_verdict(expr, pool, boxes, predicate, opts) {
return Ok(v);
}
Ok(Verdict::Undecided)
}
fn sign_targeted_bound(
expr: ExprId,
pool: &ExprPool,
boxes: &[(ExprId, f64, f64)],
opts: &BoundOptions,
which: Extremum,
) -> Result<Float> {
let prec = opts.prec;
let boxes0: Vec<FBox> = boxes
.iter()
.map(|(v, lo, hi)| (*v, Float::with_val(prec, lo), Float::with_val(prec, hi)))
.collect();
let (bound, _, _) = extremum_search(expr, pool, &boxes0, opts, which, SearchGoal::DecideSign)?;
Ok(bound)
}
fn violates_at_some_sample(
expr: ExprId,
pool: &ExprPool,
boxes: &[(ExprId, f64, f64)],
predicate: SignPredicate,
opts: &BoundOptions,
) -> Result<bool> {
let n = boxes.len();
let mut samples: Vec<Vec<f64>> = Vec::new();
samples.push(boxes.iter().map(|(_, lo, hi)| 0.5 * (lo + hi)).collect());
for i in 0..n {
for pick_lo in [true, false] {
let mut p: Vec<f64> = boxes.iter().map(|(_, lo, hi)| 0.5 * (lo + hi)).collect();
p[i] = if pick_lo { boxes[i].1 } else { boxes[i].2 };
samples.push(p);
}
}
if n <= 3 {
for mask in 0..(1usize << n) {
let p: Vec<f64> = boxes
.iter()
.enumerate()
.map(|(i, (_, lo, hi))| if mask & (1 << i) == 0 { *lo } else { *hi })
.collect();
samples.push(p);
}
}
for point in samples {
let degenerate: Vec<(ExprId, f64, f64)> = boxes
.iter()
.zip(point.iter())
.map(|((v, _, _), &c)| (*v, c, c))
.collect();
let r = match bound_on_box(expr, pool, °enerate, opts) {
Ok(r) => r,
Err(_) => continue,
};
let lo = lb(r.enclosure());
let hi = ub(r.enclosure());
let proven_violation = match predicate {
SignPredicate::Positive => hi <= 0,
SignPredicate::Negative => lo >= 0,
SignPredicate::NonNegative => hi < 0,
SignPredicate::NonPositive => lo > 0,
};
if proven_violation {
return Ok(true);
}
}
Ok(false)
}
const MAX_VANISHING_ORDER: usize = 16;
const SERIES_TAIL_TERMS: usize = 3;
const SERIES_HALVINGS: u32 = 240;
struct EndpointSeries {
j: usize,
m: usize,
cj: ArbBall,
tail: Vec<ArbBall>,
rem: ArbBall,
span: Float,
}
fn expand_at_endpoint(
g: ExprId,
pool: &ExprPool,
var: ExprId,
ilo: &Float,
ihi: &Float,
inward: i32,
opts: &BoundOptions,
) -> Option<EndpointSeries> {
let prec = opts.prec;
let p = if inward > 0 { ilo } else { ihi };
let span = Float::with_val_round(prec, ihi - ilo, Round::Down).0;
if span <= 0 {
return None;
}
let mut derivs: Vec<ExprId> = vec![g];
let extend = |derivs: &mut Vec<ExprId>| -> bool {
let last = *derivs.last().expect("derivs is never empty");
match diff(last, var, pool) {
Ok(d) => {
derivs.push(simplify(d.value, pool).value);
true
}
Err(_) => false,
}
};
let mut found = None;
for k in 0..=MAX_VANISHING_ORDER {
if k > 0 && !extend(&mut derivs) {
return None;
}
if vanishes_exactly(derivs[k], pool, var, p) {
if !enclosure_admits_zero(derivs[k], pool, var, p, opts.order, prec) {
return None;
}
continue;
}
found = Some(k);
break;
}
let j = found?;
let m = j + 1 + SERIES_TAIL_TERMS;
while derivs.len() <= m {
if !extend(&mut derivs) {
return None;
}
}
let interval = vec![(var, ilo.clone(), ihi.clone())];
let mut sup_m = None;
for (k, &d) in derivs.iter().enumerate().take(m + 1) {
let r = bound_on_fboxes(d, pool, &interval, opts).ok()?;
if !is_finite(r.enclosure()) {
return None;
}
if k == m {
sup_m = Some(mag(r.enclosure()));
}
}
let mut fact = vec![Float::with_val(prec, 1)];
for k in 1..=m {
let next = Float::with_val(prec, &fact[k - 1] * k as u32);
fact.push(next);
}
let point = vec![(var, p.clone(), p.clone())];
let at_point = |k: usize| taylor_range(derivs[k], pool, &point, opts.order, prec).ok();
let mut cj = (at_point(j)? / from_float(&fact[j], prec))?;
if inward < 0 && j % 2 == 1 {
cj = -cj;
}
if !is_finite(&cj) {
return None;
}
let mut tail = Vec::with_capacity(m - j - 1);
for (k, fk) in fact.iter().enumerate().take(m).skip(j + 1) {
let b = (at_point(k)?.abs_ball() / from_float(fk, prec))?;
if !is_finite(&b) {
return None;
}
tail.push(b);
}
let rem = (from_float(&sup_m?, prec) / from_float(&fact[m], prec))?;
if !is_finite(&rem) {
return None;
}
Some(EndpointSeries {
j,
m,
cj,
tail,
rem,
span,
})
}
impl EndpointSeries {
fn tail_bound(&self, delta: &Float, prec: u32) -> Float {
let d = from_float(delta, prec);
let mut acc = ArbBall::from_f64(0.0, prec);
for (i, c) in self.tail.iter().enumerate() {
acc = acc + c.clone() * d.powi((i + 1) as i64);
}
acc = acc + self.rem.clone() * d.powi((self.m - self.j) as i64);
ub(&acc)
}
fn reach(&self, prec: u32) -> Option<(Float, bool)> {
let (lo, hi) = (lb(&self.cj), ub(&self.cj));
let (positive, margin) = if lo > 0 {
(true, lo)
} else if hi < 0 {
(false, Float::with_val(prec, -hi))
} else {
return None;
};
let mut delta = self.span.clone();
for _ in 0..SERIES_HALVINGS {
if margin > self.tail_bound(&delta, prec) {
return Some((delta, positive));
}
delta = Float::with_val(prec, &delta / 2u32);
if delta <= 0 {
break;
}
}
None
}
}
fn endpoint_series_verdict(
expr: ExprId,
pool: &ExprPool,
boxes: &[(ExprId, f64, f64)],
predicate: SignPredicate,
opts: &BoundOptions,
) -> Option<Verdict> {
if boxes.len() != 1 {
return None;
}
let (var, lo, hi) = boxes[0];
if lo >= hi || !lo.is_finite() || !hi.is_finite() {
return None;
}
let prec = opts.prec;
let g = match predicate {
SignPredicate::NonNegative | SignPredicate::Positive => expr,
SignPredicate::NonPositive | SignPredicate::Negative => {
pool.mul(vec![pool.integer(-1_i32), expr])
}
};
let strict = matches!(predicate, SignPredicate::Positive | SignPredicate::Negative);
let a = Float::with_val(prec, lo);
let b = Float::with_val(prec, hi);
let left = expand_at_endpoint(g, pool, var, &a, &b, 1, opts);
let right = expand_at_endpoint(g, pool, var, &a, &b, -1, opts);
for s in [&left, &right].into_iter().flatten() {
if let Some((_, false)) = s.reach(prec) {
return Some(Verdict::False);
}
}
if strict
&& [&left, &right]
.iter()
.any(|e| e.as_ref().is_some_and(|s| s.j >= 1))
{
return Some(Verdict::False);
}
let collar = |e: &Option<EndpointSeries>| -> Option<Float> {
e.as_ref()
.and_then(|s| s.reach(prec))
.filter(|(_, positive)| *positive)
.map(|(d, _)| d)
};
let dl = collar(&left);
let dr = collar(&right);
if dl.is_none() && dr.is_none() {
return None;
}
let mlo = dl.map_or_else(
|| a.clone(),
|d| Float::with_val_round(prec, &a + &d, Round::Down).0,
);
let mhi = dr.map_or_else(
|| b.clone(),
|d| Float::with_val_round(prec, &b - &d, Round::Up).0,
);
if mlo >= mhi {
return Some(Verdict::True);
}
let middle = vec![(var, mlo, mhi)];
let bound = extremum_search(
g,
pool,
&middle,
opts,
Extremum::Min,
SearchGoal::DecideSign,
)
.ok()?;
(bound.0 > 0).then_some(Verdict::True)
}
#[cfg(test)]
mod tests {
use super::*;
fn sub(pool: &ExprPool, a: ExprId, b: ExprId) -> ExprId {
pool.add(vec![a, pool.mul(vec![pool.integer(-1_i32), b])])
}
fn div(pool: &ExprPool, a: ExprId, b: ExprId) -> ExprId {
pool.mul(vec![a, pool.pow(b, pool.integer(-1_i32))])
}
use crate::kernel::Domain;
fn opts() -> BoundOptions {
BoundOptions {
order: 6,
prec: 128,
tol: 1e-9,
max_subdivisions: 4096,
}
}
fn iopts() -> IntegralOptions {
IntegralOptions {
order: 8,
prec: 128,
tol: 1e-9,
max_subdivisions: 4096,
}
}
#[test]
fn x_minus_x_is_tight_zero() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = sub(&pool, x, x);
let r = bound_on_box(e, &pool, &[(x, -5.0, 5.0)], &opts()).unwrap();
assert!(r.lower() >= -1e-9 && r.upper() <= 1e-9, "{:?}", r);
}
#[test]
fn x_times_one_minus_x_needs_subdivision_and_converges() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1_i32);
let e = pool.mul(vec![x, sub(&pool, one, x)]);
let r = bound_on_box(e, &pool, &[(x, 0.0, 1.0)], &opts()).unwrap();
assert!(r.lower() <= 1e-6, "{:?}", r);
assert!(
r.upper() >= 0.25 - 1e-6 && r.upper() <= 0.25 + 1e-6,
"{:?}",
r
);
assert!(!r.budget_exhausted);
}
#[test]
fn sin_squared_plus_cos_squared_is_one() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let s = pool.func("sin", vec![x]);
let c = pool.func("cos", vec![x]);
let e = pool.add(vec![
pool.pow(s, pool.integer(2_i32)),
pool.pow(c, pool.integer(2_i32)),
]);
let r = bound_on_box(e, &pool, &[(x, -3.0, 3.0)], &opts()).unwrap();
assert!(r.lower() <= 1.0 && r.upper() >= 1.0, "{:?}", r);
assert!(r.upper() - r.lower() < 0.5, "too wide: {:?}", r);
}
#[test]
fn taylor_bound_beats_plain_interval_eval() {
use crate::ball::IntervalEval;
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1_i32);
let e = pool.mul(vec![x, sub(&pool, one, x)]);
let r = bound_on_box(e, &pool, &[(x, 0.0, 1.0)], &opts()).unwrap();
let mut ev = IntervalEval::new(128);
ev.bind(x, ArbBall::from_midpoint_radius(0.5, 0.5, 128));
let iv = ev.eval(e, &pool).unwrap();
let iv_width = iv.rad_f64() * 2.0;
assert!(
r.upper() - r.lower() < iv_width,
"{:?} vs iv width {}",
r,
iv_width
);
}
#[test]
fn budget_exhaustion_is_reported_and_still_sound() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1_i32);
let e = pool.mul(vec![x, sub(&pool, one, x)]);
let tight = BoundOptions {
order: 1,
prec: 64,
tol: 1e-12,
max_subdivisions: 1, };
let r = bound_on_box(e, &pool, &[(x, 0.0, 1.0)], &tight).unwrap();
assert!(r.budget_exhausted);
assert!(r.lower() <= 0.0 && r.upper() >= 0.25, "{:?}", r);
}
#[test]
fn two_d_box_with_near_tangential_extremum() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let diff = sub(&pool, x, y);
let sq = pool.pow(diff, pool.integer(2_i32));
let eps = pool.rational(1_i32, 10000_i32);
let e = pool.add(vec![sq, eps]);
let r = bound_on_box(e, &pool, &[(x, -1.0, 1.0), (y, -1.0, 1.0)], &opts()).unwrap();
assert!(r.lower() >= 0.0, "unsound: {:?}", r);
assert!(r.lower() <= 1e-3, "{:?}", r);
assert!(r.upper() >= 4.0 - 1e-3, "{:?}", r);
}
#[test]
fn refuses_on_pole_that_resists_bisection() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(&pool, pool.integer(1_i32), x);
let err = bound_on_box(e, &pool, &[(x, -1.0, 1.0)], &opts()).unwrap_err();
assert_eq!(crate::errors::AlkahestError::code(&err), "E-VALIDATED-003");
}
#[test]
fn bisects_away_from_a_boundary_domain_issue() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.func("log", vec![x]);
let r = bound_on_box(e, &pool, &[(x, 0.5, 2.0)], &opts()).unwrap();
assert!(r.lower() <= 0.5_f64.ln() + 1e-6);
assert!(r.upper() >= 2.0_f64.ln() - 1e-6);
}
#[test]
fn integral_of_x_squared() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.pow(x, pool.integer(2_i32));
let r = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap();
assert!(r.lower() <= 1.0 / 3.0 && r.upper() >= 1.0 / 3.0, "{:?}", r);
assert!(r.upper() - r.lower() < 1e-6, "{:?}", r);
}
#[test]
fn integral_of_sin_over_full_period() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.func("sin", vec![x]);
let two_pi = std::f64::consts::PI * 2.0;
let r = verified_integral(e, &pool, x, 0.0, two_pi, &iopts()).unwrap();
assert!(r.lower() <= 1e-4 && r.upper() >= -1e-4, "{:?}", r);
}
#[test]
fn integral_of_exp() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.func("exp", vec![x]);
let expected = std::f64::consts::E - 1.0;
let r = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap();
assert!(r.lower() <= expected && r.upper() >= expected, "{:?}", r);
assert!(r.upper() - r.lower() < 1e-6, "{:?}", r);
}
#[test]
fn integral_degenerate_interval_is_zero() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.func("exp", vec![x]);
let r = verified_integral(e, &pool, x, 1.0, 1.0, &iopts()).unwrap();
assert_eq!(r.lower(), 0.0);
assert_eq!(r.upper(), 0.0);
assert_eq!(r.subdivisions, 0);
}
#[test]
fn integral_refuses_on_singular_integrand() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(&pool, pool.integer(1_i32), pool.func("sqrt", vec![x]));
let err = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap_err();
assert!(matches!(
crate::errors::AlkahestError::code(&err),
"E-VALIDATED-003" | "E-VALIDATED-004"
));
}
fn assert_brackets(r: &IntegralResult, exact: f64, tol: f64, label: &str) {
assert!(
r.lower() <= exact && exact <= r.upper(),
"{label}: {exact} not in [{}, {}]",
r.lower(),
r.upper()
);
assert!(
r.upper() - r.lower() <= tol,
"{label}: enclosure [{}, {}] is wider than {tol}",
r.lower(),
r.upper()
);
}
#[test]
fn integral_of_log1p_over_x_is_pi_squared_over_twelve() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(
&pool,
pool.func("log", vec![pool.add(vec![pool.integer(1_i32), x])]),
x,
);
let r = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap();
let exact = std::f64::consts::PI * std::f64::consts::PI / 12.0;
assert_brackets(&r, exact, 1e-6, "ln(1+x)/x");
}
#[test]
fn integral_of_sinc_is_twice_si_of_one() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(&pool, pool.func("sin", vec![x]), x);
let r = verified_integral(e, &pool, x, -1.0, 1.0, &iopts()).unwrap();
assert_brackets(&r, 1.892_166_140_734_366_4, 1e-6, "sin(x)/x");
}
#[test]
fn integral_of_expm1_over_x() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let num = sub(&pool, pool.func("exp", vec![x]), pool.integer(1_i32));
let e = div(&pool, num, x);
let r = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap();
assert_brackets(&r, 1.317_902_151_454_403_9, 1e-6, "(exp(x)-1)/x");
}
#[test]
fn removable_extension_agrees_with_the_regular_quadrature_away_from_zero() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(
&pool,
pool.func("log", vec![pool.add(vec![pool.integer(1_i32), x])]),
x,
);
let whole = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap();
let left = verified_integral(e, &pool, x, 0.0, 0.5, &iopts()).unwrap();
let right = verified_integral(e, &pool, x, 0.5, 1.0, &iopts()).unwrap();
assert!(
whole.lower() <= left.upper() + right.upper()
&& whole.upper() >= left.lower() + right.lower(),
"split disagrees: whole [{}, {}] vs pieces [{}, {}] + [{}, {}]",
whole.lower(),
whole.upper(),
left.lower(),
left.upper(),
right.lower(),
right.upper()
);
}
#[test]
fn genuine_pole_is_not_mistaken_for_a_removable_one() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(&pool, pool.integer(1_i32), x);
let err = verified_integral(e, &pool, x, -1.0, 1.0, &iopts()).unwrap_err();
assert_eq!(crate::errors::AlkahestError::code(&err), "E-VALIDATED-003");
}
#[test]
fn double_pole_with_a_simple_numerator_zero_is_refused() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(
&pool,
pool.func("sin", vec![x]),
pool.pow(x, pool.integer(2_i32)),
);
let err = verified_integral(e, &pool, x, -1.0, 1.0, &iopts()).unwrap_err();
assert_eq!(crate::errors::AlkahestError::code(&err), "E-VALIDATED-003");
}
#[test]
fn integrable_endpoint_singularity_is_refused_with_a_message_that_says_so() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.mul(vec![pool.integer(-1_i32), pool.func("log", vec![x])]);
let err = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap_err();
assert_eq!(crate::errors::AlkahestError::code(&err), "E-VALIDATED-003");
let msg = err.to_string();
assert!(msg.contains("the left endpoint"), "{msg}");
assert!(msg.contains("integrable singularity"), "{msg}");
}
#[test]
fn interior_singularity_is_reported_as_interior() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = div(
&pool,
pool.integer(1_i32),
sub(&pool, x, pool.rational(1_i32, 2_i32)),
);
let err = verified_integral(e, &pool, x, 0.0, 1.0, &iopts()).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("an interior point"), "{msg}");
}
#[test]
fn integral_refuses_on_infinite_bounds() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = x;
let err = verified_integral(e, &pool, x, 0.0, f64::INFINITY, &iopts()).unwrap_err();
assert_eq!(crate::errors::AlkahestError::code(&err), "E-VALIDATED-005");
}
#[test]
fn integral_refuses_when_a_greater_than_b() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let err = verified_integral(x, &pool, x, 1.0, 0.0, &iopts()).unwrap_err();
assert_eq!(crate::errors::AlkahestError::code(&err), "E-VALIDATED-005");
}
#[test]
fn no_roots_verified_true_for_shifted_square() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1_i32);
let sq = pool.pow(sub(&pool, x, pool.integer(5_i32)), pool.integer(2_i32));
let e = pool.add(vec![sq, one]);
let v = verified_no_roots(e, &pool, &[(x, -10.0, 10.0)], &opts()).unwrap();
assert_eq!(v, Verdict::True);
}
#[test]
fn no_roots_verified_false_via_sign_change() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = sub(&pool, x, pool.rational(1_i32, 2_i32));
let v = verified_no_roots(e, &pool, &[(x, 0.0, 1.0)], &opts()).unwrap();
assert_eq!(v, Verdict::False);
}
#[test]
fn no_roots_false_across_a_convex_multivariate_box() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let e = sub(&pool, x, y);
let v = verified_no_roots(e, &pool, &[(x, -1.0, 1.0), (y, -1.0, 1.0)], &opts()).unwrap();
assert_eq!(v, Verdict::False);
}
#[test]
fn no_roots_undecided_for_a_multivariate_tangential_zero() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let third = pool.rational(1_i32, 3_i32);
let dx = sub(&pool, x, third);
let dy = sub(&pool, y, third);
let e = pool.add(vec![pool.mul(vec![dx, dx]), pool.mul(vec![dy, dy])]);
let cheap = BoundOptions {
max_subdivisions: 64,
..opts()
};
let v = verified_no_roots(e, &pool, &[(x, 0.0, 1.0), (y, 0.0, 1.0)], &cheap).unwrap();
assert_eq!(v, Verdict::Undecided);
}
#[test]
fn no_roots_false_for_a_tangential_zero_at_a_point_the_search_visits() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let half = pool.rational(1_i32, 2_i32);
let dx = sub(&pool, x, half);
let dy = sub(&pool, y, half);
let e = pool.add(vec![pool.mul(vec![dx, dx]), pool.mul(vec![dy, dy])]);
let cheap = BoundOptions {
max_subdivisions: 64,
..opts()
};
let v = verified_no_roots(e, &pool, &[(x, 0.0, 1.0), (y, 0.0, 1.0)], &cheap).unwrap();
assert_eq!(v, Verdict::False);
}
#[test]
fn no_roots_false_regardless_of_the_root_count_in_the_box() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let f = sub(&pool, x2, pool.integer(2_i32));
let g = pool.mul(vec![f, pool.add(vec![x2, pool.integer(1_i32)])]);
for (expr, lo, hi, label) in [
(f, -2.0, 0.0, "x^2-2 on [-2,0] (1 root)"),
(f, 0.0, 2.0, "x^2-2 on [0,2] (1 root)"),
(f, 1.3, 1.5, "x^2-2 on [1.3,1.5] (1 root)"),
(f, -2.0, 2.0, "x^2-2 on [-2,2] (2 roots)"),
(f, -10.0, 10.0, "x^2-2 on [-10,10] (2 roots)"),
(g, -2.0, 2.0, "(x^2-2)(x^2+1) on [-2,2] (2 roots)"),
] {
let v = verified_no_roots(expr, &pool, &[(x, lo, hi)], &opts()).unwrap();
assert_eq!(v, Verdict::False, "{label}");
}
}
#[test]
fn no_roots_false_for_a_root_on_a_box_endpoint() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
for (expr, lo, hi, label) in [
(x, 0.0, 1.0, "x on [0,1] (root at the left endpoint)"),
(
sub(&pool, x, pool.integer(1_i32)),
0.0,
1.0,
"x-1 on [0,1] (root at the right endpoint)",
),
(
pool.func("sin", vec![x]),
0.0,
1.0,
"sin on [0,1] (root at the left endpoint)",
),
(
pool.func("log", vec![x]),
1.0,
2.0,
"log on [1,2] (root at the left endpoint)",
),
(
sub(&pool, pool.func("exp", vec![x]), pool.integer(1_i32)),
0.0,
1.0,
"exp-1 on [0,1] (root at the left endpoint)",
),
] {
let v = verified_no_roots(expr, &pool, &[(x, lo, hi)], &opts()).unwrap();
assert_eq!(v, Verdict::False, "{label}");
}
}
#[test]
fn no_roots_undecided_when_the_endpoint_enclosure_only_straddles_zero() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let ten_to_40 =
rug::Integer::from_str_radix("10000000000000000000000000000000000000000", 10).unwrap();
let tiny = pool.rational(rug::Integer::from(1), ten_to_40);
let e = pool.add(vec![pool.func("exp", vec![x]), pool.integer(-1_i32), tiny]);
let v = verified_no_roots(e, &pool, &[(x, 0.0, 1.0)], &opts()).unwrap();
assert_eq!(v, Verdict::Undecided);
}
#[test]
fn no_roots_verdict_is_never_wrong_on_a_randomised_sweep() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let mut seed = 0x9E37_79B9_7F4A_7C15_u64;
let mut next = move |n: i64| -> i64 {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 33) as i64 % n
};
let cheap = BoundOptions {
max_subdivisions: 64,
tol: 1e-6,
..opts()
};
for _ in 0..120 {
let (r1, r2) = (next(17) - 8, next(17) - 8);
let lo_q = next(17) - 8;
let hi_q = lo_q + 1 + next(8);
let (lo, hi) = (lo_q as f64 / 4.0, hi_q as f64 / 4.0);
let d1 = sub(&pool, x, pool.rational(r1, 4_i64));
let d2 = sub(&pool, x, pool.rational(r2, 4_i64));
for (expr, roots) in [(d1, vec![r1]), (pool.mul(vec![d1, d2]), vec![r1, r2])] {
let has_root = roots.iter().any(|r| *r >= lo_q && *r <= hi_q);
let v = verified_no_roots(expr, &pool, &[(x, lo, hi)], &cheap).unwrap();
match v {
Verdict::True => assert!(
!has_root,
"certified root-free but roots {roots:?}/4 are in [{lo}, {hi}]"
),
Verdict::False => assert!(
has_root,
"certified a root but roots {roots:?}/4 are outside [{lo}, {hi}]"
),
Verdict::Undecided => {}
}
}
}
}
#[test]
fn no_roots_stays_true_where_it_was_true() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
for (expr, lo, hi) in [
(pool.add(vec![x2, pool.integer(1_i32)]), -10.0, 10.0),
(pool.add(vec![x2, pool.integer(2_i32)]), -2.0, 2.0),
(pool.func("exp", vec![x]), -5.0, 5.0),
] {
let v = verified_no_roots(expr, &pool, &[(x, lo, hi)], &opts()).unwrap();
assert_eq!(v, Verdict::True);
}
}
#[test]
fn no_roots_undecided_for_a_double_root_that_cannot_be_witnessed() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let d = sub(&pool, x, pool.rational(1_i32, 3_i32));
let e = pool.mul(vec![d, d]);
let v = verified_no_roots(e, &pool, &[(x, 0.0, 2.0)], &opts()).unwrap();
assert_eq!(v, Verdict::Undecided);
}
#[test]
fn no_roots_false_for_a_double_root_proven_at_a_point() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let d = sub(&pool, x, pool.integer(1_i32));
let e = pool.mul(vec![d, d]);
for (lo, hi) in [(0.0, 2.0), (0.0, 1.0), (1.0, 3.0)] {
let v = verified_no_roots(e, &pool, &[(x, lo, hi)], &opts()).unwrap();
assert_eq!(v, Verdict::False, "(x-1)^2 on [{lo}, {hi}]");
}
}
#[test]
fn no_roots_undecided_for_a_quartic_double_root() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let d = sub(&pool, pool.pow(x, pool.integer(2_i32)), pool.integer(1_i32));
let e = pool.mul(vec![d, d]);
let v = verified_no_roots(e, &pool, &[(x, -2.0, 2.0)], &opts()).unwrap();
assert_eq!(v, Verdict::Undecided);
}
#[test]
fn no_roots_near_tangential_case_stays_true() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let sq = pool.pow(sub(&pool, x, pool.integer(1_i32)), pool.integer(2_i32));
let eps = pool.rational(1_i32, 1_000_000_i32);
let e = pool.add(vec![sq, eps]);
let tight = BoundOptions {
order: 8,
prec: 128,
tol: 1e-10,
max_subdivisions: 8192,
};
let v = verified_no_roots(e, &pool, &[(x, 0.0, 2.0)], &tight).unwrap();
assert_eq!(v, Verdict::True);
}
#[test]
fn sign_positive_verified_true() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.func("exp", vec![x]);
let v = verified_sign(
e,
&pool,
&[(x, -5.0, 5.0)],
SignPredicate::Positive,
&opts(),
)
.unwrap();
assert_eq!(v, Verdict::True);
}
#[test]
fn sign_positive_verified_false() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = x; let v = verified_sign(
e,
&pool,
&[(x, -1.0, 1.0)],
SignPredicate::Positive,
&opts(),
)
.unwrap();
assert_eq!(v, Verdict::False);
}
#[test]
fn sign_false_is_certified_by_a_point_witness() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = sub(&pool, x, pool.rational(1_i32, 2_i32));
let v =
verified_sign(e, &pool, &[(x, 0.0, 1.0)], SignPredicate::Positive, &opts()).unwrap();
assert_eq!(v, Verdict::False);
}
#[test]
fn sign_undecided_when_neither_can_be_established() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let shifted = sub(&pool, x, pool.rational(1_i32, 3_i32));
let tiny = pool.rational(1_i32, 1_000_000_000_000_i64);
let e = sub(&pool, pool.mul(vec![shifted, shifted]), tiny);
let cheap = BoundOptions {
order: 4,
prec: 128,
tol: 1e-9,
max_subdivisions: 2,
};
let v = verified_sign(
e,
&pool,
&[(x, -1.0, 1.0)],
SignPredicate::NonNegative,
&cheap,
)
.unwrap();
assert_eq!(v, Verdict::Undecided);
}
#[test]
fn dense_sampling_cross_check_polynomial() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = sub(
&pool,
pool.pow(x, pool.integer(3_i32)),
pool.mul(vec![pool.integer(3_i32), x]),
); let r = bound_on_box(e, &pool, &[(x, -2.5, 2.5)], &opts()).unwrap();
for i in 0..=500 {
let t = -2.5 + 5.0 * (i as f64) / 500.0;
let v = t.powi(3) - 3.0 * t;
assert!(
v >= r.lower() - 1e-6 && v <= r.upper() + 1e-6,
"x={t} f={v} escaped {:?}",
r
);
}
}
fn cusa_huygens(pool: &ExprPool, x: ExprId) -> ExprId {
let two_plus_cos = pool.add(vec![pool.integer(2_i32), pool.func("cos", vec![x])]);
sub(
pool,
pool.mul(vec![x, two_plus_cos]),
pool.mul(vec![pool.integer(3_i32), pool.func("sin", vec![x])]),
)
}
#[test]
fn tight_at_the_endpoint_is_certified_by_the_series_split() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let v = verified_sign(
cusa_huygens(&pool, x),
&pool,
&[(x, 0.0, 1.5)],
SignPredicate::NonNegative,
&opts(),
)
.unwrap();
assert_eq!(v, Verdict::True);
}
#[test]
fn reversing_a_tight_inequality_is_refuted_not_certified() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let reversed = pool.mul(vec![pool.integer(-1_i32), cusa_huygens(&pool, x)]);
let v = verified_sign(
reversed,
&pool,
&[(x, 0.0, 1.5)],
SignPredicate::NonNegative,
&opts(),
)
.unwrap();
assert_eq!(v, Verdict::False);
}
#[test]
fn a_violation_only_next_to_the_endpoint_is_never_certified() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = sub(
&pool,
pool.pow(x, pool.integer(3_i32)),
pool.mul(vec![
pool.rational(1_i32, 1000_i32),
pool.pow(x, pool.integer(2_i32)),
]),
);
let v = verified_sign(
e,
&pool,
&[(x, 0.0, 1.5)],
SignPredicate::NonNegative,
&opts(),
)
.unwrap();
assert_ne!(v, Verdict::True);
}
#[test]
fn tightness_in_the_interior_is_still_out_of_reach() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let shifted = sub(&pool, x, pool.rational(7_i32, 10_i32));
let e = pool.mul(vec![
shifted,
shifted,
pool.add(vec![x, pool.integer(1_i32)]),
]);
let v = verified_sign(
e,
&pool,
&[(x, 0.0, 1.5)],
SignPredicate::NonNegative,
&opts(),
)
.unwrap();
assert_eq!(v, Verdict::Undecided);
}
#[test]
fn strict_positivity_is_false_where_the_function_vanishes_exactly() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.mul(vec![x, x]);
assert_eq!(
verified_sign(e, &pool, &[(x, 0.0, 1.0)], SignPredicate::Positive, &opts()).unwrap(),
Verdict::False
);
assert_eq!(
verified_sign(
e,
&pool,
&[(x, 0.0, 1.0)],
SignPredicate::NonNegative,
&opts()
)
.unwrap(),
Verdict::True
);
}
#[test]
fn an_unreachable_tolerance_terminates_instead_of_spinning() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = sub(
&pool,
pool.mul(vec![
pool.integer(1_000_000_000_000_i64),
pool.func("sin", vec![x]),
]),
pool.mul(vec![pool.integer(636_619_772_368_i64), x]),
);
let tight = BoundOptions {
tol: 1e-40,
..opts()
};
let r = bound_on_box(e, &pool, &[(x, 0.0, 1.5)], &tight).unwrap();
assert!(r.lower() <= 0.0 && r.upper() >= 0.0, "{r:?}");
}
#[test]
fn a_sharp_rational_constant_is_certified_rather_than_timing_out() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = sub(
&pool,
pool.mul(vec![
pool.integer(1_000_000_000_000_i64),
pool.func("sin", vec![x]),
]),
pool.mul(vec![pool.integer(636_619_772_368_i64), x]),
);
let v = verified_sign(
e,
&pool,
&[(x, 0.0, 1.5)],
SignPredicate::NonNegative,
&opts(),
)
.unwrap();
assert_eq!(v, Verdict::True);
}
}