axiolid_exact/
dyadic.rs

1//! Dyadic rationals: the exact tier.
2//!
3//! A dyadic number is `mantissa * 2^exponent` with a big-integer mantissa.
4//! Every finite `f64` is one exactly, and the set is closed under `+`, `-`
5//! and `*`, so any polynomial in `f64` inputs has an exact dyadic value and
6//! an exact sign. Division is deliberately absent (ADR 0068): callers clear
7//! denominators instead.
8//!
9//! Values are kept normalised -- the mantissa odd, or zero with exponent 0 --
10//! so equal values have equal representations and mantissas stay as short
11//! as the value allows.
12
13use num_bigint::{BigInt, Sign as BigSign};
14
15use axiolid_guarantees::Sign;
16
17use crate::arith::Arith;
18use crate::interval::Interval;
19
20/// An exact value `mantissa * 2^exponent`.
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct Dyadic {
23    mantissa: BigInt,
24    exponent: i64,
25}
26
27impl Dyadic {
28    /// Zero.
29    #[must_use]
30    pub fn zero() -> Self {
31        Self {
32            mantissa: BigInt::from(0),
33            exponent: 0,
34        }
35    }
36
37    /// `mantissa * 2^exponent`, normalised.
38    #[must_use]
39    pub fn from_parts(mantissa: BigInt, exponent: i64) -> Self {
40        Self { mantissa, exponent }.normalised()
41    }
42
43    /// The exact value of a finite `f64`, or `None` for NaN and infinities.
44    #[must_use]
45    pub fn try_from_f64(value: f64) -> Option<Self> {
46        if !value.is_finite() {
47            return None;
48        }
49        let bits = value.to_bits();
50        let negative = bits >> 63 == 1;
51        let biased = ((bits >> 52) & 0x7ff) as i64;
52        let fraction = bits & ((1u64 << 52) - 1);
53        // Subnormals have no implicit leading bit and a fixed exponent.
54        let (magnitude, exponent) = if biased == 0 {
55            (fraction, -1074)
56        } else {
57            (fraction | (1u64 << 52), biased - 1075)
58        };
59        let mut mantissa = BigInt::from(magnitude);
60        if negative {
61            mantissa = -mantissa;
62        }
63        Some(Self::from_parts(mantissa, exponent))
64    }
65
66    /// The mantissa of the normalised form.
67    #[must_use]
68    pub fn mantissa(&self) -> &BigInt {
69        &self.mantissa
70    }
71
72    /// The power-of-two exponent of the normalised form.
73    #[must_use]
74    pub fn exponent(&self) -> i64 {
75        self.exponent
76    }
77
78    /// A sound `f64` interval containing the value.
79    ///
80    /// The top 64 mantissa bits are taken with a floor shift (the value
81    /// lies within one unit of them), converted with one rounding, then
82    /// scaled by an exact power of two and widened two steps each way,
83    /// which covers both errors even at a binade boundary. Values whose
84    /// scaled result would leave the normal `f64` range get the whole line:
85    /// sound, and never decisive.
86    #[must_use]
87    pub fn enclosure(&self) -> Interval {
88        if self.mantissa.sign() == BigSign::NoSign {
89            return Interval::point(0.0);
90        }
91        let bits = self.mantissa.bits();
92        let drop = bits.saturating_sub(64);
93        let exponent = self.exponent + drop as i64;
94        // top < 2^64, so the result is normal iff 2^(exponent+63) is.
95        if !(-1000..=900).contains(&exponent) {
96            return Interval::WHOLE;
97        }
98        let scale = 2f64.powi(exponent as i32);
99        if drop == 0 && bits <= 53 {
100            // Nothing dropped and the mantissa fits a double: the value is
101            // exactly representable (the scale is a power of two within
102            // the normal range), so the enclosure is a point. The filter's
103            // strength depends on this: input coordinates arrive here.
104            let exact = i64::try_from(&self.mantissa).expect("at most 53 bits") as f64 * scale;
105            return Interval::from_bounds(exact, exact);
106        }
107        // BigInt >> floors (towards -infinity), so value is in
108        // [top, top + 1] * 2^exponent; with nothing dropped it is exactly
109        // top, and only the conversion to f64 rounds.
110        let top = &self.mantissa >> drop;
111        let top = i128::try_from(&top).expect("at most 65 bits");
112        let low = (top as f64) * scale;
113        let high = if drop == 0 {
114            low
115        } else {
116            ((top + 1) as f64) * scale
117        };
118        Interval::from_bounds(low.next_down().next_down(), high.next_up().next_up())
119    }
120
121    /// `(m, e)` with the value within a relative `2^-52` of `m * 2^e`,
122    /// where `m` is a double of magnitude in `[2^52, 2^53)`, or `(0, 0)`.
123    ///
124    /// Unlike [`Dyadic::to_f64`] this never overflows or underflows, so
125    /// ratios and roots of huge or tiny values stay accurate: combine the
126    /// parts first, apply the exponent last. For output only.
127    #[must_use]
128    pub fn approx_parts(&self) -> (f64, i64) {
129        let bits = self.mantissa.bits();
130        if bits == 0 {
131            return (0.0, 0);
132        }
133        let drop = bits.saturating_sub(53);
134        let top = &self.mantissa >> drop;
135        let top = i64::try_from(&top).expect("at most 53 bits");
136        let mut m = top as f64;
137        let mut e = self.exponent + drop as i64;
138        // Normalise short mantissas up to 53 bits so every caller gets the
139        // same magnitude range.
140        let lift = 53 - bits.min(53) as i64;
141        m *= 2f64.powi(lift as i32);
142        e -= lift;
143        (m, e)
144    }
145
146    /// Bits in the mantissa: the cost driver of every operation.
147    #[must_use]
148    pub fn bits(&self) -> u64 {
149        self.mantissa.bits()
150    }
151
152    /// A nearby double, for output only; never for decisions.
153    ///
154    /// Takes the top 64 mantissa bits, so the result is within a few ulps
155    /// of the value. Overflows to an infinity and underflows to zero like
156    /// any `f64` conversion.
157    #[must_use]
158    pub fn to_f64(&self) -> f64 {
159        let bits = self.mantissa.bits();
160        let drop = bits.saturating_sub(64);
161        let top = &self.mantissa >> drop;
162        // `top` fits in an i128 comfortably (at most 64 magnitude bits).
163        let top = i128::try_from(&top).expect("at most 64 bits");
164        let exponent = self.exponent + drop as i64;
165        let exponent = exponent.clamp(-2000, 2000) as i32;
166        (top as f64) * 2f64.powi(exponent / 2) * 2f64.powi(exponent - exponent / 2)
167    }
168
169    fn normalised(mut self) -> Self {
170        match self.mantissa.trailing_zeros() {
171            // Only zero has no trailing-zero count.
172            None => Self::zero(),
173            Some(0) => self,
174            Some(shift) => {
175                self.mantissa >>= shift;
176                self.exponent += shift as i64;
177                self
178            }
179        }
180    }
181
182    /// Both mantissas on the smaller exponent.
183    fn aligned(&self, other: &Self) -> (BigInt, BigInt, i64) {
184        if self.exponent <= other.exponent {
185            let shift = (other.exponent - self.exponent) as u64;
186            (
187                self.mantissa.clone(),
188                &other.mantissa << shift,
189                self.exponent,
190            )
191        } else {
192            let shift = (self.exponent - other.exponent) as u64;
193            (
194                &self.mantissa << shift,
195                other.mantissa.clone(),
196                other.exponent,
197            )
198        }
199    }
200}
201
202impl Arith for Dyadic {
203    /// # Panics
204    ///
205    /// On NaN or an infinity, which have no exact value. The public entry
206    /// points reject them first ([`crate::require_finite`]).
207    fn from_f64(value: f64) -> Self {
208        Self::try_from_f64(value).expect("exact arithmetic needs finite input")
209    }
210
211    fn from_dyadic(value: &Dyadic) -> Self {
212        value.clone()
213    }
214
215    fn add(&self, other: &Self) -> Self {
216        let (left, right, exponent) = self.aligned(other);
217        Self::from_parts(left + right, exponent)
218    }
219
220    fn sub(&self, other: &Self) -> Self {
221        let (left, right, exponent) = self.aligned(other);
222        Self::from_parts(left - right, exponent)
223    }
224
225    fn mul(&self, other: &Self) -> Self {
226        // Odd times odd is odd, so a product of normalised values is already
227        // normalised unless it is zero. `normalised` finds the lowest set
228        // bit in the first limb for an odd mantissa, so this costs nothing
229        // measurable and keeps zero canonical.
230        Self {
231            mantissa: &self.mantissa * &other.mantissa,
232            exponent: self.exponent + other.exponent,
233        }
234        .normalised()
235    }
236
237    fn neg(&self) -> Self {
238        Self {
239            mantissa: -&self.mantissa,
240            exponent: self.exponent,
241        }
242    }
243
244    fn sign(&self) -> Option<Sign> {
245        Some(match self.mantissa.sign() {
246            BigSign::Plus => Sign::Positive,
247            BigSign::Minus => Sign::Negative,
248            BigSign::NoSign => Sign::Zero,
249        })
250    }
251}