axiolid_guarantees/
certainty.rs

1//! Certified signs: the bridge between a float computation and a decision.
2//!
3//! No topology-changing decision may depend on an uncertified floating-point
4//! sign. A bare `f64` cannot express "I computed this and the sign is proven",
5//! so this module makes the distinction a type the compiler enforces.
6
7use crate::Precision;
8
9/// The sign of a geometric predicate, with its trustworthiness attached.
10#[non_exhaustive]
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Sign {
13    /// Certified strictly negative.
14    Negative,
15    /// Certified exactly zero (degenerate configuration).
16    Zero,
17    /// Certified strictly positive.
18    Positive,
19}
20
21impl Sign {
22    /// The sign of the negated value.
23    ///
24    /// Orientation predicates are antisymmetric: swapping two arguments must
25    /// flip the result. Expressing that as one operation keeps callers from
26    /// open-coding a match that silently mishandles `Zero`.
27    #[must_use]
28    pub const fn flip(self) -> Self {
29        match self {
30            Self::Positive => Self::Negative,
31            Self::Negative => Self::Positive,
32            Self::Zero => Self::Zero,
33        }
34    }
35}
36
37/// A predicate evaluation that may or may not be trustworthy.
38///
39/// `Uncertain` is deliberately not a sign: it carries no `Sign` payload, so a
40/// caller cannot accidentally read a value out of it. The only way to obtain a
41/// `Sign` is to handle the uncertain case explicitly.
42#[non_exhaustive]
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum Certified {
45    /// The sign is proven at the reported precision.
46    Certain {
47        /// The proven sign.
48        sign: Sign,
49        /// Precision tier at which the proof was obtained.
50        precision: Precision,
51    },
52    /// The computed value was within its own error bound of zero, so the sign
53    /// could not be determined at this precision. Escalate.
54    Uncertain {
55        /// Precision tier that failed to decide.
56        attempted: Precision,
57    },
58}
59
60impl Certified {
61    /// Certify a value against a filter's absolute error bound.
62    ///
63    /// The bound is the maximum absolute error the computation may carry. If
64    /// zero lies inside `value +- bound` the sign is not decidable here, which
65    /// is the whole point of a filtered predicate: it reports failure instead
66    /// of guessing. A non-finite value or bound is never certifiable.
67    pub fn from_filter(value: f64, error_bound: f64, precision: Precision) -> Self {
68        if !value.is_finite() || !error_bound.is_finite() || error_bound < 0.0 {
69            return Self::Uncertain {
70                attempted: precision,
71            };
72        }
73        if value > error_bound {
74            Self::Certain {
75                sign: Sign::Positive,
76                precision,
77            }
78        } else if value < -error_bound {
79            Self::Certain {
80                sign: Sign::Negative,
81                precision,
82            }
83        } else {
84            Self::Uncertain {
85                attempted: precision,
86            }
87        }
88    }
89
90    /// Certify an exact computation. Only valid where no rounding occurred.
91    ///
92    /// An exact zero is a *certain* answer -- the configuration is genuinely
93    /// degenerate -- which is different from being unable to decide.
94    /// A sign established by exact arithmetic, where no integer value exists.
95    ///
96    /// An exact predicate cascade produces a proven sign without producing a
97    /// representable magnitude: the determinant lives in a multi-term
98    /// expansion, not one `i64`. Without this constructor such a result could
99    /// only be reported as `Uncertain`, which would discard the very proof the
100    /// exact path was paid for.
101    pub const fn exact_sign(sign: Sign) -> Self {
102        Self::Certain {
103            sign,
104            precision: Precision::Exact,
105        }
106    }
107
108    pub const fn exact(value: i64) -> Self {
109        let sign = if value > 0 {
110            Sign::Positive
111        } else if value < 0 {
112            Sign::Negative
113        } else {
114            Sign::Zero
115        };
116        Self::Certain {
117            sign,
118            precision: Precision::Exact,
119        }
120    }
121
122    /// The proven sign, or `None` when escalation is required.
123    pub const fn sign(self) -> Option<Sign> {
124        match self {
125            Self::Certain { sign, .. } => Some(sign),
126            Self::Uncertain { .. } => None,
127        }
128    }
129
130    /// Whether this result is safe to drive a topology decision.
131    pub const fn is_certain(self) -> bool {
132        matches!(self, Self::Certain { .. })
133    }
134}
135
136/// The precision tiers a filtered predicate steps through, weakest first.
137///
138/// This is the executable form of the fast-path/escalation design: try a cheap
139/// filter, and only pay for stronger arithmetic on the cases that need it. A
140/// backend advertises how far it can escalate; a caller learns whether the
141/// answer it got was cheap or expensive.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143pub struct EscalationLadder {
144    ceiling: Precision,
145}
146
147impl EscalationLadder {
148    /// Tiers in escalation order. `Mixed` is not a rung: it describes an
149    /// operation's internal strategy, not a step of increasing certainty.
150    const RUNGS: [Precision; 3] = [Precision::F32, Precision::F64, Precision::Exact];
151
152    /// A ladder that may escalate up to and including `ceiling`.
153    pub const fn new(ceiling: Precision) -> Self {
154        Self { ceiling }
155    }
156
157    /// A ladder ending in exact arithmetic: every sign is decidable.
158    pub const fn exact() -> Self {
159        Self::new(Precision::Exact)
160    }
161
162    /// Highest precision this ladder may reach.
163    pub const fn ceiling(self) -> Precision {
164        self.ceiling
165    }
166
167    /// Whether reaching `ceiling` guarantees every sign becomes decidable.
168    ///
169    /// Only exact arithmetic does. A ladder topping out at `F64` can still
170    /// return `Uncertain`, and a caller that needs a decision must know that.
171    pub const fn is_total(self) -> bool {
172        matches!(self.ceiling, Precision::Exact)
173    }
174
175    /// The next tier to try after `current`, or `None` at the ceiling.
176    pub fn next_after(self, current: Precision) -> Option<Precision> {
177        let index = Self::RUNGS.iter().position(|&rung| rung == current)?;
178        Self::RUNGS
179            .iter()
180            .skip(index + 1)
181            .copied()
182            .find(|&rung| self.permits(rung))
183    }
184
185    /// Whether this ladder may use `precision`.
186    pub fn permits(self, precision: Precision) -> bool {
187        let Some(rung) = Self::rung_index(precision) else {
188            return false;
189        };
190        match Self::rung_index(self.ceiling) {
191            Some(ceiling) => rung <= ceiling,
192            None => false,
193        }
194    }
195
196    /// Tiers this ladder will actually attempt, in order.
197    pub fn rungs(self) -> impl Iterator<Item = Precision> {
198        Self::RUNGS
199            .into_iter()
200            .filter(move |&rung| self.permits(rung))
201    }
202
203    fn rung_index(precision: Precision) -> Option<usize> {
204        Self::RUNGS.iter().position(|&rung| rung == precision)
205    }
206}