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}