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 recompute exactly with error-free transformations.
13//!
14//! Step 1 succeeds for almost all real inputs, so the exact path is rare.
15
16use axiolid_core::Point2;
17use axiolid_guarantees::{Certified, Precision, Sign};
18
19use crate::expansion::{two_diff, two_product, two_sum};
20
21/// Machine epsilon for binary64: the gap between 1.0 and the next value.
22const EPSILON: f64 = f64::EPSILON / 2.0;
23
24/// Relative error bound for the 2x2 determinant filter.
25///
26/// The determinant costs two products and one subtraction; propagating the
27/// standard `(1 + eps)` model over that expression yields `3*eps + O(eps^2)`.
28/// The `16.0 * EPSILON * EPSILON` term absorbs the higher-order remainder, so
29/// the bound is a true upper bound rather than a first-order approximation.
30const ORIENT2D_ERROR_FACTOR: f64 = (3.0 + 16.0 * EPSILON) * EPSILON;
31
32/// Orientation of `c` relative to the directed line `a` -> `b`.
33///
34/// Returns [`Sign::Positive`] when `a`, `b`, `c` turn counter-clockwise,
35/// [`Sign::Negative`] for clockwise, and [`Sign::Zero`] when the three points
36/// are exactly collinear.
37///
38/// The result is always [`Certified::Certain`]: this function escalates to
39/// exact arithmetic internally, so it never returns an unproven sign. A caller
40/// may therefore use it to drive a topology decision directly.
41#[must_use]
42pub fn orient2d(a: Point2, b: Point2, c: Point2) -> Certified {
43 match orient2d_filter(a, b, c) {
44 Certified::Certain { sign, .. } => Certified::exact_sign(sign),
45 // The filter could not prove a sign, so pay for exact arithmetic. This
46 // is the whole point of the cascade: correctness does not depend on the
47 // fast path being lucky.
48 Certified::Uncertain { .. } => Certified::exact_sign(orient2d_exact(a, b, c)),
49 // `Certified` is non-exhaustive. A future variant we do not understand
50 // must escalate, never be assumed decisive.
51 _ => Certified::exact_sign(orient2d_exact(a, b, c)),
52 }
53}
54
55/// The fast filter alone, exposed so the escalation can be observed and tested.
56///
57/// Returns [`Certified::Uncertain`] when the floating-point determinant is too
58/// close to zero for its own error bound to exclude the opposite sign.
59#[must_use]
60pub fn orient2d_filter(a: Point2, b: Point2, c: Point2) -> Certified {
61 let left = (a.x - c.x) * (b.y - c.y);
62 let right = (a.y - c.y) * (b.x - c.x);
63 let determinant = left - right;
64
65 // The bound scales with the operand magnitudes: a determinant of 1e-9 is
66 // decisive for millimetre coordinates and noise for national-grid ones.
67 let magnitude = left.abs() + right.abs();
68 let error_bound = ORIENT2D_ERROR_FACTOR * magnitude;
69
70 Certified::from_filter(determinant, error_bound, Precision::F64)
71}
72
73/// Exact sign of the orientation determinant.
74///
75/// Every intermediate is carried as an unevaluated (value, error) pair, so no
76/// bit of the determinant is discarded. The sign of the resulting expansion is
77/// the sign of its largest-magnitude non-zero component.
78#[must_use]
79fn orient2d_exact(a: Point2, b: Point2, c: Point2) -> Sign {
80 // Coordinate differences, exactly.
81 let (acx, acx_err) = two_diff(a.x, c.x);
82 let (bcy, bcy_err) = two_diff(b.y, c.y);
83 let (acy, acy_err) = two_diff(a.y, c.y);
84 let (bcx, bcx_err) = two_diff(b.x, c.x);
85
86 // The determinant is (acx * bcy) - (acy * bcx). Each product is expanded to
87 // four terms: the two rounded halves plus their cross-error contributions.
88 let (left, left_err) = two_product(acx, bcy);
89 let (right, right_err) = two_product(acy, bcx);
90
91 // Correction terms for the fact that acx/bcy themselves carried errors.
92 let left_correction = acx * bcy_err + acx_err * bcy + acx_err * bcy_err;
93 let right_correction = acy * bcx_err + acy_err * bcx + acy_err * bcx_err;
94
95 // Sum the expansion from smallest to largest so no term is absorbed early.
96 let (head, head_err) = two_diff(left, right);
97 // `head_err` is provably zero here. This function is only reached when the
98 // filter was uncertain, which means |left - right| is small relative to
99 // |left| + |right|; by Sterbenz's lemma such a subtraction is exact. The
100 // term is kept so the expression stays a correct expansion sum if this
101 // function is ever called directly, and asserted so the assumption is
102 // checked rather than believed.
103 debug_assert_eq!(head_err, 0.0, "filtered inputs make this subtraction exact");
104 let tail = (left_err - right_err) + (left_correction - right_correction);
105 let (total, total_err) = two_sum(head, head_err + tail);
106
107 // The most significant non-zero component determines the sign; a zero head
108 // with a non-zero tail means the leading terms cancelled exactly.
109 let value = if total != 0.0 { total } else { total_err };
110 if value > 0.0 {
111 Sign::Positive
112 } else if value < 0.0 {
113 Sign::Negative
114 } else {
115 Sign::Zero
116 }
117}