use ogeom_core::predicates::Sign;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Interval {
lo: f64,
hi: f64,
}
impl Interval {
#[must_use]
pub const fn point(value: f64) -> Self {
Self {
lo: value,
hi: value,
}
}
#[must_use]
pub fn new(lo: f64, hi: f64) -> Self {
assert!(lo <= hi, "an interval needs ordered, comparable bounds");
Self { lo, hi }
}
#[must_use]
pub fn about(value: f64, radius: f64) -> Self {
assert!(radius >= 0.0, "an uncertainty is a magnitude");
Self::new(value - radius, value + radius)
}
#[must_use]
pub const fn lo(&self) -> f64 {
self.lo
}
#[must_use]
pub const fn hi(&self) -> f64 {
self.hi
}
#[must_use]
pub fn width(&self) -> f64 {
self.hi - self.lo
}
#[must_use]
pub fn contains(&self, value: f64) -> bool {
self.lo <= value && value <= self.hi
}
#[must_use]
pub fn certain_sign(&self) -> Option<Sign> {
if self.lo > 0.0 {
Some(Sign::Positive)
} else if self.hi < 0.0 {
Some(Sign::Negative)
} else if self.lo == 0.0 && self.hi == 0.0 {
Some(Sign::Zero)
} else {
None
}
}
#[must_use]
pub const fn neg(&self) -> Self {
Self {
lo: -self.hi,
hi: -self.lo,
}
}
#[must_use]
pub fn add(&self, other: &Self) -> Self {
Self {
lo: (self.lo + other.lo).next_down(),
hi: (self.hi + other.hi).next_up(),
}
}
#[must_use]
pub fn sub(&self, other: &Self) -> Self {
Self {
lo: (self.lo - other.hi).next_down(),
hi: (self.hi - other.lo).next_up(),
}
}
#[must_use]
pub fn mul(&self, other: &Self) -> Self {
let products = [
self.lo * other.lo,
self.lo * other.hi,
self.hi * other.lo,
self.hi * other.hi,
];
let mut lo = products[0];
let mut hi = products[0];
for p in &products[1..] {
lo = lo.min(*p);
hi = hi.max(*p);
}
Self {
lo: lo.next_down(),
hi: hi.next_up(),
}
}
#[must_use]
pub fn square(&self) -> Self {
let (a, b) = (self.lo * self.lo, self.hi * self.hi);
if self.lo <= 0.0 && self.hi >= 0.0 {
Self {
lo: 0.0,
hi: a.max(b).next_up(),
}
} else {
Self {
lo: a.min(b).next_down().max(0.0),
hi: a.max(b).next_up(),
}
}
}
#[must_use]
pub fn abs(&self) -> Self {
if self.lo >= 0.0 {
*self
} else if self.hi <= 0.0 {
self.neg()
} else {
Self {
lo: 0.0,
hi: self.hi.max(-self.lo),
}
}
}
#[must_use]
pub fn sqrt(&self) -> Option<Self> {
if self.hi < 0.0 {
return None;
}
let lo = if self.lo <= 0.0 {
0.0
} else {
self.lo.sqrt().next_down().max(0.0)
};
Some(Self {
lo,
hi: self.hi.sqrt().next_up(),
})
}
#[must_use]
pub fn checked_div(&self, other: &Self) -> Option<Self> {
if other.lo <= 0.0 && other.hi >= 0.0 {
return None;
}
let quotients = [
self.lo / other.lo,
self.lo / other.hi,
self.hi / other.lo,
self.hi / other.hi,
];
let mut lo = quotients[0];
let mut hi = quotients[0];
for q in "ients[1..] {
lo = lo.min(*q);
hi = hi.max(*q);
}
Some(Self {
lo: lo.next_down(),
hi: hi.next_up(),
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use proptest::prelude::*;
fn finite() -> impl Strategy<Value = f64> {
prop_oneof![
Just(0.0),
-1e12..1e12f64,
(-1.0..1.0f64).prop_map(|x| x * 1e-12),
]
}
proptest! {
#[test]
fn point_operations_enclose_their_own_result(a in finite(), b in finite()) {
let (x, y) = (Interval::point(a), Interval::point(b));
prop_assert!(x.add(&y).contains(a + b));
prop_assert!(x.sub(&y).contains(a - b));
prop_assert!(x.mul(&y).contains(a * b));
prop_assert!(x.square().contains(a * a));
prop_assert!(x.abs().contains(a.abs()));
if a >= 0.0 {
prop_assert!(x.sqrt().unwrap().contains(a.sqrt()));
}
if b != 0.0 {
prop_assert!(x.checked_div(&y).unwrap().contains(a / b));
}
}
#[test]
fn enclosures_stay_enclosures(a in finite(), b in finite(), r in 0.0..1e-6f64) {
let x = Interval::about(a, r);
let y = Interval::about(b, r);
prop_assert!(x.add(&y).contains(a + b));
prop_assert!(x.mul(&y).contains(a * b));
prop_assert!(x.sub(&y).contains(a - b));
}
}
#[test]
fn signs_are_certain_only_away_from_zero() {
assert_eq!(
Interval::new(1e-300, 2e-300).certain_sign(),
Some(Sign::Positive)
);
assert_eq!(
Interval::new(-2.0, -1e-300).certain_sign(),
Some(Sign::Negative)
);
assert_eq!(Interval::point(0.0).certain_sign(), Some(Sign::Zero));
assert_eq!(Interval::new(-1e-300, 1e-300).certain_sign(), None);
}
#[test]
fn the_classic_rounding_case_is_enclosed() {
let z = Interval::point(0.1).add(&Interval::point(0.2));
assert!(z.contains(0.1 + 0.2));
assert!(z.width() <= 4.0 * f64::EPSILON);
}
#[test]
fn squares_of_straddling_intervals_start_at_zero() {
let s = Interval::new(-2.0, 3.0).square();
assert_eq!(s.lo(), 0.0);
assert!(s.contains(9.0) && s.contains(0.25));
}
#[test]
fn division_through_zero_refuses() {
assert!(
Interval::point(1.0)
.checked_div(&Interval::new(-1.0, 1.0))
.is_none()
);
}
#[test]
fn a_filtered_gate_decision() {
let residual = 1e-12;
let (ax, ay) = (
Interval::about(1.0, residual),
Interval::about(0.0, residual),
);
let (bx, by) = (
Interval::about(1.0, residual),
Interval::about(1e-6, residual),
);
let cross = ax.mul(&by).sub(&ay.mul(&bx));
assert_eq!(cross.certain_sign(), Some(Sign::Positive));
let by = Interval::about(1e-12, residual);
let cross = ax.mul(&by).sub(&ay.mul(&bx));
assert_eq!(cross.certain_sign(), None);
}
}