axiolid_predicates/static_filter.rs
1//! Static filters: bounds computed once from a coordinate range.
2//!
3//! The dynamic filter in each predicate computes a *permanent* -- a sum of
4//! absolute products -- on every call, which costs roughly as much as the
5//! determinant itself. When a caller can state an upper bound on coordinate
6//! magnitude up front (a model's bounding box, a quantisation grid), the error
7//! bound can be precomputed and the per-call work drops to one comparison.
8//!
9//! The trade is coverage, not correctness: a static bound is necessarily
10//! looser than a per-input one, so it defers more often. It never certifies a
11//! sign the dynamic filter would reject, because it is a strictly larger
12//! bound. A caller that exceeds the declared range gets `None` rather than a
13//! silently invalid answer.
14
15use axiolid_core::{Point2, Point3};
16use axiolid_guarantees::Sign;
17
18/// Machine epsilon for binary64.
19const EPSILON: f64 = f64::EPSILON / 2.0;
20
21/// Precomputed error bounds for a declared coordinate range.
22///
23/// Construct once per model, reuse across every predicate call.
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct StaticFilter {
26 /// Maximum absolute coordinate value this filter is valid for.
27 bound: f64,
28 /// Absolute error bound for the `orient2d` determinant.
29 orient2d: f64,
30 /// Absolute error bound for the `orient3d` determinant.
31 orient3d: f64,
32}
33
34impl StaticFilter {
35 /// Build bounds valid for coordinates with `|x| <= bound`.
36 ///
37 /// Returns `None` for a non-finite or non-positive bound, and for a bound
38 /// so large that the derived error bound is not finite: in both cases no
39 /// sign could be certified, and returning a filter that always defers
40 /// would hide the configuration error.
41 #[must_use]
42 pub fn new(bound: f64) -> Option<Self> {
43 if !bound.is_finite() || bound <= 0.0 {
44 return None;
45 }
46 // A coordinate difference is at most 2*bound, so a 2x2 determinant
47 // term is at most (2*bound)^2 and a 3x3 term (2*bound)^3.
48 let span = 2.0 * bound;
49 let orient2d = (3.0 + 16.0 * EPSILON) * EPSILON * (2.0 * span * span);
50 let orient3d = (7.0 + 56.0 * EPSILON) * EPSILON * (6.0 * span * span * span);
51 if !orient2d.is_finite() || !orient3d.is_finite() {
52 return None;
53 }
54 Some(Self {
55 bound,
56 orient2d,
57 orient3d,
58 })
59 }
60
61 /// The coordinate range this filter was built for.
62 #[must_use]
63 pub const fn bound(self) -> f64 {
64 self.bound
65 }
66
67 /// Whether every coordinate of a 2D point is inside the declared range.
68 #[must_use]
69 fn covers2(self, p: Point2) -> bool {
70 p.x.abs() <= self.bound && p.y.abs() <= self.bound
71 }
72
73 /// Whether every coordinate of a 3D point is inside the declared range.
74 #[must_use]
75 fn covers3(self, p: Point3) -> bool {
76 p.x.abs() <= self.bound && p.y.abs() <= self.bound && p.z.abs() <= self.bound
77 }
78}
79
80impl StaticFilter {
81 /// Try to settle `orient2d` with the precomputed bound.
82 ///
83 /// `None` means "not settled": either a point lies outside the declared
84 /// range, or the determinant is too small for the static bound. The caller
85 /// must fall back to the full predicate, which is always correct.
86 #[must_use]
87 pub fn orient2d(self, a: Point2, b: Point2, c: Point2) -> Option<Sign> {
88 if !(self.covers2(a) && self.covers2(b) && self.covers2(c)) {
89 return None;
90 }
91 let determinant = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);
92 decide(determinant, self.orient2d)
93 }
94
95 /// Try to settle `orient3d` with the precomputed bound.
96 #[must_use]
97 pub fn orient3d(self, a: Point3, b: Point3, c: Point3, d: Point3) -> Option<Sign> {
98 if !(self.covers3(a) && self.covers3(b) && self.covers3(c) && self.covers3(d)) {
99 return None;
100 }
101 let (adx, ady, adz) = (a.x - d.x, a.y - d.y, a.z - d.z);
102 let (bdx, bdy, bdz) = (b.x - d.x, b.y - d.y, b.z - d.z);
103 let (cdx, cdy, cdz) = (c.x - d.x, c.y - d.y, c.z - d.z);
104 let determinant = adz * (bdx * cdy - cdx * bdy)
105 + bdz * (cdx * ady - adx * cdy)
106 + cdz * (adx * bdy - bdx * ady);
107 decide(determinant, self.orient3d)
108 }
109}
110
111/// Certify a sign only when the magnitude strictly exceeds the bound.
112///
113/// Strict, not `>=`: a determinant exactly equal to its own error bound could
114/// have true value zero, so the sign is not proven.
115#[inline]
116#[must_use]
117fn decide(determinant: f64, bound: f64) -> Option<Sign> {
118 if determinant > bound {
119 Some(Sign::Positive)
120 } else if determinant < -bound {
121 Some(Sign::Negative)
122 } else {
123 None
124 }
125}