Skip to main content

symplex/base/
numeric.rs

1//! Exact conversions between machine floats and rationals.
2//!
3//! Every finite `f64` is a dyadic rational `m · 2^e`, so it can be
4//! converted to a [`Ratio<BigInt>`] *exactly* — no rounding, no chosen
5//! denominator.  That is the right default for a library whose core
6//! promise is exact arithmetic: `0.1_f64` really is
7//! `3602879701896397/36028797018963968`, and pretending otherwise hides
8//! error.
9//!
10//! When the caller *wants* the "nice" rational a human meant
11//! (`0.1 → 1/10`), use [`f64_to_ratio_approx`], which returns the best
12//! rational approximation with a bounded denominator (Stern–Brocot /
13//! continued-fraction convergents), or the higher-level
14//! `Context::from_f64_approx`.
15//!
16//! These two functions replace the several ad-hoc `(x * 10^k).round()`
17//! helpers that used to be scattered through the crate, each with a
18//! different scale and each silently saturating on large inputs.
19
20use astro_float::{BigFloat, RoundingMode};
21use num_bigint::{BigInt, Sign};
22use num_rational::Ratio;
23use num_traits::{One, Signed, ToPrimitive, Zero};
24
25/// The exact rational number used by the exact linear algebra, linear
26/// programming, polytope and certificate modules (`Ratio<BigInt>`).
27/// Re-exported as `linprog::Q` and in the prelude.
28pub type Q = Ratio<BigInt>;
29
30/// The rational `n / d`.
31///
32/// # Panics
33///
34/// Panics if `d == 0` (a programming error, like a zero literal
35/// denominator).
36///
37/// # Examples
38///
39/// ```
40/// use symplex::linprog::q;
41/// assert_eq!(q(2, 4), q(1, 2));
42/// ```
43pub fn q(n: i64, d: i64) -> Q {
44    Ratio::new(BigInt::from(n), BigInt::from(d))
45}
46
47/// The integer `n` as a rational.
48///
49/// # Examples
50///
51/// ```
52/// use symplex::linprog::{q, qi};
53/// assert_eq!(qi(3), q(6, 2));
54/// ```
55pub fn qi(n: i64) -> Q {
56    Ratio::from_integer(BigInt::from(n))
57}
58
59/// Convert a finite `f64` to the exact rational it represents.
60///
61/// Returns `None` for NaN and ±∞.  Negative zero maps to `0`.
62///
63/// # Examples
64///
65/// ```
66/// use symplex::base::numeric::f64_to_ratio_exact;
67/// use num_bigint::BigInt;
68/// use num_rational::Ratio;
69///
70/// assert_eq!(f64_to_ratio_exact(0.5), Some(Ratio::new(BigInt::from(1), BigInt::from(2))));
71/// assert_eq!(f64_to_ratio_exact(-3.0), Some(Ratio::from_integer(BigInt::from(-3))));
72/// // 0.1 is *not* 1/10 in binary floating point:
73/// let tenth = f64_to_ratio_exact(0.1).unwrap();
74/// assert_eq!(*tenth.denom(), BigInt::from(36028797018963968_u64));
75/// assert!(f64_to_ratio_exact(f64::NAN).is_none());
76/// assert!(f64_to_ratio_exact(f64::INFINITY).is_none());
77/// ```
78#[must_use]
79pub fn f64_to_ratio_exact(x: f64) -> Option<Ratio<BigInt>> {
80    if !x.is_finite() {
81        return None;
82    }
83    if x == 0.0 {
84        return Some(Ratio::zero());
85    }
86
87    let bits = x.to_bits();
88    let sign = if bits >> 63 == 0 {
89        Sign::Plus
90    } else {
91        Sign::Minus
92    };
93    let exponent = ((bits >> 52) & 0x7ff) as i64;
94    let fraction = bits & 0x000f_ffff_ffff_ffff;
95
96    // Subnormals have an implicit leading 0 and a fixed exponent of -1074;
97    // normals have an implicit leading 1 and exponent (e - 1075) after
98    // folding the 52-bit fraction into the mantissa.
99    let (mantissa, exp2) = if exponent == 0 {
100        (fraction, -1074_i64)
101    } else {
102        (fraction | (1_u64 << 52), exponent - 1075)
103    };
104
105    let mantissa = BigInt::from_biguint(sign, mantissa.into());
106    let ratio = if exp2 >= 0 {
107        Ratio::from_integer(mantissa << (exp2 as usize))
108    } else {
109        Ratio::new(mantissa, BigInt::one() << ((-exp2) as usize))
110    };
111    Some(ratio)
112}
113
114/// Best rational approximation to `x` with denominator at most `max_denom`.
115///
116/// Walks the continued-fraction convergents (and semiconvergents) of the
117/// exact value of `x`, so the result is the *closest* rational with a
118/// denominator not exceeding `max_denom` — this is what turns `0.1` into
119/// `1/10`, `0.3333333333333333` into `1/3`, and `3.14159` into `355/113`
120/// (for `max_denom = 1000`).
121///
122/// Returns `None` for NaN / ±∞ or `max_denom == 0`.
123///
124/// # Examples
125///
126/// ```
127/// use symplex::base::numeric::f64_to_ratio_approx;
128/// use num_bigint::BigInt;
129/// use num_rational::Ratio;
130///
131/// let r = |p: i64, q: i64| Ratio::new(BigInt::from(p), BigInt::from(q));
132/// assert_eq!(f64_to_ratio_approx(0.1, 1_000_000), Some(r(1, 10)));
133/// assert_eq!(f64_to_ratio_approx(1.0 / 3.0, 1_000_000), Some(r(1, 3)));
134/// assert_eq!(f64_to_ratio_approx(0.3, 1_000_000), Some(r(3, 10)));
135/// assert_eq!(f64_to_ratio_approx(std::f64::consts::PI, 1000), Some(r(355, 113)));
136/// assert_eq!(f64_to_ratio_approx(-2.5, 10), Some(r(-5, 2)));
137/// assert_eq!(f64_to_ratio_approx(7.0, 1), Some(r(7, 1)));
138/// ```
139#[must_use]
140pub fn f64_to_ratio_approx(x: f64, max_denom: u64) -> Option<Ratio<BigInt>> {
141    if max_denom == 0 {
142        return None;
143    }
144    let exact = f64_to_ratio_exact(x)?;
145    Some(best_rational_approx(&exact, &BigInt::from(max_denom)))
146}
147
148/// Best rational approximation to `target` with denominator `≤ max_denom`,
149/// via continued-fraction convergents and the final semiconvergent.
150///
151/// `max_denom` must be `≥ 1`.
152#[must_use]
153pub fn best_rational_approx(target: &Ratio<BigInt>, max_denom: &BigInt) -> Ratio<BigInt> {
154    debug_assert!(max_denom.is_positive());
155    if target.denom() <= max_denom {
156        return target.clone();
157    }
158
159    let negative = target.is_negative();
160    let t = target.abs();
161
162    // Standard convergent recurrence:  h_n = a_n h_{n-1} + h_{n-2}, same for k.
163    let (mut h_prev, mut h) = (BigInt::one(), BigInt::zero()); // h_{-1}=1, h_{-2}=0
164    let (mut k_prev, mut k) = (BigInt::zero(), BigInt::one()); // k_{-1}=0, k_{-2}=1
165    let mut rem = t.clone();
166
167    loop {
168        let a = rem.floor().to_integer();
169        let h_next = &a * &h_prev + &h;
170        let k_next = &a * &k_prev + &k;
171
172        if &k_next > max_denom {
173            // The next convergent h_n/k_n overshoots the denominator bound.
174            // Here h_prev/k_prev = h_{n-1}/k_{n-1} (last good convergent) and
175            // h/k = h_{n-2}/k_{n-2}.  The semiconvergents are
176            //     (h_{n-2} + m·h_{n-1}) / (k_{n-2} + m·k_{n-1}),  0 < m < a_n,
177            // and the largest admissible m gives the closest one.  Pick
178            // whichever of that and the last convergent is nearer.
179            let m = (max_denom - &k) / &k_prev;
180            let candidate = if m.is_zero() {
181                None
182            } else {
183                Some(Ratio::new(&h + &m * &h_prev, &k + &m * &k_prev))
184            };
185            let conv = Ratio::new(h_prev.clone(), k_prev.clone());
186            let best = match candidate {
187                Some(semi) if (&semi - &t).abs() < (&conv - &t).abs() => semi,
188                _ => conv,
189            };
190            return if negative { -best } else { best };
191        }
192
193        h = std::mem::replace(&mut h_prev, h_next);
194        k = std::mem::replace(&mut k_prev, k_next);
195
196        let frac = &rem - Ratio::from_integer(a);
197        if frac.is_zero() {
198            // Exact: the convergent is the target itself.
199            let exact = Ratio::new(h_prev.clone(), k_prev.clone());
200            return if negative { -exact } else { exact };
201        }
202        rem = frac.recip();
203    }
204}
205
206/// Convert a rational to the nearest `f64`, returning `None` if it does
207/// not fit (overflow to ±∞ is reported as `None`).
208#[must_use]
209pub fn ratio_to_f64(r: &Ratio<BigInt>) -> Option<f64> {
210    // `to_f64` on Ratio<BigInt> performs a correctly-rounded division when
211    // both parts fit in f64 range; otherwise fall back to scaling.
212    if let Some(v) = r.to_f64()
213        && v.is_finite()
214    {
215        return Some(v);
216    }
217    // Scale down by a common power of two until both fit.
218    let shift = r.numer().bits().max(r.denom().bits()).saturating_sub(1000) as usize;
219    let n = r.numer() >> shift;
220    let d = r.denom() >> shift;
221    if d.is_zero() {
222        return None;
223    }
224    let v = n.to_f64()? / d.to_f64()?;
225    v.is_finite().then_some(v)
226}
227
228/// Convert a `BigInt` to a `BigFloat`, correctly rounded (to nearest,
229/// ties to even) to `prec` bits.
230///
231/// Values that fit in `i128` are converted directly.  Larger integers are
232/// handed to astro-float as their limbs — the float `0.limbs · 2^(64·len)`
233/// is the integer itself — and rounded once, in time linear in the size.
234/// Before 0.29 the limbs were accumulated one at a time (`acc·2⁶⁴ + limb`)
235/// at the full width of the integer, a full-width multiplication per limb:
236/// quadratic, 12 s (debug) for the 86,000-bit p-value of a binomial test,
237/// where `Ratio::to_f64` takes 70 µs.
238pub(crate) fn bigint_to_bigfloat(n: &BigInt, prec: usize) -> BigFloat {
239    if let Some(v) = n.to_i128() {
240        // astro-float builds an `i128` only at 128 bits or more (NaN below).
241        let mut out = BigFloat::from_i128(v, prec.max(128));
242        if prec < 128 {
243            let _ = out.set_precision(prec, RoundingMode::ToEven);
244        }
245        return out;
246    }
247    // astro-float's `Word` is 32 or 64 bits depending on the target (and
248    // not always on `target_pointer_width` alone), so the words are packed
249    // from 32-bit digits for whichever width it is, little-endian.
250    let (sign, digits) = n.to_u32_digits();
251    let per_word = astro_float::WORD_BIT_SIZE / 32;
252    let words: Vec<astro_float::Word> = digits
253        .chunks(per_word)
254        .map(|chunk| {
255            chunk
256                .iter()
257                .enumerate()
258                .fold(0 as astro_float::Word, |w, (i, &d)| {
259                    w | (astro_float::Word::from(d) << (32 * i))
260                })
261        })
262        .collect();
263    let sign = if sign == Sign::Minus {
264        astro_float::Sign::Neg
265    } else {
266        astro_float::Sign::Pos
267    };
268    let exponent = words
269        .len()
270        .checked_mul(astro_float::WORD_BIT_SIZE)
271        .and_then(|b| astro_float::Exponent::try_from(b).ok());
272    let Some(exponent) = exponent else {
273        // Beyond astro-float's exponent range.
274        return if sign == astro_float::Sign::Neg {
275            astro_float::INF_NEG
276        } else {
277            astro_float::INF_POS
278        };
279    };
280    let mut out = BigFloat::from_words(&words, sign, exponent);
281    let _ = out.set_precision(prec, RoundingMode::ToEven);
282    out
283}
284
285/// Convert a `Ratio<BigInt>` to a `BigFloat` at `prec` bits: numerator and
286/// denominator are rounded to `prec + 64` bits (exact when they fit, as
287/// they do for every rational of moderate size; see [`bigint_to_bigfloat`])
288/// and divided with rounding mode `rm`, so the result is within
289/// `2^(−prec)` relative of the rational however large its parts.
290pub(crate) fn ratio_to_bigfloat(r: &Ratio<BigInt>, prec: usize, rm: RoundingMode) -> BigFloat {
291    if r.is_zero() {
292        return BigFloat::from_i32(0, prec);
293    }
294    if r.denom().is_one() {
295        return bigint_to_bigfloat(r.numer(), prec);
296    }
297    let wp = prec + 64;
298    let n = bigint_to_bigfloat(r.numer(), wp);
299    let d = bigint_to_bigfloat(r.denom(), wp);
300    n.div(&d, prec, rm)
301}
302
303/// Integer square root helper for tests and callers that need exactness.
304#[must_use]
305pub fn is_perfect_square(n: &BigInt) -> bool {
306    if n.is_negative() {
307        return false;
308    }
309    let r = n.sqrt();
310    &r * &r == *n
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    fn r(p: i64, q: i64) -> Ratio<BigInt> {
318        Ratio::new(BigInt::from(p), BigInt::from(q))
319    }
320
321    #[test]
322    fn exact_round_trips_through_f64() {
323        for &x in &[
324            0.0,
325            -0.0,
326            1.0,
327            -1.0,
328            0.5,
329            0.1,
330            1.0 / 3.0,
331            1e300,
332            -1e-300,
333            f64::MIN_POSITIVE,
334            f64::MAX,
335            5e-324, // smallest subnormal
336            123_456_789.987_654_3,
337        ] {
338            let ratio = f64_to_ratio_exact(x).unwrap();
339            let back = ratio_to_f64(&ratio).unwrap();
340            assert_eq!(back.to_bits(), (x + 0.0).to_bits(), "x = {x:e}");
341        }
342    }
343
344    #[test]
345    fn exact_rejects_non_finite() {
346        assert!(f64_to_ratio_exact(f64::NAN).is_none());
347        assert!(f64_to_ratio_exact(f64::INFINITY).is_none());
348        assert!(f64_to_ratio_exact(f64::NEG_INFINITY).is_none());
349    }
350
351    #[test]
352    fn exact_known_values() {
353        assert_eq!(f64_to_ratio_exact(0.75), Some(r(3, 4)));
354        assert_eq!(f64_to_ratio_exact(-1024.0), Some(r(-1024, 1)));
355        assert_eq!(f64_to_ratio_exact(1.5e3), Some(r(1500, 1)));
356    }
357
358    #[test]
359    fn approx_recovers_human_decimals() {
360        let cases = [
361            (0.1, 1, 10),
362            (0.2, 1, 5),
363            (0.3, 3, 10),
364            (0.25, 1, 4),
365            (0.125, 1, 8),
366            (2.0 / 3.0, 2, 3),
367            (0.142857142857, 1, 7),
368        ];
369        for &(x, p, q) in &cases {
370            assert_eq!(f64_to_ratio_approx(x, 1_000_000), Some(r(p, q)), "x = {x}");
371        }
372        // √2 has no small rational form.  The f64 `SQRT_2` lies just below
373        // the true √2, so among denominators ≤ 100 the closest rational to
374        // *that float* is 140/99 (the next convergent 99/70 is above it).
375        let best = f64_to_ratio_approx(std::f64::consts::SQRT_2, 100).unwrap();
376        assert!(*best.denom() <= BigInt::from(100));
377        let exact = f64_to_ratio_exact(std::f64::consts::SQRT_2).unwrap();
378        for &(p, q) in &[(99_i64, 70_i64), (141, 100), (17, 12), (7, 5)] {
379            assert!(
380                (&best - &exact).abs() <= (&r(p, q) - &exact).abs(),
381                "{best} is worse than {p}/{q}"
382            );
383        }
384    }
385
386    #[test]
387    fn approx_respects_denominator_bound() {
388        for &md in &[1u64, 2, 3, 7, 10, 100, 1000, 1_000_000] {
389            for &x in &[
390                0.1,
391                0.7,
392                std::f64::consts::PI,
393                -std::f64::consts::E,
394                12345.6789,
395            ] {
396                let a = f64_to_ratio_approx(x, md).unwrap();
397                assert!(*a.denom() <= BigInt::from(md), "x={x}, md={md}, got {a}");
398                // Must be at least as good as naive rounding with that denominator.
399                let naive = Ratio::new(
400                    BigInt::from((x * md as f64).round() as i64),
401                    BigInt::from(md),
402                );
403                let exact = f64_to_ratio_exact(x).unwrap();
404                assert!(
405                    (&a - &exact).abs() <= (&naive - &exact).abs(),
406                    "x={x}, md={md}: {a} worse than naive {naive}"
407                );
408            }
409        }
410    }
411
412    #[test]
413    fn approx_negative_and_integer() {
414        assert_eq!(f64_to_ratio_approx(-0.5, 10), Some(r(-1, 2)));
415        assert_eq!(f64_to_ratio_approx(-7.0, 10), Some(r(-7, 1)));
416        assert_eq!(f64_to_ratio_approx(0.0, 10), Some(r(0, 1)));
417        assert!(f64_to_ratio_approx(1.0, 0).is_none());
418    }
419
420    #[test]
421    fn best_rational_exact_when_within_bound() {
422        let t = r(22, 7);
423        assert_eq!(best_rational_approx(&t, &BigInt::from(7)), t);
424        assert_eq!(best_rational_approx(&t, &BigInt::from(1000)), t);
425    }
426
427    #[test]
428    fn perfect_square() {
429        assert!(is_perfect_square(&BigInt::from(0)));
430        assert!(is_perfect_square(&BigInt::from(144)));
431        assert!(!is_perfect_square(&BigInt::from(145)));
432        assert!(!is_perfect_square(&BigInt::from(-4)));
433    }
434}