use crate::math_compat as mc;
use core::fmt;
use core::ops::{Add, Div, Mul, Neg, Sub};
pub const TRANSCEND_ULP: u32 = 4;
const TWO_PI: f64 = core::f64::consts::TAU;
const PI: f64 = core::f64::consts::PI;
const HALF_PI: f64 = core::f64::consts::FRAC_PI_2;
#[inline]
fn wide_down(x: f64, ulps: u32) -> f64 {
if x.is_nan() {
return f64::NEG_INFINITY;
}
let mut r = x;
let mut i = 0;
while i < ulps {
r = r.next_down();
i += 1;
}
r
}
#[inline]
fn wide_up(x: f64, ulps: u32) -> f64 {
if x.is_nan() {
return f64::INFINITY;
}
let mut r = x;
let mut i = 0;
while i < ulps {
r = r.next_up();
i += 1;
}
r
}
#[inline]
fn hull4(a: f64, b: f64, c: f64, d: f64) -> (f64, f64) {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for v in [a, b, c, d] {
if v.is_nan() {
return (f64::NEG_INFINITY, f64::INFINITY);
}
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
(lo, hi)
}
#[inline]
fn contains_grid_point(a: f64, b: f64, base: f64, period: f64) -> bool {
let mid = 0.5 * (a + b);
let kf = mc::round((mid - base) / period);
let guard = 1e-9 * if period > 1.0 { period } else { 1.0 };
for dk in [-1.0, 0.0, 1.0] {
let x = base + (kf + dk) * period;
if x >= a - guard && x <= b + guard {
return true;
}
}
false
}
#[derive(Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Interval {
lo: f64,
hi: f64,
}
impl Interval {
#[inline]
pub fn new(lo: f64, hi: f64) -> Interval {
if lo.is_nan() || hi.is_nan() {
return Interval::whole();
}
if lo <= hi {
Interval { lo, hi }
} else {
Interval { lo: hi, hi: lo }
}
}
#[inline]
pub fn point(x: f64) -> Interval {
if x.is_nan() {
Interval::whole()
} else {
Interval { lo: x, hi: x }
}
}
#[inline]
pub const fn whole() -> Interval {
Interval {
lo: f64::NEG_INFINITY,
hi: f64::INFINITY,
}
}
#[inline]
pub const fn lo(self) -> f64 {
self.lo
}
#[inline]
pub const fn hi(self) -> f64 {
self.hi
}
#[inline]
pub fn width(self) -> f64 {
wide_up(self.hi - self.lo, 1)
}
#[inline]
pub fn mid(self) -> f64 {
0.5 * (self.lo + self.hi)
}
#[inline]
pub fn mag(self) -> f64 {
let a = mc::abs(self.lo);
let b = mc::abs(self.hi);
if a > b {
a
} else {
b
}
}
#[inline]
pub fn is_finite(self) -> bool {
self.lo.is_finite() && self.hi.is_finite()
}
#[inline]
pub fn contains(self, x: f64) -> bool {
!x.is_nan() && self.lo <= x && x <= self.hi
}
#[inline]
pub fn contains_interval(self, other: Interval) -> bool {
self.lo <= other.lo && other.hi <= self.hi
}
#[inline]
pub fn hull(self, other: Interval) -> Interval {
Interval::new(self.lo.min(other.lo), self.hi.max(other.hi))
}
#[inline]
pub fn recip(self) -> Interval {
if self.lo <= 0.0 && self.hi >= 0.0 {
return Interval::whole();
}
let a = 1.0 / self.hi;
let b = 1.0 / self.lo;
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
Interval::new(wide_down(lo, 1), wide_up(hi, 1))
}
pub fn powi(self, n: i32) -> Interval {
if n == 0 {
return Interval::point(1.0);
}
let p = n.unsigned_abs();
let nat = self.pow_nat(p);
if n < 0 {
nat.recip()
} else {
nat
}
}
fn pow_nat(self, p: u32) -> Interval {
if p == 1 {
return self;
}
let pi = p as i32;
if p.is_multiple_of(2) {
let a = mc::abs(self.lo);
let b = mc::abs(self.hi);
let (mn, mx) = if a < b { (a, b) } else { (b, a) };
let lo = if self.lo <= 0.0 && self.hi >= 0.0 {
0.0
} else {
mc::powi(mn, pi)
};
let hi = mc::powi(mx, pi);
Interval::new(wide_down(lo, 1), wide_up(hi, 1))
} else {
let lo = mc::powi(self.lo, pi);
let hi = mc::powi(self.hi, pi);
Interval::new(wide_down(lo, 1), wide_up(hi, 1))
}
}
#[inline]
pub fn abs(self) -> Interval {
if self.lo >= 0.0 {
self
} else if self.hi <= 0.0 {
Interval::new(-self.hi, -self.lo)
} else {
let m = if -self.lo > self.hi {
-self.lo
} else {
self.hi
};
Interval::new(0.0, m)
}
}
#[inline]
pub fn sqrt(self) -> Interval {
if self.hi < 0.0 {
return Interval::whole();
}
let lo_in = if self.lo < 0.0 { 0.0 } else { self.lo };
let lo = wide_down(mc::sqrt(lo_in), 1);
let hi = wide_up(mc::sqrt(self.hi), 1);
Interval::new(lo.max(0.0), hi)
}
#[inline]
pub fn hypot(self, other: Interval) -> Interval {
let a = self.abs();
let b = other.abs();
let lo = wide_down(mc::hypot(a.lo, b.lo), TRANSCEND_ULP);
let hi = wide_up(mc::hypot(a.hi, b.hi), TRANSCEND_ULP);
Interval::new(lo.max(0.0), hi)
}
#[inline]
pub fn exp(self) -> Interval {
let lo = wide_down(mc::exp(self.lo), TRANSCEND_ULP);
let hi = wide_up(mc::exp(self.hi), TRANSCEND_ULP);
Interval::new(lo.max(0.0), hi)
}
#[inline]
pub fn ln(self) -> Interval {
if self.hi <= 0.0 {
return Interval::whole();
}
let lo = if self.lo <= 0.0 {
f64::NEG_INFINITY
} else {
wide_down(mc::ln(self.lo), TRANSCEND_ULP)
};
let hi = wide_up(mc::ln(self.hi), TRANSCEND_ULP);
Interval::new(lo, hi)
}
#[inline]
pub fn asinh(self) -> Interval {
let s = (self.powi(2) + Interval::point(1.0)).sqrt();
(self + s).ln()
}
#[inline]
pub fn atanh(self) -> Interval {
let num = Interval::point(1.0) + self;
let den = Interval::point(1.0) - self;
Interval::point(0.5) * (num / den).ln()
}
pub fn sin(self) -> Interval {
if !self.is_finite() || self.width() >= TWO_PI {
return Interval::new(-1.0, 1.0);
}
let sa = mc::sin(self.lo);
let sb = mc::sin(self.hi);
let mut lo = sa.min(sb);
let mut hi = sa.max(sb);
if contains_grid_point(self.lo, self.hi, HALF_PI, TWO_PI) {
hi = 1.0;
}
if contains_grid_point(self.lo, self.hi, -HALF_PI, TWO_PI) {
lo = -1.0;
}
let lo = wide_down(lo, TRANSCEND_ULP).max(-1.0);
let hi = wide_up(hi, TRANSCEND_ULP).min(1.0);
Interval::new(lo, hi)
}
pub fn cos(self) -> Interval {
if !self.is_finite() || self.width() >= TWO_PI {
return Interval::new(-1.0, 1.0);
}
let ca = mc::cos(self.lo);
let cb = mc::cos(self.hi);
let mut lo = ca.min(cb);
let mut hi = ca.max(cb);
if contains_grid_point(self.lo, self.hi, 0.0, TWO_PI) {
hi = 1.0;
}
if contains_grid_point(self.lo, self.hi, PI, TWO_PI) {
lo = -1.0;
}
let lo = wide_down(lo, TRANSCEND_ULP).max(-1.0);
let hi = wide_up(hi, TRANSCEND_ULP).min(1.0);
Interval::new(lo, hi)
}
pub fn tan(self) -> Interval {
if !self.is_finite() || self.width() >= PI {
return Interval::whole();
}
if contains_grid_point(self.lo, self.hi, HALF_PI, PI) {
return Interval::whole();
}
let lo = wide_down(mc::tan(self.lo), TRANSCEND_ULP);
let hi = wide_up(mc::tan(self.hi), TRANSCEND_ULP);
Interval::new(lo, hi)
}
#[inline]
pub fn atan(self) -> Interval {
let lo = wide_down(mc::atan(self.lo), TRANSCEND_ULP);
let hi = wide_up(mc::atan(self.hi), TRANSCEND_ULP);
Interval::new(lo, hi)
}
pub fn asin(self) -> Interval {
let a = self.lo.max(-1.0);
let b = self.hi.min(1.0);
if a > b {
return Interval::new(-HALF_PI, HALF_PI);
}
let lo = wide_down(mc::asin(a), TRANSCEND_ULP).max(-HALF_PI);
let hi = wide_up(mc::asin(b), TRANSCEND_ULP).min(HALF_PI);
Interval::new(lo, hi)
}
pub fn acos(self) -> Interval {
let a = self.lo.max(-1.0);
let b = self.hi.min(1.0);
if a > b {
return Interval::new(0.0, PI);
}
let lo = wide_down(mc::acos(b), TRANSCEND_ULP).max(0.0);
let hi = wide_up(mc::acos(a), TRANSCEND_ULP).min(PI);
Interval::new(lo, hi)
}
pub fn atan2(self, other: Interval) -> Interval {
let y = self;
let x = other;
let y_has_zero = y.lo <= 0.0 && y.hi >= 0.0;
let x_has_zero = x.lo <= 0.0 && x.hi >= 0.0;
if y_has_zero && (x_has_zero || x.lo < 0.0) {
return Interval::new(wide_down(-PI, 1), wide_up(PI, 1));
}
let mut xs = [x.lo, x.hi, 0.0];
let mut ys = [y.lo, y.hi, 0.0];
let nx = if x.lo < 0.0 && x.hi > 0.0 { 3 } else { 2 };
let ny = if y.lo < 0.0 && y.hi > 0.0 { 3 } else { 2 };
sort_prefix(&mut xs, nx);
sort_prefix(&mut ys, ny);
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
let mut i = 0;
while i + 1 < nx {
let mut j = 0;
while j + 1 < ny {
for &yy in &[ys[j], ys[j + 1]] {
for &xx in &[xs[i], xs[i + 1]] {
let a = mc::atan2(yy, xx);
if a < lo {
lo = a;
}
if a > hi {
hi = a;
}
}
}
j += 1;
}
i += 1;
}
Interval::new(wide_down(lo, TRANSCEND_ULP), wide_up(hi, TRANSCEND_ULP))
}
}
#[inline]
fn sort_prefix(arr: &mut [f64; 3], n: usize) {
let mut i = 1;
while i < n {
let mut j = i;
while j > 0 && arr[j - 1] > arr[j] {
arr.swap(j - 1, j);
j -= 1;
}
i += 1;
}
}
impl From<f64> for Interval {
#[inline]
fn from(x: f64) -> Interval {
Interval::point(x)
}
}
impl Add for Interval {
type Output = Interval;
#[inline]
fn add(self, o: Interval) -> Interval {
Interval::new(wide_down(self.lo + o.lo, 1), wide_up(self.hi + o.hi, 1))
}
}
impl Sub for Interval {
type Output = Interval;
#[inline]
fn sub(self, o: Interval) -> Interval {
Interval::new(wide_down(self.lo - o.hi, 1), wide_up(self.hi - o.lo, 1))
}
}
impl Mul for Interval {
type Output = Interval;
#[inline]
fn mul(self, o: Interval) -> Interval {
let (lo, hi) = hull4(
self.lo * o.lo,
self.lo * o.hi,
self.hi * o.lo,
self.hi * o.hi,
);
Interval::new(wide_down(lo, 1), wide_up(hi, 1))
}
}
impl Div for Interval {
type Output = Interval;
#[inline]
fn div(self, o: Interval) -> Interval {
if o.lo <= 0.0 && o.hi >= 0.0 {
return Interval::whole();
}
let (lo, hi) = hull4(
self.lo / o.lo,
self.lo / o.hi,
self.hi / o.lo,
self.hi / o.hi,
);
Interval::new(wide_down(lo, 1), wide_up(hi, 1))
}
}
impl Neg for Interval {
type Output = Interval;
#[inline]
fn neg(self) -> Interval {
Interval {
lo: -self.hi,
hi: -self.lo,
}
}
}
impl fmt::Debug for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}, {}]", self.lo, self.hi)
}
}
impl fmt::Display for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}, {}]", self.lo, self.hi)
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IntervalCoord {
pub x: Interval,
pub y: Interval,
}
impl IntervalCoord {
#[inline]
pub const fn new(x: Interval, y: Interval) -> IntervalCoord {
IntervalCoord { x, y }
}
#[inline]
pub fn contains(self, x: f64, y: f64) -> bool {
self.x.contains(x) && self.y.contains(y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn point_contains_itself() {
for &v in &[0.0, -1.5, 3.25e12, -7e-8] {
assert!(Interval::point(v).contains(v));
}
}
#[test]
fn add_sub_mul_div_enclose_exact() {
let vals = [-3.7, -1.0, -0.25, 0.0, 0.5, 2.0, 9.9, 123.456];
for &a in &vals {
for &b in &vals {
let ia = Interval::point(a);
let ib = Interval::point(b);
assert!((ia + ib).contains(a + b), "add {a} {b}");
assert!((ia - ib).contains(a - b), "sub {a} {b}");
assert!((ia * ib).contains(a * b), "mul {a} {b}");
if b != 0.0 {
assert!((ia / ib).contains(a / b), "div {a} {b}");
}
}
}
}
#[test]
fn known_rational_enclosures_are_tight() {
let two = Interval::point(2.0);
let three = Interval::point(3.0);
let sum = two + three; assert!(sum.contains(5.0));
assert!(sum.width() < 1e-14);
let prod = two * three; assert!(prod.contains(6.0));
assert!(prod.width() < 1e-14);
let sq = Interval::new(2.0, 3.0).powi(2);
assert!(sq.contains(4.0) && sq.contains(9.0));
assert!(sq.lo() <= 4.0 && sq.hi() >= 9.0);
assert!(sq.lo() > 3.99 && sq.hi() < 9.01);
let sqz = Interval::new(-2.0, 3.0).powi(2);
assert!(sqz.contains(0.0));
assert!(sqz.lo() <= 0.0 && sqz.hi() >= 9.0);
}
#[test]
fn monotone_widening_under_input_widening() {
let narrow = Interval::new(0.30, 0.31);
let wide = Interval::new(0.20, 0.41);
assert!(wide.contains_interval(narrow));
for f in [
|i: Interval| i.sin(),
|i: Interval| i.cos(),
|i: Interval| i.exp(),
|i: Interval| i.atan(),
|i: Interval| i * i,
|i: Interval| i.sqrt(),
] {
let fn_narrow = f(narrow);
let fn_wide = f(wide);
assert!(
fn_wide.contains_interval(fn_narrow),
"widening shrank output: narrow={fn_narrow:?} wide={fn_wide:?}"
);
}
}
#[test]
fn transcendentals_enclose_f64() {
let xs = [-2.3, -0.7, 0.0, 0.4, 1.1, 2.9, 5.5];
for &x in &xs {
assert!(Interval::point(x).sin().contains(x.sin()), "sin {x}");
assert!(Interval::point(x).cos().contains(x.cos()), "cos {x}");
assert!(Interval::point(x).atan().contains(x.atan()), "atan {x}");
assert!(Interval::point(x).exp().contains(x.exp()), "exp {x}");
if x > 0.0 {
assert!(Interval::point(x).ln().contains(x.ln()), "ln {x}");
assert!(Interval::point(x).sqrt().contains(x.sqrt()), "sqrt {x}");
}
if x.abs() <= 1.0 {
assert!(Interval::point(x).asin().contains(x.asin()), "asin {x}");
assert!(Interval::point(x).acos().contains(x.acos()), "acos {x}");
}
}
}
#[test]
fn sin_cos_critical_points_reach_extrema() {
use core::f64::consts::PI;
let s = Interval::new(PI / 2.0 - 0.1, PI / 2.0 + 0.1).sin();
assert!(s.hi() >= 1.0 - 1e-12, "sin max not reached: {s:?}");
let s2 = Interval::new(-PI / 2.0 - 0.1, -PI / 2.0 + 0.1).sin();
assert!(s2.lo() <= -1.0 + 1e-12, "sin min not reached: {s2:?}");
let c = Interval::new(-0.2, 0.2).cos();
assert!(c.hi() >= 1.0 - 1e-12, "cos max not reached: {c:?}");
let c2 = Interval::new(PI - 0.2, PI + 0.2).cos();
assert!(c2.lo() <= -1.0 + 1e-12, "cos min not reached: {c2:?}");
let full = Interval::new(-10.0, 10.0).sin();
assert!(full.lo() <= -1.0 + 1e-12 && full.hi() >= 1.0 - 1e-12);
}
#[test]
fn sin_encloses_true_extrema_densely() {
let a = 0.3;
let b = 2.9; let iv = Interval::new(a, b).sin();
let mut t = a;
while t <= b {
assert!(iv.contains(t.sin()), "sin({t}) escaped {iv:?}");
t += 0.01;
}
}
#[test]
fn tan_pole_gives_whole_line() {
use core::f64::consts::PI;
let t = Interval::new(PI / 2.0 - 0.05, PI / 2.0 + 0.05).tan();
assert!(!t.is_finite());
let t2 = Interval::new(0.1, 0.2).tan();
assert!(t2.is_finite());
assert!(t2.contains(0.15_f64.tan()));
}
#[test]
fn atan2_matches_quadrants_and_encloses() {
let y = Interval::new(1.0, 2.0);
let x = Interval::new(3.0, 4.0);
let a = y.atan2(x);
for &yy in &[1.0_f64, 1.5, 2.0] {
for &xx in &[3.0_f64, 3.5, 4.0] {
assert!(a.contains(yy.atan2(xx)), "atan2 {yy} {xx} escaped {a:?}");
}
}
let full = Interval::new(-1.0, 1.0).atan2(Interval::new(-1.0, 1.0));
assert!(full.lo() <= -PI + 1e-9 && full.hi() >= PI - 1e-9);
}
#[test]
fn recip_and_negative_powi() {
let r = Interval::new(2.0, 4.0).recip();
assert!(r.contains(0.25) && r.contains(0.5));
let p = Interval::new(2.0, 2.0).powi(-2);
assert!(p.contains(0.25));
assert!(!Interval::new(-1.0, 1.0).recip().is_finite());
}
#[test]
fn asinh_atanh_compose_correctly() {
for &x in &[-0.9_f64, -0.3, 0.0, 0.2, 0.8] {
let expect = (
(x + (x * x + 1.0).sqrt()).ln(),
0.5 * ((1.0 + x) / (1.0 - x)).ln(),
);
assert!(Interval::point(x).asinh().contains(expect.0), "asinh {x}");
assert!(Interval::point(x).atanh().contains(expect.1), "atanh {x}");
}
}
#[test]
fn hull_and_intersect_helpers() {
let a = Interval::new(0.0, 2.0);
let b = Interval::new(1.0, 3.0);
let h = a.hull(b);
assert_eq!((h.lo(), h.hi()), (0.0, 3.0));
assert!(a.contains_interval(Interval::new(0.5, 1.5)));
}
}