Skip to main content

axiolid_exact_arrangement/
ratio.rs

1//! Rounding of an exact ratio to binary64.
2
3use axiolid_exact::Dyadic;
4use num_bigint::{BigUint, Sign as BigSign};
5
6/// `numerator / denominator` rounded once to the nearest binary64, ties to
7/// even, subnormals included. `None` for a zero denominator or a result
8/// beyond the finite range.
9pub fn round_ratio(numerator: &Dyadic, denominator: &Dyadic) -> Option<f64> {
10    let n = numerator.mantissa().magnitude();
11    let d = denominator.mantissa().magnitude();
12    if d.bits() == 0 {
13        return None;
14    }
15    if n.bits() == 0 {
16        return Some(0.0);
17    }
18    let negative = (numerator.mantissa().sign() == BigSign::Minus)
19        != (denominator.mantissa().sign() == BigSign::Minus);
20
21    // q = floor(n·2^k / d) has 55 or 56 bits; the value is (q + frac)·2^e.
22    let k = 55 + d.bits() as i64 - n.bits() as i64;
23    let (q, sticky) = if k >= 0 {
24        let scaled = n << k as u64;
25        (&scaled / d, (&scaled % d).bits() != 0)
26    } else {
27        let scaled = d << (-k) as u64;
28        (n / &scaled, (n % &scaled).bits() != 0)
29    };
30    let e = numerator.exponent() - denominator.exponent() - k;
31    let q_bits = q.bits() as i64;
32    // The lowest kept bit: 53 significant bits, or the subnormal grid.
33    let lsb = (e + q_bits - 1 - 52).max(-1074);
34    let drop = lsb - e;
35    if drop > q_bits {
36        return Some(0.0);
37    }
38    let kept = &q >> drop as u64;
39    let rest = &q - (&kept << drop as u64);
40    let half = BigUint::from(1u8) << (drop - 1) as u64;
41    let round_up = match rest.cmp(&half) {
42        std::cmp::Ordering::Greater => true,
43        std::cmp::Ordering::Equal => sticky || kept.bit(0),
44        std::cmp::Ordering::Less => false,
45    };
46    let mantissa = u64::try_from(&kept).ok()? + u64::from(round_up);
47    if mantissa == 0 {
48        return Some(0.0);
49    }
50    let top = lsb + (64 - mantissa.leading_zeros() as i64);
51    if top > 1024 {
52        return None;
53    }
54    let magnitude = scale_pow2(mantissa as f64, lsb);
55    Some(if negative { -magnitude } else { magnitude })
56}
57
58/// `x · 2^e` for an `x` and `e` whose product is exactly representable.
59fn scale_pow2(mut x: f64, mut e: i64) -> f64 {
60    let pow2 = |s: i64| f64::from_bits(((1023 + s) as u64) << 52);
61    while e != 0 {
62        let step = e.clamp(-500, 500);
63        x *= pow2(step);
64        e -= step;
65    }
66    x
67}
68
69#[cfg(test)]
70mod tests {
71    use axiolid_exact::Arith;
72
73    use super::*;
74
75    fn dyadic(x: f64) -> Dyadic {
76        Dyadic::try_from_f64(x).expect("finite")
77    }
78
79    /// A small seeded generator; the tests need spread, not quality.
80    struct Lcg(u64);
81
82    impl Lcg {
83        fn next(&mut self) -> u64 {
84            self.0 = self
85                .0
86                .wrapping_mul(6364136223846793005)
87                .wrapping_add(1442695040888963407);
88            self.0 >> 11
89        }
90    }
91
92    #[test]
93    fn division_of_doubles_matches_ieee() {
94        let mut rng = Lcg(7);
95        for _ in 0..4000 {
96            let scale = |r: u64| {
97                let mantissa = (r >> 3) as f64 + 1.0;
98                let shift = (r % 61) as i32 - 30;
99                mantissa * 2f64.powi(shift) * if r & 1 == 0 { 1.0 } else { -1.0 }
100            };
101            let (n, d) = (scale(rng.next()), scale(rng.next()));
102            let got = round_ratio(&dyadic(n), &dyadic(d)).expect("in range");
103            assert!(got.to_bits() == (n / d).to_bits(), "{n} / {d}");
104        }
105    }
106
107    #[test]
108    fn ties_round_to_even_and_sticky_bits_round_up() {
109        let ulp = f64::EPSILON;
110        // 1 + 2^-53 is the tie between 1 and 1 + 2^-52: even is 1.
111        let tie = dyadic(1.0).add(&dyadic(ulp / 2.0));
112        assert_eq!(round_ratio(&tie, &dyadic(1.0)), Some(1.0));
113        // 1 + 3·2^-53 is the tie between 1 + 2^-52 and 1 + 2^-51: even is the latter.
114        let tie = dyadic(1.0).add(&dyadic(1.5 * ulp));
115        assert_eq!(round_ratio(&tie, &dyadic(1.0)), Some(1.0 + 2.0 * ulp));
116        // Just above the first tie rounds up.
117        let above = dyadic(1.0)
118            .add(&dyadic(ulp / 2.0))
119            .add(&dyadic(2f64.powi(-200)));
120        assert_eq!(round_ratio(&above, &dyadic(1.0)), Some(1.0 + ulp));
121        // The same tie plus a remainder only the sticky bit sees: the
122        // quotient needs a division that does not terminate.
123        let sticky = dyadic(3.0)
124            .add(&dyadic(1.5 * ulp))
125            .add(&dyadic(2f64.powi(-200)));
126        assert_eq!(round_ratio(&sticky, &dyadic(3.0)), Some(1.0 + ulp));
127        // Without the remainder it is the tie again, and rounds to even.
128        let tie = dyadic(3.0).add(&dyadic(1.5 * ulp));
129        assert_eq!(round_ratio(&tie, &dyadic(3.0)), Some(1.0));
130    }
131
132    #[test]
133    fn subnormals_and_underflow_round_correctly() {
134        let min = f64::from_bits(1);
135        assert_eq!(round_ratio(&dyadic(min), &dyadic(1.0)), Some(min));
136        // Exactly half the smallest subnormal ties to even (zero)...
137        let half = Dyadic::from_parts(1.into(), -1075);
138        assert_eq!(round_ratio(&half, &dyadic(1.0)), Some(0.0));
139        // ...and anything above rounds up to it.
140        let above = half.add(&Dyadic::from_parts(1.into(), -1200));
141        assert_eq!(round_ratio(&above, &dyadic(1.0)), Some(min));
142        // Three halves of the smallest subnormal ties to the even 2·min.
143        let three_halves = Dyadic::from_parts(3.into(), -1075);
144        assert_eq!(round_ratio(&three_halves, &dyadic(1.0)), Some(2.0 * min));
145        // Far below the grid.
146        let tiny = Dyadic::from_parts(1.into(), -3000);
147        assert_eq!(round_ratio(&tiny, &dyadic(1.0)), Some(0.0));
148    }
149
150    #[test]
151    fn overflow_and_zero_denominator_are_refused() {
152        let huge = Dyadic::from_parts(1.into(), 1024);
153        assert_eq!(round_ratio(&huge, &dyadic(1.0)), None);
154        assert_eq!(round_ratio(&dyadic(1.0), &Dyadic::zero()), None);
155        let max = dyadic(f64::MAX);
156        assert_eq!(round_ratio(&max, &dyadic(1.0)), Some(f64::MAX));
157        // Rounds up past MAX.
158        let past = max.add(&Dyadic::from_parts(1.into(), 970));
159        assert_eq!(round_ratio(&past, &dyadic(1.0)), None);
160    }
161
162    #[test]
163    fn signs_combine_and_zero_is_positive_zero() {
164        assert_eq!(round_ratio(&dyadic(-3.0), &dyadic(4.0)), Some(-0.75));
165        assert_eq!(round_ratio(&dyadic(3.0), &dyadic(-4.0)), Some(-0.75));
166        assert_eq!(round_ratio(&dyadic(-3.0), &dyadic(-4.0)), Some(0.75));
167        let zero = round_ratio(&Dyadic::zero(), &dyadic(-4.0)).expect("zero");
168        assert_eq!(zero.to_bits(), 0.0f64.to_bits());
169    }
170}