Skip to main content

axiolid_predicates/
orientation.rs

1//! Certified orientation predicates.
2//!
3//! `orient2d` answers "does C lie left of, right of, or exactly on the directed
4//! line AB". The answer is a *sign*, and a sign drives topology, so a plausible
5//! answer is not good enough: this module returns [`Certified`] and escalates
6//! to exact arithmetic rather than guessing when the filter is inconclusive.
7//!
8//! The strategy is the standard filtered cascade:
9//!
10//! 1. Evaluate the determinant in plain f64 and compute a forward error bound.
11//! 2. If the magnitude exceeds the bound, the sign is proven; return it.
12//! 3. Otherwise evaluate the determinant exactly (see below).
13//!
14//! Step 1 succeeds for almost all real inputs, so the exact path is rare.
15//!
16//! The exact path first tries an expansion: each coordinate difference is
17//! split into its rounded head and its exact rounding tail, and the products
18//! of heads and tails are summed exactly (four terms when no difference
19//! rounds, as on grid-aligned collinear points, the common degenerate case,
20//! and up to sixteen otherwise). It runs only where none of its products can
21//! overflow or underflow: every difference in [1e-90, 1e90] and every
22//! nonzero tail at least 1e-120. Any other input goes to a fixed-size dyadic
23//! evaluation over the original coordinates, as `orient3d`'s fallback does
24//! (#404): it forms no difference and no rounded product, so it is exact for
25//! every finite input. An earlier expansion evaluation had no such guard; it
26//! overflowed beyond about 1e154, lost error terms to underflow, and could
27//! misjudge coordinates of very different magnitudes. The filter itself declines an input
28//! whose differences are outside [1e-90, 1e90], where its products may
29//! overflow or underflow, and so hands it to the exact path.
30
31use axiolid_core::Point2;
32use axiolid_guarantees::{Certified, Precision, Sign};
33
34use crate::expansion::{two_diff, two_product, two_sum};
35use crate::orient2_dyadic::orient2d_exact_dyadic;
36
37/// Machine epsilon for binary64: the gap between 1.0 and the next value.
38const EPSILON: f64 = f64::EPSILON / 2.0;
39
40/// Relative error bound for the 2x2 determinant filter.
41///
42/// The determinant costs two products and one subtraction; propagating the
43/// standard `(1 + eps)` model over that expression yields `3*eps + O(eps^2)`.
44/// The `16.0 * EPSILON * EPSILON` term absorbs the higher-order remainder, so
45/// the bound is a true upper bound rather than a first-order approximation.
46const ORIENT2D_ERROR_FACTOR: f64 = (3.0 + 16.0 * EPSILON) * EPSILON;
47
48/// Orientation of `c` relative to the directed line `a` -> `b`.
49///
50/// Returns `Sign::Positive` when `a`, `b`, `c` turn counter-clockwise,
51/// `Sign::Negative` for clockwise, and `Sign::Zero` when the three points
52/// are exactly collinear.
53///
54/// The result is [`Certified::Certain`] for every finite input: this function
55/// escalates to exact arithmetic internally, so it never returns an unproven
56/// sign. A caller may therefore use it to drive a topology decision directly.
57/// A non-finite coordinate returns [`Certified::Uncertain`], not a guess.
58#[must_use]
59pub fn orient2d(a: Point2, b: Point2, c: Point2) -> Certified {
60    match orient2d_filter(a, b, c) {
61        Certified::Certain { sign, .. } => Certified::exact_sign(sign),
62        // The filter could not prove a sign, so pay for exact arithmetic. This
63        // is the whole point of the cascade: correctness does not depend on the
64        // fast path being lucky. `Certified` is non-exhaustive: a future
65        // variant we do not understand must escalate, never be assumed decisive.
66        _ => orient2d_exact_expansion(a, b, c)
67            .or_else(|| orient2d_exact_dyadic(a, b, c))
68            .map_or(
69                Certified::Uncertain {
70                    attempted: Precision::Exact,
71                },
72                Certified::exact_sign,
73            ),
74    }
75}
76
77/// Smallest nonzero rounding tail the expansion stage accepts. With the
78/// differences in [1e-90, 1e90], a tail at or above this keeps every product
79/// of two parts (at least 1e-240) and its own error term, some 2^-106 smaller
80/// still, far above the subnormal range.
81const MIN_TAIL: f64 = 1.0e-120;
82
83/// Exact sign through expansions, or `None` when that is not exact.
84///
85/// Each coordinate difference is `high + tail` exactly (`two_diff`), so the
86/// determinant is the exact sum of the products of those parts, as in the end
87/// stage of Shewchuk's `orient2dadapt`. When every difference is in the
88/// filter's range and every nonzero tail is at least [`MIN_TAIL`], each of
89/// the eight part products is exactly `hi + lo`, so the sign is that of an
90/// exact sum of up to sixteen doubles. This settles the common degenerate
91/// case, exactly collinear points on a coordinate grid, without the bit-level
92/// dyadic accumulator.
93fn orient2d_exact_expansion(a: Point2, b: Point2, c: Point2) -> Option<Sign> {
94    let [acx, bcy, acy, bcx] = differences(a, b, c)?;
95    let tails = [
96        two_diff(a.x, c.x).1,
97        two_diff(b.y, c.y).1,
98        two_diff(a.y, c.y).1,
99        two_diff(b.x, c.x).1,
100    ];
101    if tails
102        .iter()
103        .any(|t| *t != 0.0 && !(MIN_TAIL..=1.0e90).contains(&t.abs()))
104    {
105        return None;
106    }
107    let [tacx, tbcy, tacy, tbcx] = tails;
108    if tails == [0.0; 4] {
109        // The usual case: no difference rounded, so only the main products.
110        let (left_high, left_low) = two_product(acx, bcy);
111        let (right_high, right_low) = two_product(acy, bcx);
112        return sign_of_exact_sum(&[left_low, -right_low, left_high, -right_high]);
113    }
114    // Terms of `left - right`; a pair with a zero tail is skipped outright.
115    let mut terms = [0.0_f64; 16];
116    let mut count = 0;
117    // Interleaved left and right, so the near-equal products cancel early and
118    // the running expansion stays short.
119    let pairs = [
120        (acx, bcy, false),
121        (acy, bcx, true),
122        (acx, tbcy, false),
123        (acy, tbcx, true),
124        (tacx, bcy, false),
125        (tacy, bcx, true),
126        (tacx, tbcy, false),
127        (tacy, tbcx, true),
128    ];
129    for (x, y, negate) in pairs {
130        if x == 0.0 || y == 0.0 {
131            continue;
132        }
133        let (high, low) = two_product(x, y);
134        let sign = if negate { -1.0 } else { 1.0 };
135        terms[count] = sign * high;
136        terms[count + 1] = sign * low;
137        count += 2;
138    }
139    sign_of_exact_sum(&terms[..count])
140}
141
142/// Sign of the exact sum of finite doubles (Shewchuk's grow-expansion).
143fn sign_of_exact_sum(terms: &[f64]) -> Option<Sign> {
144    let mut expansion = [0.0_f64; 16];
145    let mut len = 0;
146    for &term in terms.iter().filter(|t| **t != 0.0) {
147        let mut carry = term;
148        let mut kept = 0;
149        for index in 0..len {
150            let (sum, error) = two_sum(carry, expansion[index]);
151            carry = sum;
152            // Zero errors are dropped, which keeps the expansion short when
153            // the terms cancel, as they do for collinear points.
154            if error != 0.0 {
155                expansion[kept] = error;
156                kept += 1;
157            }
158        }
159        expansion[kept] = carry;
160        len = kept + 1;
161    }
162    // Components are non-overlapping and increasing, so the last nonzero one
163    // carries the sign of the whole sum.
164    let top = expansion[..len].iter().rev().find(|v| **v != 0.0);
165    Some(match top {
166        None => Sign::Zero,
167        Some(v) if v.is_nan() => return None,
168        Some(v) if *v > 0.0 => Sign::Positive,
169        Some(_) => Sign::Negative,
170    })
171}
172
173/// The four coordinate differences of the determinant, or `None` when one
174/// overflowed or is so small that a product of two may underflow: such a
175/// determinant cannot be bounded in f64 and is left to the exact path.
176fn differences(a: Point2, b: Point2, c: Point2) -> Option<[f64; 4]> {
177    let d = [a.x - c.x, b.y - c.y, a.y - c.y, b.x - c.x];
178    d.iter()
179        .all(|v| *v == 0.0 || (1.0e-90..=1.0e90).contains(&v.abs()))
180        .then_some(d)
181}
182
183/// The fast filter alone, exposed so the escalation can be observed and tested.
184///
185/// Returns [`Certified::Uncertain`] when the floating-point determinant is too
186/// close to zero for its own error bound to exclude the opposite sign.
187#[must_use]
188pub fn orient2d_filter(a: Point2, b: Point2, c: Point2) -> Certified {
189    let Some([acx, bcy, acy, bcx]) = differences(a, b, c) else {
190        return Certified::Uncertain {
191            attempted: Precision::F64,
192        };
193    };
194    let left = acx * bcy;
195    let right = acy * bcx;
196    let determinant = left - right;
197
198    // The bound scales with the operand magnitudes: a determinant of 1e-9 is
199    // decisive for millimetre coordinates and noise for national-grid ones.
200    let magnitude = left.abs() + right.abs();
201    let error_bound = ORIENT2D_ERROR_FACTOR * magnitude;
202
203    Certified::from_filter(determinant, error_bound, Precision::F64)
204}