mesh: exact predicates (Shewchuk adaptive, symbolic perturbation, indirect LPI/TPI) (#8)

* mesh: exact predicates (Shewchuk adaptive orient/incircle/insphere), symbolic perturbation, indirect LPI/TPI predicates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh: predicate benchmark (filter hit rate, CAD-degenerate inputs); record the predicate decision

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh: simplify predicates (destructuring over fallbacks, shared-minor exact insphere, cheaper expansion product)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 15:04:56 -04:00
1 parent 90254bb32f
commit 4667655e08
17 files changed
+2683 -2

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
//! Interval arithmetic, the dynamic filter of the indirect predicates (Attene 2020, §5.2;
//! Brönnimann, Burnikel & Pion 1998).
//!
//! Attene switches the FPU to directed rounding; Rust has no portable control of the rounding
//! mode (and wasm none at all), so each bound is computed with the default round-to-nearest and
//! then moved outward by one unit in the last place (`next_down` / `next_up`). A round-to-nearest
//! result is within half an ulp of the exact value, so the widened interval always contains it,
//! subnormals included. Overflow gives infinite bounds and `inf - inf` gives NaN; both make the
//! sign uncertain, which sends the predicate on to the exact stage.
use std::ops::{Add, Mul, Sub};
use super::Sign;
use super::ring::{Ring, Signed};
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Interval {
lo: f64,
hi: f64,
}
impl Interval {
const NAN: Interval = Interval { lo: f64::NAN, hi: f64::NAN };
#[inline(always)]
fn widened(lo: f64, hi: f64) -> Self {
if lo.is_nan() || hi.is_nan() { Self::NAN } else { Self { lo: lo.next_down(), hi: hi.next_up() } }
}
/// The midpoint, an approximation of the value.
pub(crate) fn mid(&self) -> f64 {
0.5 * self.lo + 0.5 * self.hi
}
}
impl Add for Interval {
type Output = Self;
#[inline(always)]
fn add(self, rhs: Self) -> Self {
Self::widened(self.lo + rhs.lo, self.hi + rhs.hi)
}
}
impl Sub for Interval {
type Output = Self;
#[inline(always)]
fn sub(self, rhs: Self) -> Self {
Self::widened(self.lo - rhs.hi, self.hi - rhs.lo)
}
}
impl Mul for Interval {
type Output = Self;
#[inline(always)]
fn mul(self, rhs: Self) -> Self {
let p = [self.lo * rhs.lo, self.lo * rhs.hi, self.hi * rhs.lo, self.hi * rhs.hi];
// `f64::min`/`max` skip NaN (from `0 * inf`), which would drop a bound: check first.
if p.iter().any(|x| x.is_nan()) {
return Self::NAN;
}
let lo = p.iter().copied().fold(f64::INFINITY, f64::min);
let hi = p.iter().copied().fold(f64::NEG_INFINITY, f64::max);
Self::widened(lo, hi)
}
}
impl Ring for Interval {
#[inline(always)]
fn from_f64(x: f64) -> Self {
Self { lo: x, hi: x }
}
}
impl Signed for Interval {
#[inline(always)]
fn 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
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intervals_enclose_and_flag_uncertainty() {
let third = Interval::from_f64(1.0) * Interval::from_f64(1.0 / 3.0);
let x = third + third + third - Interval::from_f64(1.0);
assert!(x.lo <= 0.0 && x.hi >= 0.0);
assert_eq!(x.sign(), None);
let big = Interval::from_f64(f64::MAX) * Interval::from_f64(2.0);
assert_eq!((big - big).sign(), None);
assert_eq!((Interval::from_f64(2.0) * Interval::from_f64(3.0)).sign(), Some(Sign::Positive));
}
}