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}