use super::taylor::{taylor_range, TaylorContext, MAX_ORDER};
use super::{contains_zero, from_bounds, from_float, is_finite, lb, ub, width, ValidatedError};
use crate::ball::ArbBall;
use crate::kernel::{ExprId, ExprPool};
use rug::float::Round;
use rug::Float;
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 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 = Float::with_val(prec, Float::with_val(prec, lo + hi) / 2u32);
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> {
if boxes.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 boxes0: Vec<FBox> = boxes
.iter()
.map(|(v, lo, hi)| (*v, Float::with_val(prec, lo), Float::with_val(prec, hi)))
.collect();
let (lo_bound, used_lo, exhausted_lo) =
extremum_search(expr, pool, &boxes0, opts, Extremum::Min)?;
let (hi_bound, used_hi, exhausted_hi) =
extremum_search(expr, pool, &boxes0, opts, Extremum::Max)?;
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,
}
fn extremum_search(
expr: ExprId,
pool: &ExprPool,
boxes0: &[FBox],
opts: &BoundOptions,
which: Extremum,
) -> 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 best_ub: Option<Float> = None;
let mut subdivisions = 0usize;
let mut exhausted = false;
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);
if let Some(ub_best) = &best_ub {
if &key > ub_best {
continue;
}
let gap = Float::with_val(prec, ub_best - &key);
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 {
active.push((key, b));
let stuck = active
.iter()
.all(|(_, bx)| max_dim_width(bx, prec) <= floor);
if stuck {
exhausted = true;
break;
}
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())
.fold(None::<Float>, |acc, k| {
Some(match acc {
Some(cur) if cur <= k => cur,
_ => k,
})
})
.or_else(|| best_ub.clone())
.ok_or_else(|| ValidatedError::InvalidInput {
what: "no enclosure was produced".into(),
})?;
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)
}
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, b_f)];
let mut total: Option<ArbBall> = None;
let mut subdivisions = 0usize;
let mut exhausted = false;
while let Some((lo, hi)) = stack.pop() {
match local_integral(expr, pool, var, &lo, &hi, opts.order, prec) {
Ok(piece) => {
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 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 = Float::with_val(prec, Float::with_val(prec, &lo + &hi) / 2u32);
stack.push((mid.clone(), hi));
stack.push((lo, mid));
}
}
Err(e) if is_recoverable_domain_issue(&e) => {
let piece_w = Float::with_val(prec, &hi - &lo).to_f64_round(Round::Up);
if subdivisions >= opts.max_subdivisions || piece_w <= floor {
return Err(e);
}
subdivisions += 1;
let mid = Float::with_val(prec, Float::with_val(prec, &lo + &hi) / 2u32);
stack.push((mid.clone(), hi));
stack.push((lo, mid));
}
Err(e) => return Err(e),
}
}
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
}
}
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);
}
if boxes.len() == 1 {
let (v, lo, hi) = boxes[0];
if lo < hi {
let flo = bound_on_box(expr, pool, &[(v, lo, lo)], opts);
let fhi = bound_on_box(expr, pool, &[(v, hi, hi)], opts);
if let (Ok(flo), Ok(fhi)) = (flo, fhi) {
if let (Some(slo), Some(shi)) = (
determined_sign(flo.enclosure()),
determined_sign(fhi.enclosure()),
) {
if slo != shi {
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);
}
Ok(Verdict::Undecided)
}
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)
}
#[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"
));
}
#[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_undecided_when_enclosure_straddles_zero_without_sign_change() {
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::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
);
}
}
}