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}