Files
OneCAD-Simulation/crates/mesh/src/predicates/interval.rs
T
aa68bbbcdf Constrained Delaunay tetrahedralization (boundary recovery) and mesh.tet.generate (M2) (#29)
* predicates: LNC points, generic insphere and its perturbed form (Diazzi et al. 2023 §4.3)

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

* mesh: constrained Delaunay tetrahedralization (segment recovery with LNC Steiner points, gift-wrapping facet recovery, labelling)

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

* cdt: rounding repair, mesh.tet.generate, corpus CDT column; review cleanups (shared perturbation rule, one facet scan, cached coordinates, worklist segment recovery)

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

* mesh.tet.generate is ready in the catalogue; corpus table with the CDT column

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>
2026-10-13 18:37:09 -04:00

117 lines
3.3 KiB
Rust

//! 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 lower bound.
pub(crate) fn lo(&self) -> f64 {
self.lo
}
/// The upper bound.
pub(crate) fn hi(&self) -> f64 {
self.hi
}
/// 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));
}
}