use crate::kernel::expr::PredicateKind;
use crate::kernel::{ExprData, ExprId, ExprPool};
use crate::primitive::PrimitiveRegistry;
use rug::{float::Round, ops::Pow, Float};
use std::collections::HashMap;
use std::fmt;
use std::sync::OnceLock;
pub const DEFAULT_PREC: u32 = 128;
#[derive(Clone, Debug)]
pub struct ArbBall {
pub mid: Float,
pub rad: Float,
pub prec: u32,
}
impl ArbBall {
pub fn new(prec: u32) -> Self {
ArbBall {
mid: Float::new(prec),
rad: Float::new(prec),
prec,
}
}
pub fn from_f64(v: f64, prec: u32) -> Self {
let mid = Float::with_val(prec, v);
let rad = Float::with_val(prec, 0.0);
ArbBall { mid, rad, prec }
}
pub fn from_midpoint_radius(mid: f64, rad: f64, prec: u32) -> Self {
ArbBall {
mid: Float::with_val(prec, mid),
rad: Float::with_val(prec, rad.abs()),
prec,
}
}
pub fn from_integer(n: &rug::Integer, prec: u32) -> Self {
ArbBall {
mid: Float::with_val(prec, n),
rad: Float::with_val(prec, 0.0),
prec,
}
}
pub fn from_rational(r: &rug::Rational, prec: u32) -> Self {
let mid = Float::with_val(prec, r);
let exact = Float::with_val(prec * 2, r);
let diff = Float::with_val(prec, &exact - &mid).abs();
ArbBall {
mid,
rad: diff,
prec,
}
}
pub fn infinity(prec: u32) -> Self {
let inf = Float::with_val(prec, f64::INFINITY);
ArbBall {
mid: Float::new(prec),
rad: inf,
prec,
}
}
pub fn is_exact(&self) -> bool {
self.mid.is_finite() && self.rad == 0
}
fn is_indeterminate(&self) -> bool {
self.mid.is_nan() || self.rad.is_nan() || !self.mid.is_finite() || self.rad.is_infinite()
}
fn sanitized(self) -> Self {
if self.is_indeterminate() {
return ArbBall::infinity(self.prec);
}
self
}
pub fn contains(&self, v: f64) -> bool {
if self.is_indeterminate() {
return true;
}
let v = Float::with_val(self.prec, v);
let lo = Float::with_val(self.prec, &self.mid - &self.rad);
let hi = Float::with_val(self.prec, &self.mid + &self.rad);
v >= lo && v <= hi
}
pub fn lo(&self) -> Float {
if self.is_indeterminate() {
return Float::with_val(self.prec, f64::NEG_INFINITY);
}
Float::with_val_round(self.prec, &self.mid - &self.rad, Round::Down).0
}
pub fn hi(&self) -> Float {
if self.is_indeterminate() {
return Float::with_val(self.prec, f64::INFINITY);
}
Float::with_val_round(self.prec, &self.mid + &self.rad, Round::Up).0
}
pub fn mid_f64(&self) -> f64 {
self.mid.to_f64()
}
pub fn rad_f64(&self) -> f64 {
self.rad.to_f64()
}
fn add_rounding_error(&mut self) {
if self.mid.is_infinite() || self.mid.is_nan() || self.rad.is_nan() {
self.mid = Float::new(self.prec);
self.rad = Float::with_val(self.prec, f64::INFINITY);
return;
}
let mut scale = Float::with_val(
self.prec + 32,
Float::with_val(self.prec + 32, self.mid.abs_ref()) + &self.rad,
);
scale >>= self.prec.saturating_sub(2);
self.rad += &scale;
}
}
impl fmt::Display for ArbBall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{:.6} ± {:.2e}]", self.mid.to_f64(), self.rad.to_f64())
}
}
impl PartialEq for ArbBall {
fn eq(&self, other: &Self) -> bool {
self.mid == other.mid && self.rad == other.rad
}
}
impl std::ops::Add for ArbBall {
type Output = Self;
fn add(self, rhs: Self) -> Self {
let prec = self.prec.max(rhs.prec);
let mid = Float::with_val(prec, &self.mid + &rhs.mid);
let rad = Float::with_val(prec, Float::with_val(prec + 32, &self.rad + &rhs.rad));
let mut out = ArbBall { mid, rad, prec };
out.add_rounding_error();
out.sanitized()
}
}
impl std::ops::Sub for ArbBall {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
let prec = self.prec.max(rhs.prec);
let mid = Float::with_val(prec, &self.mid - &rhs.mid);
let rad = Float::with_val(prec, Float::with_val(prec + 32, &self.rad + &rhs.rad));
let mut out = ArbBall { mid, rad, prec };
out.add_rounding_error();
out.sanitized()
}
}
impl std::ops::Mul for ArbBall {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
let prec = self.prec.max(rhs.prec);
let mid = Float::with_val(prec, &self.mid * &rhs.mid);
let work = prec + 32;
let ma = Float::with_val(work, self.mid.abs_ref());
let mb = Float::with_val(work, rhs.mid.abs_ref());
let rad = Float::with_val(
prec,
Float::with_val(work, &ma * &rhs.rad)
+ Float::with_val(work, &mb * &self.rad)
+ Float::with_val(work, &self.rad * &rhs.rad),
);
let mut out = ArbBall { mid, rad, prec };
out.add_rounding_error();
out.sanitized()
}
}
impl std::ops::Neg for ArbBall {
type Output = Self;
fn neg(self) -> Self {
ArbBall {
mid: -self.mid,
rad: self.rad,
prec: self.prec,
}
.sanitized()
}
}
impl std::ops::Div for ArbBall {
type Output = Option<Self>;
fn div(self, rhs: Self) -> Option<Self> {
if rhs.contains(0.0) {
return None; }
let prec = self.prec.max(rhs.prec);
let lo_rhs = rhs.lo();
let hi_rhs = rhs.hi();
let work = prec.saturating_add(32);
let quot = |a: &Float, b: &Float| Float::with_val(work, a) / Float::with_val(work, b);
let corners = [
quot(&self.lo(), &lo_rhs),
quot(&self.lo(), &hi_rhs),
quot(&self.hi(), &lo_rhs),
quot(&self.hi(), &hi_rhs),
];
if corners.iter().any(|c| c.is_nan()) {
return None;
}
let min = corners
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap()
.clone();
let max = corners
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap()
.clone();
Some(ArbBall::from_endpoints(&min, &max, prec))
}
}
impl ArbBall {
pub fn powi(&self, n: i64) -> Self {
if n == 0 {
return ArbBall::from_f64(1.0, self.prec);
}
if n < 0 {
let pos = self.powi(-n);
return (ArbBall::from_f64(1.0, self.prec) / pos)
.unwrap_or_else(|| ArbBall::infinity(self.prec));
}
let prec = self.prec;
let (lo, hi) = (self.lo(), self.hi());
let Ok(exp) = u32::try_from(n) else {
return ArbBall::infinity(prec);
};
if !(lo.is_finite() && hi.is_finite()) {
return ArbBall::infinity(prec);
}
let work = prec + 32;
let a = Float::with_val(work, Float::with_val(work, &lo).pow(exp));
let b = Float::with_val(work, Float::with_val(work, &hi).pow(exp));
let (mut min, max) = if a <= b { (a, b) } else { (b, a) };
if exp % 2 == 0 && lo <= 0 && hi >= 0 {
min = Float::new(work);
}
ArbBall::from_endpoints(&min, &max, prec)
}
pub fn pow_f(&self, exp: &ArbBall) -> Self {
let prec = self.prec;
let lo = self.lo();
let hi = self.hi();
let e_lo = exp.lo();
let e_hi = exp.hi();
if !(lo.is_finite() && hi.is_finite() && e_lo.is_finite() && e_hi.is_finite()) {
return ArbBall::infinity(prec);
}
if lo > 0 || (lo == 0 && e_lo >= 0) {
let work = prec + 32;
let corner =
|b: &Float, e: &Float| Float::with_val(work, b).pow(Float::with_val(work, e));
let corners = [
corner(&lo, &e_lo),
corner(&lo, &e_hi),
corner(&hi, &e_lo),
corner(&hi, &e_hi),
];
if corners.iter().any(|c| !c.is_finite()) {
return ArbBall::infinity(prec);
}
let min = corners
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap()
.clone();
let max = corners
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap()
.clone();
return ArbBall::from_endpoints(&min, &max, prec);
}
if exp.is_exact() && e_lo.is_integer() {
if let Some(n) = e_lo.to_integer().and_then(|n| n.to_i64()) {
return self.powi(n);
}
}
ArbBall::infinity(prec) }
pub fn sin(&self) -> Self {
let prec = self.prec;
let mid = Float::with_val(prec, self.mid.clone().sin());
let rad = self.rad.clone();
let mut b = ArbBall { mid, rad, prec };
b.add_rounding_error();
b
}
pub fn cos(&self) -> Self {
let prec = self.prec;
let mid = Float::with_val(prec, self.mid.clone().cos());
let rad = self.rad.clone();
let mut b = ArbBall { mid, rad, prec };
b.add_rounding_error();
b
}
pub fn exp(&self) -> Self {
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).exp();
let hi = Float::with_val(work, self.hi()).exp();
ArbBall::from_endpoints(&lo, &hi, prec)
}
pub fn log(&self) -> Option<Self> {
if self.lo() <= 0 {
return None; }
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).ln();
let hi = Float::with_val(work, self.hi()).ln();
Some(ArbBall::from_endpoints(&lo, &hi, prec))
}
pub fn sqrt(&self) -> Option<Self> {
if self.lo() < 0 {
return None;
}
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).sqrt();
let hi = Float::with_val(work, self.hi()).sqrt();
Some(ArbBall::from_endpoints(&lo, &hi, prec))
}
pub fn tan(&self) -> Option<Self> {
let prec = self.prec;
let lo = self.lo();
let hi = self.hi();
if !(lo.is_finite() && hi.is_finite()) {
return None;
}
if self.cos().contains(0.0) {
return Some(ArbBall::infinity(prec));
}
let work = prec + 32;
let lo_tan = Float::with_val(work, &lo).tan();
let hi_tan = Float::with_val(work, &hi).tan();
Some(ArbBall::from_endpoints(&lo_tan, &hi_tan, prec))
}
pub fn sinh(&self) -> Self {
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).sinh();
let hi = Float::with_val(work, self.hi()).sinh();
ArbBall::from_endpoints(&lo, &hi, prec)
}
pub fn cosh(&self) -> Self {
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).cosh();
let hi = Float::with_val(work, self.hi()).cosh();
let (min_val, max_val) = if self.lo() <= 0 && self.hi() >= 0 {
let cosh_lo = lo.clone();
let cosh_hi = hi.clone();
let min = Float::with_val(work, 1_f64);
let max = if cosh_lo > cosh_hi { cosh_lo } else { cosh_hi };
(min, max)
} else if lo < hi {
(lo, hi)
} else {
(hi, lo)
};
ArbBall::from_endpoints(&min_val, &max_val, prec)
}
pub fn tanh(&self) -> Self {
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).tanh();
let hi = Float::with_val(work, self.hi()).tanh();
ArbBall::from_endpoints(&lo, &hi, prec)
}
pub fn asin(&self) -> Option<Self> {
if self.lo() < -1 || self.hi() > 1 {
return None;
}
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).asin();
let hi = Float::with_val(work, self.hi()).asin();
Some(ArbBall::from_endpoints(&lo, &hi, prec))
}
pub fn acos(&self) -> Option<Self> {
if self.lo() < -1 || self.hi() > 1 {
return None;
}
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).acos();
let hi = Float::with_val(work, self.hi()).acos();
Some(ArbBall::from_endpoints(&hi, &lo, prec))
}
pub fn atan(&self) -> Self {
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).atan();
let hi = Float::with_val(work, self.hi()).atan();
ArbBall::from_endpoints(&lo, &hi, prec)
}
pub fn asinh(&self) -> Self {
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).asinh();
let hi = Float::with_val(work, self.hi()).asinh();
ArbBall::from_endpoints(&lo, &hi, prec)
}
pub fn acosh(&self) -> Option<Self> {
if self.lo() < 1 {
return None;
}
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).acosh();
let hi = Float::with_val(work, self.hi()).acosh();
Some(ArbBall::from_endpoints(&lo, &hi, prec))
}
pub fn atanh(&self) -> Option<Self> {
if self.lo() <= -1 || self.hi() >= 1 {
return None;
}
let prec = self.prec;
let work = prec + 32;
let lo = Float::with_val(work, self.lo()).atanh();
let hi = Float::with_val(work, self.hi()).atanh();
Some(ArbBall::from_endpoints(&lo, &hi, prec))
}
pub fn erf(&self) -> Self {
let prec = self.prec;
let mid = Float::with_val(prec, self.mid.clone().erf());
let rad = Float::with_val(prec, &self.rad * &erf_lipschitz(prec));
let mut b = ArbBall { mid, rad, prec };
b.add_rounding_error();
b
}
pub fn erfc(&self) -> Self {
let prec = self.prec;
let mid = Float::with_val(prec, self.mid.clone().erfc());
let rad = Float::with_val(prec, &self.rad * &erf_lipschitz(prec));
let mut b = ArbBall { mid, rad, prec };
b.add_rounding_error();
b
}
fn from_point(v: &Float, prec: u32) -> Self {
let mid = Float::with_val(prec, v);
let rad = Float::with_val(prec, Float::with_val(prec + 32, v - &mid).abs());
let mut b = ArbBall { mid, rad, prec };
b.add_rounding_error();
b
}
fn from_endpoints(lo: &Float, hi: &Float, prec: u32) -> Self {
let mid = Float::with_val(prec, Float::with_val(prec + 32, lo + hi) / 2u32);
let rad = Float::with_val(prec, Float::with_val(prec + 32, hi - lo) / 2u32).abs();
let mut b = ArbBall { mid, rad, prec };
b.add_rounding_error();
b.sanitized()
}
pub fn lambert_w0(&self) -> Option<Self> {
let prec = self.prec;
let (low, _) = lambert_w0_bracket(&self.lo(), prec)?;
let (_, high) = lambert_w0_bracket(&self.hi(), prec)?;
Some(ArbBall::from_endpoints(&low, &high, prec))
}
pub fn gamma(&self) -> Option<Self> {
let prec = self.prec;
let a = self.lo();
let b = self.hi();
if !(a.is_finite() && b.is_finite()) || a <= 0 {
return None;
}
let work = prec + 32;
let ga = ArbBall::from_point(&Float::with_val(work, &a).gamma(), prec);
let gb = ArbBall::from_point(&Float::with_val(work, &b).gamma(), prec);
let mut psi_a = Float::with_val(work, &a);
psi_a.digamma_mut();
let mut psi_b = Float::with_val(work, &b);
psi_b.digamma_mut();
let width = ArbBall::from_point(&Float::with_val(work, &b - &a), prec);
let upper = {
let (x, y) = (ga.hi(), gb.hi());
if x > y {
x
} else {
y
}
};
let ta_far = ga.clone() + ga.clone() * ArbBall::from_point(&psi_a, prec) * width.clone();
let tb_far = gb.clone() - gb.clone() * ArbBall::from_point(&psi_b, prec) * width;
let line_min = |near: Float, far: Float| if near < far { near } else { far };
let from_a = line_min(ga.lo(), ta_far.lo());
let from_b = line_min(gb.lo(), tb_far.lo());
let mut lower = if from_a > from_b { from_a } else { from_b };
if lower < 0 {
lower = Float::new(prec);
}
Some(ArbBall::from_endpoints(&lower, &upper, prec))
}
pub fn digamma(&self) -> Option<Self> {
let prec = self.prec;
let lo = self.lo();
let hi = self.hi();
if !(lo.is_finite() && hi.is_finite()) {
return None;
}
let k_min = Float::with_val(prec, lo.ceil_ref());
if k_min <= hi && k_min <= 0 {
return None;
}
let work = prec + 32;
let mut flo = Float::with_val(work, &lo);
flo.digamma_mut();
let mut fhi = Float::with_val(work, &hi);
fhi.digamma_mut();
Some(ArbBall::from_endpoints(&flo, &fhi, prec))
}
pub fn bessel_jn(&self, n: i32) -> Self {
let prec = self.prec;
let mut mid = Float::with_val(prec, &self.mid);
mid.jn_mut(n);
let mut b = ArbBall {
mid,
rad: self.rad.clone(),
prec,
};
b.add_rounding_error();
b
}
pub fn abs_ball(&self) -> Self {
let prec = self.prec;
if self.lo() <= 0 && self.hi() >= 0 {
let max_abs = self.lo().abs().max(&self.hi().abs()).clone();
ArbBall {
mid: max_abs.clone() / 2_f64,
rad: max_abs / 2_f64,
prec,
}
.sanitized()
} else {
let mid = Float::with_val(prec, self.mid.clone().abs());
let rad = self.rad.clone();
let mut b = ArbBall { mid, rad, prec };
b.add_rounding_error();
b
}
}
pub fn floor_ball(&self) -> Self {
let prec = self.prec;
let lo_floor = Float::with_val(prec, self.lo().floor());
let hi_floor = Float::with_val(prec, self.hi().floor());
let diff = Float::with_val(prec, &hi_floor - &lo_floor);
let sum = Float::with_val(prec, &lo_floor + &hi_floor);
let mut b = ArbBall {
mid: sum / 2_f64,
rad: diff / 2_f64,
prec,
};
b.add_rounding_error();
b
}
pub fn ceil_ball(&self) -> Self {
let prec = self.prec;
let lo_ceil = Float::with_val(prec, self.lo().ceil());
let hi_ceil = Float::with_val(prec, self.hi().ceil());
let diff = Float::with_val(prec, &hi_ceil - &lo_ceil);
let sum = Float::with_val(prec, &lo_ceil + &hi_ceil);
let mut b = ArbBall {
mid: sum / 2_f64,
rad: diff / 2_f64,
prec,
};
b.add_rounding_error();
b
}
pub fn fresnel_s(&self) -> Option<Self> {
self.fresnel(true)
}
pub fn fresnel_c(&self) -> Option<Self> {
self.fresnel(false)
}
fn fresnel(&self, sine: bool) -> Option<Self> {
let prec = self.prec;
let (s, c) = crate::primitive::fresnel::fresnel_pair_ball(&self.mid, prec)?;
let point = if sine { s } else { c };
let mut b = ArbBall {
mid: point.mid,
rad: Float::with_val(prec, &self.rad + &point.rad),
prec,
};
b.add_rounding_error();
Some(b)
}
pub fn dilog(&self) -> Option<Self> {
let prec = self.prec;
let lo = crate::primitive::polylog::dilog_ball_point(&self.lo(), prec)?;
let hi = crate::primitive::polylog::dilog_ball_point(&self.hi(), prec)?;
let low = lo.lo();
let high = hi.hi();
let sum = Float::with_val(prec, &low + &high);
let diff = Float::with_val(prec, &high - &low);
let mut b = ArbBall {
mid: sum / 2_f64,
rad: Float::with_val(prec, diff / 2_f64).abs(),
prec,
};
b.add_rounding_error();
Some(b)
}
pub fn trigamma(&self) -> Option<Self> {
let lo = self.lo().to_f64();
let hi = self.hi().to_f64();
if !(lo.is_finite() && hi.is_finite()) || lo <= 0.0 {
return None;
}
let prec = self.prec;
let work = prec + 32;
let flo = crate::special::trigamma(&Float::with_val(work, self.lo()))?;
let fhi = crate::special::trigamma(&Float::with_val(work, self.hi()))?;
let sum = Float::with_val(prec, &flo + &fhi);
let diff = Float::with_val(prec, &flo - &fhi);
let mut b = ArbBall {
mid: sum / 2_f64,
rad: Float::with_val(prec, diff / 2_f64).abs(),
prec,
};
b.add_rounding_error();
Some(b)
}
}
fn erf_lipschitz(prec: u32) -> Float {
let work = prec + 32;
let two_over_sqrt_pi = Float::with_val(
work,
2u32 / Float::with_val(work, rug::float::Constant::Pi).sqrt(),
);
let mut v = Float::with_val(prec, &two_over_sqrt_pi);
if v < two_over_sqrt_pi {
v.next_up();
}
v
}
fn lambert_w0_bracket(x: &Float, prec: u32) -> Option<(Float, Float)> {
if !x.is_finite() {
return None;
}
let work = prec + 64;
let minus_one = Float::with_val(work, -1);
let xf = x.to_f64();
let mut w = match (xf.is_finite(), crate::special::lambert_w0(xf)) {
(true, Some(v)) if v.is_finite() => Float::with_val(work, v),
_ if *x > 1 => {
let l = Float::with_val(work, x).ln();
let ll = Float::with_val(work, l.clone().ln());
l - ll
}
_ => Float::with_val(work, 0),
};
for _ in 0..48 {
let ew = Float::with_val(work, w.clone().exp());
let num = Float::with_val(work, Float::with_val(work, &w * &ew) - x);
let den = Float::with_val(work, &ew * Float::with_val(work, &w + 1u32));
if den == 0 || !den.is_finite() {
break;
}
let step = Float::with_val(work, &num / &den);
if !step.is_finite() {
break;
}
let next = Float::with_val(work, &w - &step);
w = if next < minus_one {
Float::with_val(work, &w + &minus_one) / 2u32
} else {
next
};
if step.is_zero() {
break;
}
}
let g = |v: &Float| -> ArbBall {
let vb = ArbBall::from_point(v, work);
vb.clone() * vb.exp()
};
let xb = ArbBall::from_point(x, work);
let (x_lo, x_hi) = (xb.lo(), xb.hi());
let mut unit = Float::with_val(work, w.clone().abs() + 1u32);
unit >>= work.saturating_sub(4);
let mut low: Option<Float> = None;
let mut delta = unit.clone();
for _ in 0..400 {
let mut v = Float::with_val(work, &w - &delta);
if v < minus_one {
v = minus_one.clone();
}
if g(&v).hi() <= x_lo {
low = Some(v);
break;
}
if v == minus_one {
break;
}
delta *= 2u32;
}
let low = match low {
Some(v) => v,
None if g(&minus_one).hi() <= x_lo => minus_one.clone(),
None => return None,
};
let mut high: Option<Float> = None;
let mut delta = unit;
for _ in 0..400 {
let u = Float::with_val(work, &w + &delta);
if u >= minus_one && g(&u).lo() >= x_hi {
high = Some(u);
break;
}
delta *= 2u32;
}
let high = high?;
Some((
Float::with_val(prec, ArbBall::from_point(&low, prec).lo()),
Float::with_val(prec, ArbBall::from_point(&high, prec).hi()),
))
}
#[derive(Clone, Debug)]
pub struct AcbBall {
pub re: ArbBall,
pub im: ArbBall,
}
impl AcbBall {
pub fn from_real(re: ArbBall) -> Self {
let prec = re.prec;
AcbBall {
re,
im: ArbBall::new(prec),
}
}
pub fn from_f64(re: f64, im: f64, prec: u32) -> Self {
AcbBall {
re: ArbBall::from_f64(re, prec),
im: ArbBall::from_f64(im, prec),
}
}
pub fn modulus(&self) -> ArbBall {
let re2 = self.re.clone() * self.re.clone();
let im2 = self.im.clone() * self.im.clone();
let sum = re2 + im2;
sum.sqrt()
.unwrap_or_else(|| ArbBall::infinity(self.re.prec))
}
}
impl fmt::Display for AcbBall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} + {}·i", self.re, self.im)
}
}
fn registry() -> &'static PrimitiveRegistry {
static REGISTRY: OnceLock<PrimitiveRegistry> = OnceLock::new();
REGISTRY.get_or_init(PrimitiveRegistry::dispatch_registry)
}
pub struct IntervalEval {
bindings: HashMap<ExprId, ArbBall>,
pub prec: u32,
}
impl IntervalEval {
pub fn new(prec: u32) -> Self {
IntervalEval {
bindings: HashMap::new(),
prec,
}
}
pub fn bind(&mut self, var: ExprId, ball: ArbBall) {
self.bindings.insert(var, ball);
}
pub fn eval(&self, expr: ExprId, pool: &ExprPool) -> Option<ArbBall> {
self.eval_node(expr, pool)
}
fn eval_predicate(&self, pred: ExprId, pool: &ExprPool) -> Option<bool> {
let ExprData::Predicate { kind, args } = pool.get(pred) else {
return None;
};
match kind {
PredicateKind::True => Some(true),
PredicateKind::False => Some(false),
PredicateKind::Not => Some(!self.eval_predicate(*args.first()?, pool)?),
PredicateKind::And => {
for &a in &args {
if !self.eval_predicate(a, pool)? {
return Some(false);
}
}
Some(true)
}
PredicateKind::Or => {
for &a in &args {
if self.eval_predicate(a, pool)? {
return Some(true);
}
}
Some(false)
}
PredicateKind::Lt
| PredicateKind::Le
| PredicateKind::Gt
| PredicateKind::Ge
| PredicateKind::Eq
| PredicateKind::Ne => {
let [lhs, rhs] = args.as_slice() else {
return None;
};
let lhs = self.eval_node(*lhs, pool)?;
let rhs = self.eval_node(*rhs, pool)?;
let lhs_lo = lhs.lo();
let lhs_hi = lhs.hi();
let rhs_lo = rhs.lo();
let rhs_hi = rhs.hi();
match kind {
PredicateKind::Lt if lhs_hi < rhs_lo => Some(true),
PredicateKind::Lt if lhs_lo >= rhs_hi => Some(false),
PredicateKind::Le if lhs_hi <= rhs_lo => Some(true),
PredicateKind::Le if lhs_lo > rhs_hi => Some(false),
PredicateKind::Gt if lhs_lo > rhs_hi => Some(true),
PredicateKind::Gt if lhs_hi <= rhs_lo => Some(false),
PredicateKind::Ge if lhs_lo >= rhs_hi => Some(true),
PredicateKind::Ge if lhs_hi < rhs_lo => Some(false),
PredicateKind::Eq if lhs.is_exact() && rhs.is_exact() && lhs.mid == rhs.mid => {
Some(true)
}
PredicateKind::Eq if lhs_hi < rhs_lo || lhs_lo > rhs_hi => Some(false),
PredicateKind::Ne if lhs_hi < rhs_lo || lhs_lo > rhs_hi => Some(true),
PredicateKind::Ne if lhs.is_exact() && rhs.is_exact() && lhs.mid == rhs.mid => {
Some(false)
}
_ => None,
}
}
}
}
fn eval_node(&self, expr: ExprId, pool: &ExprPool) -> Option<ArbBall> {
match pool.get(expr) {
ExprData::Integer(n) => Some(ArbBall::from_integer(&n.0, self.prec)),
ExprData::Rational(r) => Some(ArbBall::from_rational(&r.0, self.prec)),
ExprData::Float(f) => Some(ArbBall::from_f64(f.inner.to_f64(), self.prec)),
ExprData::Symbol { .. } => self.bindings.get(&expr).cloned(),
ExprData::Add(args) => {
let mut acc = ArbBall::from_f64(0.0, self.prec);
for &a in &args {
acc = acc + self.eval_node(a, pool)?;
}
Some(acc)
}
ExprData::Mul(args) => {
let mut acc = ArbBall::from_f64(1.0, self.prec);
for &a in &args {
acc = acc * self.eval_node(a, pool)?;
}
Some(acc)
}
ExprData::Pow { base, exp } => {
let b = self.eval_node(base, pool)?;
let e = self.eval_node(exp, pool)?;
if let ExprData::Integer(n) = pool.get(exp) {
let nv = n.0.to_i64()?;
return Some(b.powi(nv));
}
Some(b.pow_f(&e))
}
ExprData::Func { name, args } if !args.is_empty() => {
let mut vals = Vec::with_capacity(args.len());
for &a in &args {
vals.push(self.eval_node(a, pool)?);
}
registry().numeric_ball(&name, &vals)
}
ExprData::Piecewise { branches, default } => {
for (c, v) in branches {
match self.eval_predicate(c, pool) {
Some(true) => return self.eval_node(v, pool),
Some(false) => {}
None => return None,
}
}
self.eval_node(default, pool)
}
ExprData::Predicate { .. } => {
let v = if self.eval_predicate(expr, pool)? {
1.0
} else {
0.0
};
Some(ArbBall::from_f64(v, self.prec))
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::{Domain, ExprPool};
fn p() -> ExprPool {
ExprPool::new()
}
#[test]
fn ball_contains_midpoint() {
let b = ArbBall::from_midpoint_radius(3.0, 0.5, 64);
assert!(b.contains(3.0));
assert!(b.contains(2.5));
assert!(b.contains(3.5));
assert!(!b.contains(4.0));
}
#[test]
fn ball_add_enclosure() {
let a = ArbBall::from_midpoint_radius(1.0, 0.1, 64);
let b = ArbBall::from_midpoint_radius(2.0, 0.2, 64);
let c = a + b;
assert!(c.contains(2.7));
assert!(c.contains(3.0));
assert!(c.contains(3.3));
}
#[test]
fn ball_mul_enclosure() {
let a = ArbBall::from_midpoint_radius(2.0, 0.5, 64); let b = ArbBall::from_midpoint_radius(3.0, 0.5, 64); let c = a * b;
assert!(c.contains(4.0));
assert!(c.contains(8.0));
}
#[test]
fn ball_lambert_w0_at_one() {
let b = ArbBall::from_f64(1.0, 128);
assert!(b.lambert_w0().is_some());
}
#[test]
fn ball_powi_exact() {
let b = ArbBall::from_f64(3.0, 128);
let b3 = b.powi(3);
assert!(b3.contains(27.0));
assert!(!b3.contains(26.0));
}
#[test]
fn ball_sin_enclosure() {
let pi_2 = std::f64::consts::PI / 2.0;
let b = ArbBall::from_midpoint_radius(pi_2, 0.01, 128);
let s = b.sin();
assert!(s.contains(1.0));
}
#[test]
fn ball_exp_enclosure() {
let b = ArbBall::from_midpoint_radius(0.0, 0.1, 128); let e = b.exp();
assert!(e.contains(0.905));
assert!(e.contains(1.0));
assert!(e.contains(1.105));
}
#[test]
fn ball_log_enclosure() {
let b = ArbBall::from_midpoint_radius(2.0, 0.5, 128); let l = b.log().unwrap();
assert!(l.contains(0.41));
assert!(l.contains(0.91));
assert!(l.contains(2_f64.ln()));
}
#[test]
fn ball_log_fails_at_nonpositive() {
let b = ArbBall::from_midpoint_radius(0.0, 0.5, 128); assert!(b.log().is_none());
}
#[test]
fn interval_eval_constant() {
let pool = p();
let five = pool.integer(5_i32);
let eval = IntervalEval::new(128);
let r = eval.eval(five, &pool).unwrap();
assert!(r.contains(5.0));
}
#[test]
fn interval_eval_piecewise_with_binding() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let pw = pool.piecewise(
vec![(pool.pred_gt(x, pool.integer(0_i32)), x)],
pool.integer(-1_i32),
);
let mut ev = IntervalEval::new(128);
ev.bind(x, ArbBall::from_midpoint_radius(1.0, 1e-6, 128));
let r = ev.eval(pw, &pool).unwrap();
assert!(r.contains(1.0));
}
#[test]
fn interval_eval_refuses_piecewise_threshold_spanning_binding() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let pw = pool.piecewise(
vec![(pool.pred_gt(x, pool.integer(0_i32)), pool.integer(1_i32))],
pool.integer(-1_i32),
);
let mut ev = IntervalEval::new(128);
ev.bind(x, ArbBall::from_midpoint_radius(0.1, 0.2, 128));
assert!(ev.eval(pw, &pool).is_none());
}
#[test]
fn interval_eval_polynomial() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let one = pool.integer(1_i32);
let expr = pool.add(vec![x2, one]);
let x_ball = ArbBall::from_midpoint_radius(3.0, 0.1, 128);
let mut eval = IntervalEval::new(128);
eval.bind(x, x_ball);
let r = eval.eval(expr, &pool).unwrap();
assert!(r.contains(9.5));
assert!(r.contains(10.0));
assert!(r.contains(10.5));
}
#[test]
fn interval_eval_unbound_is_none() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let eval = IntervalEval::new(128);
assert!(eval.eval(x, &pool).is_none());
}
#[test]
fn interval_eval_rational() {
let pool = p();
let third = pool.rational(1, 3);
let eval = IntervalEval::new(128);
let r = eval.eval(third, &pool).unwrap();
let mid = r.mid_f64();
assert!((mid - 1.0 / 3.0).abs() < 1e-15, "mid={mid}");
assert!(r.rad_f64() < 1e-30, "rad={}", r.rad_f64());
}
#[test]
fn interval_eval_accepts_every_primitive_that_advertises_numeric_ball() {
use crate::primitive::{Capabilities, PrimitiveRegistry};
let reg = PrimitiveRegistry::default_registry();
let pool = p();
let x = pool.symbol("x", Domain::Real);
let probes = [1.5_f64, 0.5];
let refused: Vec<String> = reg
.iter()
.filter(|(_, caps)| caps.contains(Capabilities::NUMERIC_BALL))
.filter(|(name, _)| {
let call = pool.func(*name, vec![x]);
probes.iter().all(|&probe| {
let mut ev = IntervalEval::new(128);
ev.bind(x, ArbBall::from_f64(probe, 128));
ev.eval(call, &pool).is_none()
})
})
.map(|(name, _)| name.to_string())
.collect();
assert!(
refused.is_empty(),
"these primitives advertise numeric_ball but interval evaluation \
refuses them: {refused:?}"
);
}
#[test]
fn interval_eval_evaluates_bessel() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut ev = IntervalEval::new(128);
ev.bind(x, ArbBall::from_f64(1.0, 128));
let j0 = ev.eval(pool.func("bessel_j0", vec![x]), &pool).unwrap();
let j1 = ev.eval(pool.func("bessel_j1", vec![x]), &pool).unwrap();
assert!(
(j0.mid_f64() - 0.765_197_686_557_966_5).abs() < 1e-15,
"J0(1) = {j0}"
);
assert!(
(j1.mid_f64() - 0.440_050_585_744_933_5).abs() < 1e-15,
"J1(1) = {j1}"
);
assert!(j0.rad_f64() < 1e-30 && j1.rad_f64() < 1e-30);
}
#[test]
fn interval_eval_refuses_a_unary_primitive_at_the_wrong_arity() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let mut ev = IntervalEval::new(128);
ev.bind(x, ArbBall::from_f64(1.0, 128));
ev.bind(y, ArbBall::from_f64(2.0, 128));
assert!(ev.eval(pool.func("sin", vec![x, y]), &pool).is_none());
assert!(ev.eval(pool.func("sin", vec![]), &pool).is_none());
}
#[test]
fn acb_modulus() {
let z = AcbBall::from_f64(3.0, 4.0, 128);
let m = z.modulus();
assert!(m.contains(5.0));
}
}
#[cfg(test)]
mod indeterminate_ball_tests {
use super::*;
const PREC: u32 = 128;
#[test]
fn an_indeterminate_operand_never_produces_nan_endpoints() {
let unknown = ArbBall::infinity(PREC);
let zero_centred = ArbBall::from_midpoint_radius(0.0, 0.5, PREC);
let products = [
unknown.clone() * zero_centred.clone(),
zero_centred * unknown.clone(),
unknown.clone() * unknown.clone(),
unknown.clone() + unknown.clone(),
unknown.clone() - unknown.clone(),
unknown.powi(2),
unknown.powi(-2),
unknown.abs_ball(),
unknown.sin(),
unknown.exp(),
];
for (i, p) in products.iter().enumerate() {
assert!(!p.lo().is_nan(), "case {i}: lo is NaN");
assert!(!p.hi().is_nan(), "case {i}: hi is NaN");
assert!(p.lo() <= 0 && p.hi() >= 0, "case {i}: not the whole line");
for probe in [0.0, 1.0, -5.0, 1e300] {
assert!(p.contains(probe), "case {i}: refuses to contain {probe}");
}
}
}
#[test]
fn powi_encloses_from_the_endpoints() {
let pm1 = ArbBall::from_midpoint_radius(0.0, 1.0, PREC);
let sq = pm1.powi(2);
assert!(
sq.lo() >= -1e-30,
"even power must not dip below 0: {}",
sq.lo()
);
assert!(sq.hi() >= 1.0, "1 is attained at both endpoints");
assert!(Float::with_val(PREC, sq.hi() - 1u32) < 1e-30);
let two_three = ArbBall::from_midpoint_radius(2.5, 0.5, PREC);
let inv6 = two_three.powi(-6);
assert!(
inv6.lo().is_finite() && inv6.hi().is_finite(),
"no finite enclosure"
);
let one_over_729 = Float::with_val(PREC + 64, 1u32) / 729u32;
assert!(inv6.lo() <= one_over_729 && inv6.hi() >= 1.0 / 64.0);
assert!(
inv6.lo() > 0.0 && inv6.hi() < 0.02,
"enclosure is not tight: {inv6}"
);
let cube = ArbBall::from_midpoint_radius(-2.325, 1.0, PREC).powi(3);
assert!(cube.hi() < 0, "(-3.325, -1.325)³ is negative throughout");
assert!(cube.lo() <= -36.75 && cube.lo() >= -36.76);
assert!(cube.hi() >= -2.3263 && cube.hi() <= -2.326);
}
#[test]
fn powi_encloses_dense_samples() {
const WORK: u32 = PREC + 64;
for &(mid, rad) in &[
(0.0_f64, 1.0_f64),
(-2.325, 1.0),
(2.5, 0.5),
(0.5, 0.5),
(-0.25, 0.75),
(3.0, 0.0),
] {
let b = ArbBall::from_midpoint_radius(mid, rad, PREC);
let (lo, hi) = (Float::with_val(WORK, b.lo()), Float::with_val(WORK, b.hi()));
for n in [1_u32, 2, 3, 4, 5, 6, 7] {
let p = b.powi(n as i64);
let (plo, phi) = (Float::with_val(WORK, p.lo()), Float::with_val(WORK, p.hi()));
for k in 0..=64u32 {
let t = Float::with_val(WORK, &lo)
+ Float::with_val(WORK, Float::with_val(WORK, &hi - &lo) * k) / 64u32;
let v = Float::with_val(WORK, t.clone().pow(n));
assert!(
plo <= v && v <= phi,
"x^{n} over [{mid}±{rad}] misses {v} at x = {t}"
);
}
}
}
}
}
#[cfg(test)]
mod rounding_soundness_tests {
use super::*;
const PREC: u32 = 128;
fn encloses(b: &ArbBall, v: &Float) -> bool {
*v >= b.lo() && *v <= b.hi()
}
#[test]
fn transcendental_ops_never_report_zero_radius_on_exact_input() {
let half = ArbBall::from_f64(0.5, PREC);
let two = ArbBall::from_f64(2.0, PREC);
let mut zero_radius: Vec<&str> = Vec::new();
let mut check = |name: &'static str, b: ArbBall| {
if b.rad == 0 {
zero_radius.push(name);
}
};
check("exp", half.exp());
check("sin", half.sin());
check("cos", half.cos());
check("log", two.log().expect("log 2 defined"));
check("sqrt", two.sqrt().expect("sqrt 2 defined"));
check("tan", half.tan().expect("tan 0.5 defined"));
check("asin", half.asin().expect("asin 0.5 defined"));
check("acos", half.acos().expect("acos 0.5 defined"));
check("atan", half.atan());
check("asinh", half.asinh());
check("atanh", half.atanh().expect("atanh 0.5 defined"));
assert!(
zero_radius.is_empty(),
"these ops claim an irrational result is exact: {zero_radius:?}"
);
}
#[test]
fn bessel_encloses_an_interior_extremum() {
let b = ArbBall::from_midpoint_radius(0.0, 1.0, PREC); let j0 = b.bessel_jn(0);
assert!(j0.contains(1.0), "J0 on [-1,1] misses its maximum: {j0}");
assert!(j0.contains(0.7651976865579666), "{j0}");
let b = ArbBall::from_midpoint_radius(3.0, 1.0, PREC); let j1 = b.bessel_jn(1);
for x in [2.0_f64, 2.5, 3.0, 3.5, 4.0] {
let mut v = Float::with_val(PREC, x);
v.jn_mut(1);
assert!(j1.contains(v.to_f64()), "J1({x}) outside {j1}");
}
}
#[test]
fn bessel_enclosure_holds_on_random_intervals() {
let mut seed = 0x2545_F491_4F6C_DD1D_u64;
let mut next = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 11) as f64 / (1u64 << 53) as f64
};
for _ in 0..200 {
let centre = (next() - 0.5) * 40.0;
let radius = next() * 5.0;
for n in [0_i32, 1] {
let ball = ArbBall::from_midpoint_radius(centre, radius, PREC);
let out = ball.bessel_jn(n);
for k in 0..=20 {
let x = centre - radius + 2.0 * radius * (k as f64 / 20.0);
let mut v = Float::with_val(PREC, x);
v.jn_mut(n);
assert!(
out.contains(v.to_f64()),
"J{n}({x}) = {v} escapes {out} for [{}, {}]",
centre - radius,
centre + radius
);
}
}
}
}
#[test]
fn tan_encloses_a_box_that_crosses_a_pole() {
let pi = std::f64::consts::PI;
for (lo, hi) in [
(0.1, 0.2 + pi),
(0.1, 0.5 + pi),
(-1.0, 1.0 + pi),
(-1.4292036732051034, 4.570796326794897),
(1.5, 1.7), (-10.0, 10.0), ] {
let b = ArbBall::from_midpoint_radius((lo + hi) / 2.0, (hi - lo) / 2.0, PREC);
let out = b.tan().expect("finite box");
for k in 0..=200 {
let x = lo + (hi - lo) * (k as f64 / 200.0);
let v = Float::with_val(PREC + 32, x).tan();
assert!(
encloses(&out, &v),
"tan({x}) = {v} escapes {out} for [{lo}, {hi}]"
);
}
}
}
#[test]
fn tan_stays_tight_on_a_pole_free_box() {
let b = ArbBall::from_midpoint_radius(0.55, 0.45, PREC); let out = b.tan().expect("finite box");
assert!(out.lo() > 0.10033, "lower bound {} is not tight", out.lo());
assert!(out.hi() < 1.5575, "upper bound {} is not tight", out.hi());
assert!(out.contains(0.5463024898437905)); }
#[test]
fn pow_f_encloses_a_base_straddling_zero() {
let base = ArbBall::from_midpoint_radius(1.0, 2.0, PREC); for e in [2.0_f64, 4.0, 2.0, 6.0] {
let out = base.pow_f(&ArbBall::from_f64(e, PREC));
for k in 0..=100 {
let x = -1.0 + 4.0 * (k as f64 / 100.0);
let v = Float::with_val(PREC + 32, x).pow(e);
assert!(encloses(&out, &v), "{x}^{e} = {v} escapes {out}");
}
}
let out = base.pow_f(&ArbBall::from_f64(-2.0, PREC));
assert!(
out.contains(1e6),
"x^-2 over [-1, 3] claims the finite {out}"
);
assert!(
out.contains(1e300),
"x^-2 over [-1, 3] claims the finite {out}"
);
let out = base.pow_f(&ArbBall::from_f64(1.5, PREC));
assert!(out.rad.is_infinite(), "(-1)^1.5 is not real, got {out}");
}
#[test]
fn pow_f_stays_tight_on_a_positive_base() {
let base = ArbBall::from_midpoint_radius(2.5, 0.5, PREC); let out = base.pow_f(&ArbBall::from_f64(0.5, PREC));
let (sqrt2, sqrt3) = (std::f64::consts::SQRT_2, 1.7320508075688772_f64);
assert!(
out.lo() > sqrt2 - 1e-9,
"lower bound {} is not tight",
out.lo()
);
assert!(
out.hi() < sqrt3 + 1e-9,
"upper bound {} is not tight",
out.hi()
);
let out = base.pow_f(&ArbBall::from_midpoint_radius(0.5, 1.5, PREC));
for k in 0..=20 {
let x = 2.0 + (k as f64) / 20.0;
for j in 0..=20 {
let y = -1.0 + 3.0 * (j as f64) / 20.0;
let v = Float::with_val(PREC + 32, x).pow(y);
assert!(encloses(&out, &v), "{x}^{y} = {v} escapes {out}");
}
}
assert!(out.hi() < 9.000001, "upper bound {} is not tight", out.hi());
}
#[test]
fn endpoint_hull_kernels_enclose_random_wide_intervals() {
let work = PREC + 32;
let mut seed = 0x9E37_79B9_7F4A_7C15_u64;
let mut next = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 11) as f64 / (1u64 << 53) as f64
};
for _ in 0..200 {
let centre = (next() - 0.5) * 20.0;
let radius = next() * 5.0;
let ball = ArbBall::from_midpoint_radius(centre, radius, PREC);
let lo = ball.lo();
let width = Float::with_val(work, ball.hi() - &lo);
let kernels: Vec<(&str, Option<ArbBall>)> = vec![
("exp", Some(ball.exp())),
("sin", Some(ball.sin())),
("cos", Some(ball.cos())),
("tan", ball.tan()),
("log", ball.log()),
("sqrt", ball.sqrt()),
("sinh", Some(ball.sinh())),
("cosh", Some(ball.cosh())),
("tanh", Some(ball.tanh())),
("asin", ball.asin()),
("acos", ball.acos()),
("atan", Some(ball.atan())),
("asinh", Some(ball.asinh())),
("acosh", ball.acosh()),
("atanh", ball.atanh()),
("erf", Some(ball.erf())),
("erfc", Some(ball.erfc())),
("gamma", ball.gamma()),
("digamma", ball.digamma()),
("bessel_j0", Some(ball.bessel_jn(0))),
("square", Some(ball.pow_f(&ArbBall::from_f64(2.0, PREC)))),
("cube", Some(ball.pow_f(&ArbBall::from_f64(3.0, PREC)))),
("recip_sq", Some(ball.pow_f(&ArbBall::from_f64(-2.0, PREC)))),
("sqrt_pow", Some(ball.pow_f(&ArbBall::from_f64(0.5, PREC)))),
];
for (name, out) in kernels {
let Some(out) = out else { continue };
for k in 0..=50 {
let t = Float::with_val(work, k as f64 / 50.0);
let x = Float::with_val(work, &lo + Float::with_val(work, &width * &t));
let v = match name {
"exp" => x.clone().exp(),
"sin" => x.clone().sin(),
"cos" => x.clone().cos(),
"tan" => x.clone().tan(),
"log" => x.clone().ln(),
"sqrt" => x.clone().sqrt(),
"sinh" => x.clone().sinh(),
"cosh" => x.clone().cosh(),
"tanh" => x.clone().tanh(),
"asin" => x.clone().asin(),
"acos" => x.clone().acos(),
"atan" => x.clone().atan(),
"asinh" => x.clone().asinh(),
"acosh" => x.clone().acosh(),
"atanh" => x.clone().atanh(),
"erf" => x.clone().erf(),
"erfc" => x.clone().erfc(),
"gamma" => x.clone().gamma(),
"digamma" => {
let mut t = x.clone();
t.digamma_mut();
t
}
"bessel_j0" => {
let mut t = x.clone();
t.jn_mut(0);
t
}
"square" => Float::with_val(work, &x * &x),
"cube" => Float::with_val(work, Float::with_val(work, &x * &x) * &x),
"recip_sq" => Float::with_val(work, 1u32 / Float::with_val(work, &x * &x)),
"sqrt_pow" => x.clone().sqrt(),
_ => unreachable!(),
};
if !v.is_finite() {
continue;
}
assert!(
encloses(&out, &v),
"{name}({x}) = {v} escapes {out} on [{lo}, {}]",
ball.hi()
);
}
}
}
}
#[test]
fn digamma_does_not_walk_every_integer_in_the_box() {
let b = ArbBall::from_midpoint_radius(5e17, 4.9e17, PREC);
let out = b.digamma().expect("no pole in [1e16, 9.9e17]");
for x in [1e16_f64, 5e17, 9.9e17] {
let mut v = Float::with_val(PREC + 32, x);
v.digamma_mut();
assert!(encloses(&out, &v), "ψ({x}) = {v} escapes {out}");
}
assert!(ArbBall::from_midpoint_radius(0.0, 1.0, PREC)
.digamma()
.is_none());
assert!(ArbBall::from_midpoint_radius(-2.5, 1.0, PREC)
.digamma()
.is_none());
}
#[test]
fn rounding_term_stays_at_precision_scale() {
let b = ArbBall::from_f64(0.5, PREC).exp();
let bound = Float::with_val(PREC, 1e-30);
assert!(
b.rad < bound,
"radius {} is far above the 2^-prec scale",
b.rad
);
}
}
#[cfg(test)]
mod special_kernel_tests {
use super::*;
const P: u32 = 128;
#[test]
fn lambert_w0_encloses_high_precision_values() {
let omega = Float::with_val(
256,
Float::parse("0.5671432904097838729999686622103555497538").unwrap(),
);
let b = ArbBall::from_f64(1.0, P).lambert_w0().unwrap();
assert!(
b.lo() <= omega && omega <= b.hi(),
"W₀(1) = {omega} escaped [{}, {}]",
b.lo(),
b.hi()
);
assert!(b.rad_f64() < 1e-30, "rad = {}", b.rad_f64());
let e = Float::with_val(P, 1u32).exp();
let b = ArbBall {
mid: e,
rad: Float::new(P),
prec: P,
}
.lambert_w0()
.unwrap();
assert!(b.contains(1.0), "W₀(e) must enclose 1, got {b}");
assert!(b.rad_f64() < 1e-30);
}
#[test]
fn lambert_w0_enclosure_satisfies_its_defining_equation() {
for x in [-0.3, -0.1, 0.0, 0.25, 1.0, 2.5, 10.0, 1e3, 1e10] {
let xb = ArbBall::from_f64(x, P);
let w = xb.lambert_w0().unwrap_or_else(|| panic!("W₀({x}) refused"));
let g = w.clone() * w.exp();
assert!(g.contains(x), "W₀({x}) enclosure gives g = {g}, not {x}");
}
}
#[test]
fn lambert_w0_refuses_below_the_branch_point() {
for x in [-0.5, -0.4, -0.37, -1.0, -1e6] {
assert!(
ArbBall::from_f64(x, P).lambert_w0().is_none(),
"W₀({x}) is not real but was answered"
);
}
}
#[test]
fn lambert_w0_over_a_box_brackets_interior_points() {
let b = ArbBall::from_midpoint_radius(1.5, 1.0, P)
.lambert_w0()
.unwrap();
for k in 0..=50 {
let x = 0.5 + 2.0 * (k as f64) / 50.0;
let w = crate::special::lambert_w0(x).unwrap();
assert!(
b.lo().to_f64() - 1e-12 <= w && w <= b.hi().to_f64() + 1e-12,
"W₀({x}) = {w} escaped {b}"
);
}
}
#[test]
fn gamma_brackets_dense_samples() {
for (lo, hi) in [
(0.5_f64, 0.6_f64),
(1.0, 2.0),
(1.4, 1.5),
(0.25, 3.0),
(4.0, 4.25),
(9.0, 10.0),
(0.01, 0.02),
] {
let b = ArbBall::from_endpoints(&Float::with_val(P, lo), &Float::with_val(P, hi), P)
.gamma()
.unwrap_or_else(|| panic!("Γ on [{lo},{hi}] refused"));
for k in 0..=100 {
let t = lo + (hi - lo) * (k as f64) / 100.0;
let truth = Float::with_val(P + 64, t).gamma();
assert!(
b.lo() <= truth && truth <= b.hi(),
"Γ({t}) = {truth} escaped [{}, {}]",
b.lo(),
b.hi()
);
}
}
}
#[test]
fn gamma_does_not_assume_monotonicity() {
let b = ArbBall::from_endpoints(&Float::with_val(P, 1.0), &Float::with_val(P, 2.0), P)
.gamma()
.unwrap();
assert!(b.lo() < 0.8857, "lower bound {} misses the minimum", b.lo());
assert!(b.hi() >= 1.0, "upper bound {} misses Γ(1) = 1", b.hi());
}
#[test]
fn gamma_refuses_non_positive_boxes() {
for (lo, hi) in [(-1.0_f64, 1.0_f64), (0.0, 1.0), (-3.0, -2.0), (-0.5, -0.4)] {
assert!(
ArbBall::from_endpoints(&Float::with_val(P, lo), &Float::with_val(P, hi), P)
.gamma()
.is_none(),
"Γ on [{lo},{hi}] should refuse"
);
}
}
}