forked from 1b-agent/fieldcraft-upstream
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:
17 files changed
+2683
-2
No files matched your search
@@ -0,0 +1,316 @@
|
||||
//! Floating-point expansions (Shewchuk 1997, §2.2–§2.6).
|
||||
//!
|
||||
//! An expansion is a sum of doubles, stored in order of increasing magnitude, whose components
|
||||
//! are nonoverlapping (here always nonadjacent or strongly nonoverlapping, as the theorems that
|
||||
//! the algorithms below rely on require). Zero components are eliminated, so a zero expansion is
|
||||
//! the single component `0.0` and the sign of any expansion is the sign of its last component.
|
||||
//!
|
||||
//! The two algorithms (Fast-Expansion-Sum with zero elimination, Theorem 13; Scale-Expansion with
|
||||
//! zero elimination, Theorem 19) are written once against a sink. [`Exp`] is a fixed-capacity
|
||||
//! stack expansion for the adaptive stages, whose capacities are checked at compile time;
|
||||
//! [`Expansion`] is a heap expansion with exact `+`, `-` and `*` for the final exact stage.
|
||||
|
||||
use std::ops::{Add, Mul, Neg, Sub};
|
||||
|
||||
use super::Sign;
|
||||
use super::eft::{fast_two_sum, split, two_diff, two_product, two_product_presplit, two_sum};
|
||||
use super::ring::{Ring, Signed};
|
||||
|
||||
/// Merges two expansions into one stream of components of nondecreasing magnitude.
|
||||
struct Merge<'a> {
|
||||
e: &'a [f64],
|
||||
f: &'a [f64],
|
||||
}
|
||||
|
||||
impl Iterator for Merge<'_> {
|
||||
type Item = f64;
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self) -> Option<f64> {
|
||||
match (self.e.split_first(), self.f.split_first()) {
|
||||
(Some((&a, e_rest)), Some((&b, f_rest))) => {
|
||||
// `(b > a) == (b > -a)` is true when |a| < |b| (Shewchuk's comparison).
|
||||
if (b > a) == (b > -a) {
|
||||
self.e = e_rest;
|
||||
Some(a)
|
||||
} else {
|
||||
self.f = f_rest;
|
||||
Some(b)
|
||||
}
|
||||
}
|
||||
(Some((&a, rest)), None) => {
|
||||
self.e = rest;
|
||||
Some(a)
|
||||
}
|
||||
(None, Some((&b, rest))) => {
|
||||
self.f = rest;
|
||||
Some(b)
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast-Expansion-Sum with zero elimination: emits `e + f`, at most `e.len() + f.len()`
|
||||
/// components.
|
||||
#[inline]
|
||||
fn sum_into(e: &[f64], f: &[f64], mut emit: impl FnMut(f64)) {
|
||||
let mut merged = Merge { e, f };
|
||||
let Some(mut q) = merged.next() else {
|
||||
emit(0.0);
|
||||
return;
|
||||
};
|
||||
let mut emitted = false;
|
||||
if let Some(x) = merged.next() {
|
||||
let (q_new, h) = fast_two_sum(x, q);
|
||||
q = q_new;
|
||||
if h != 0.0 {
|
||||
emit(h);
|
||||
emitted = true;
|
||||
}
|
||||
for x in merged {
|
||||
let (q_new, h) = two_sum(q, x);
|
||||
q = q_new;
|
||||
if h != 0.0 {
|
||||
emit(h);
|
||||
emitted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if q != 0.0 || !emitted {
|
||||
emit(q);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale-Expansion with zero elimination: emits `e * b`, at most `2 * e.len()` components.
|
||||
#[inline]
|
||||
fn scale_into(e: &[f64], b: f64, mut emit: impl FnMut(f64)) {
|
||||
let (b_hi, b_lo) = split(b);
|
||||
let mut components = e.iter().copied();
|
||||
let Some(first) = components.next() else {
|
||||
emit(0.0);
|
||||
return;
|
||||
};
|
||||
let mut emitted = false;
|
||||
let (mut q, h) = two_product_presplit(first, b, b_hi, b_lo);
|
||||
if h != 0.0 {
|
||||
emit(h);
|
||||
emitted = true;
|
||||
}
|
||||
for component in components {
|
||||
let (p1, p0) = two_product_presplit(component, b, b_hi, b_lo);
|
||||
let (s, h) = two_sum(q, p0);
|
||||
if h != 0.0 {
|
||||
emit(h);
|
||||
emitted = true;
|
||||
}
|
||||
let (q_new, h) = fast_two_sum(p1, s);
|
||||
q = q_new;
|
||||
if h != 0.0 {
|
||||
emit(h);
|
||||
emitted = true;
|
||||
}
|
||||
}
|
||||
if q != 0.0 || !emitted {
|
||||
emit(q);
|
||||
}
|
||||
}
|
||||
|
||||
/// The sign of a zero-eliminated expansion: the sign of its most significant component.
|
||||
fn sign_of(components: &[f64]) -> Sign {
|
||||
components.last().map_or(Sign::Zero, |&x| Sign::of(x))
|
||||
}
|
||||
|
||||
/// A stack expansion of at most `N` components. Constructors that combine expansions check at
|
||||
/// compile time that `N` holds every component the operation can produce, so `push` never runs
|
||||
/// out of room.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Exp<const N: usize> {
|
||||
c: [f64; N],
|
||||
n: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> Exp<N> {
|
||||
#[inline(always)]
|
||||
fn empty() -> Self {
|
||||
Self { c: [0.0; N], n: 0 }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn push(&mut self, x: f64) {
|
||||
// In bounds by the compile-time capacity checks of the constructors.
|
||||
if let Some(slot) = self.c.get_mut(self.n) {
|
||||
*slot = x;
|
||||
self.n += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn as_slice(&self) -> &[f64] {
|
||||
self.c.get(..self.n).unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// The expansion `hi + lo` produced by an error-free transformation (`lo` the roundoff).
|
||||
#[inline(always)]
|
||||
pub(crate) fn from_pair((hi, lo): (f64, f64)) -> Self {
|
||||
const { assert!(N >= 2) };
|
||||
let mut out = Self::empty();
|
||||
if lo != 0.0 {
|
||||
out.push(lo);
|
||||
}
|
||||
if hi != 0.0 || out.n == 0 {
|
||||
out.push(hi);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `e + f`.
|
||||
#[inline]
|
||||
pub(crate) fn sum<const A: usize, const B: usize>(e: &Exp<A>, f: &Exp<B>) -> Self {
|
||||
const { assert!(N >= A + B) };
|
||||
let mut out = Self::empty();
|
||||
sum_into(e.as_slice(), f.as_slice(), |x| out.push(x));
|
||||
out
|
||||
}
|
||||
|
||||
/// `e * b`.
|
||||
#[inline]
|
||||
pub(crate) fn scale<const A: usize>(e: &Exp<A>, b: f64) -> Self {
|
||||
const { assert!(N >= 2 * A) };
|
||||
let mut out = Self::empty();
|
||||
scale_into(e.as_slice(), b, |x| out.push(x));
|
||||
out
|
||||
}
|
||||
|
||||
/// `-e`, exactly.
|
||||
#[inline]
|
||||
pub(crate) fn negated(mut self) -> Self {
|
||||
if let Some(live) = self.c.get_mut(..self.n) {
|
||||
live.iter_mut().for_each(|x| *x = -*x);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// An approximation of the value (Shewchuk's `estimate`): the sum of the components.
|
||||
#[inline]
|
||||
pub(crate) fn estimate(&self) -> f64 {
|
||||
self.as_slice().iter().sum()
|
||||
}
|
||||
|
||||
pub(crate) fn sign(&self) -> Sign {
|
||||
sign_of(self.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
/// The exact two-by-two product difference `a * b - c * d` as a four-component expansion
|
||||
/// (Shewchuk's Two-Product followed by Two-Two-Diff).
|
||||
#[inline]
|
||||
pub(crate) fn cross_diff(a: f64, b: f64, c: f64, d: f64) -> Exp<4> {
|
||||
let ab = Exp::<2>::from_pair(two_product(a, b));
|
||||
let cd = Exp::<2>::from_pair(two_product(c, d)).negated();
|
||||
Exp::sum(&ab, &cd)
|
||||
}
|
||||
|
||||
/// A heap expansion with exact ring operations, for the final exact stage of a predicate.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Expansion(Vec<f64>);
|
||||
|
||||
impl Expansion {
|
||||
/// The exact difference `a - b`.
|
||||
pub(crate) fn diff(a: f64, b: f64) -> Self {
|
||||
Self(Exp::<2>::from_pair(two_diff(a, b)).as_slice().to_vec())
|
||||
}
|
||||
|
||||
fn scaled(&self, b: f64) -> Self {
|
||||
let mut v = Vec::with_capacity(2 * self.0.len());
|
||||
scale_into(&self.0, b, |x| v.push(x));
|
||||
Self(v)
|
||||
}
|
||||
|
||||
pub(crate) fn sign(&self) -> Sign {
|
||||
sign_of(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Expansion {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
let mut v = Vec::with_capacity(self.0.len() + rhs.0.len());
|
||||
sum_into(&self.0, &rhs.0, |x| v.push(x));
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Expansion {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(mut self) -> Self {
|
||||
for x in self.0.iter_mut() {
|
||||
*x = -*x;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Expansion {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self {
|
||||
self + (-rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for Expansion {
|
||||
type Output = Self;
|
||||
|
||||
/// The sum of `self` scaled by each component of `rhs` (distributivity; every partial
|
||||
/// product is exact).
|
||||
#[allow(clippy::suspicious_arithmetic_impl)] // the product is a sum of scaled expansions
|
||||
fn mul(self, rhs: Self) -> Self {
|
||||
let (long, short) = if self.0.len() >= rhs.0.len() { (self, rhs) } else { (rhs, self) };
|
||||
let mut terms = short.0.iter().map(|&b| long.scaled(b));
|
||||
let first = terms.next().unwrap_or_else(|| Self(vec![0.0]));
|
||||
terms.fold(first, |acc, term| acc + term)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ring for Expansion {
|
||||
fn from_f64(x: f64) -> Self {
|
||||
Self(vec![x])
|
||||
}
|
||||
}
|
||||
|
||||
impl Signed for Expansion {
|
||||
fn sign(&self) -> Option<Sign> {
|
||||
Some(sign_of(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sums_and_products_are_exact() {
|
||||
let big = Expansion::from_f64(1e300);
|
||||
let tiny = Expansion::from_f64(1e-300);
|
||||
let s = big.clone() + tiny.clone();
|
||||
assert_eq!(s.0, vec![1e-300, 1e300]);
|
||||
assert_eq!((s - big).0, vec![1e-300]);
|
||||
let a = Expansion::diff(1.0, 1e-20); // 1 - 1e-20, two components
|
||||
let sq = a.clone() * a;
|
||||
// (1 - x)^2 = 1 - 2x + x^2 with x = 1e-20 (as doubles: the x^2 term is exact too).
|
||||
let expect = Expansion::from_f64(1.0) - Expansion::from_f64(2e-20) + Expansion::from_f64(1e-20) * Expansion::from_f64(1e-20);
|
||||
assert_eq!((sq - expect).sign(), Sign::Zero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_and_heap_agree() {
|
||||
let e = cross_diff(3.0, 1e17, 1.0, 3e17 - 64.0);
|
||||
assert_eq!(e.estimate(), 64.0);
|
||||
let s = Exp::<8>::scale(&e, 1.0 + f64::EPSILON);
|
||||
assert_eq!(s.sign(), Sign::Positive);
|
||||
assert_eq!(Exp::<4>::from_pair((0.0, 0.0)).as_slice(), &[0.0]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user