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}