|
27 | 27 | //! Since all arithmetic is exact, any non-zero pivot gives the correct result |
28 | 28 | //! (there is no numerical stability concern). Every finite `f64` is exactly |
29 | 29 | //! representable as a rational, so the result is provably correct. |
| 30 | +//! |
| 31 | +//! ## f64 → `BigRational` conversion |
| 32 | +//! |
| 33 | +//! All entry conversions use `f64_to_bigrational`, which decomposes the |
| 34 | +//! IEEE 754 binary64 bit representation (\[9\]) into sign, exponent, and |
| 35 | +//! significand and constructs a `BigRational` directly — avoiding the GCD |
| 36 | +//! normalization that `BigRational::from_float` performs. See Goldberg |
| 37 | +//! \[10\] for background on floating-point representation and exact |
| 38 | +//! rational reconstruction. Reference numbers refer to `REFERENCES.md`. |
30 | 39 |
|
31 | 40 | use num_bigint::{BigInt, Sign}; |
32 | 41 | use num_rational::BigRational; |
@@ -67,15 +76,62 @@ fn validate_finite_vec<const D: usize>(v: &Vector<D>) -> Result<(), LaError> { |
67 | 76 | Ok(()) |
68 | 77 | } |
69 | 78 |
|
70 | | -/// Convert an `f64` to an exact `BigRational`. |
| 79 | +/// Convert an `f64` to an exact `BigRational` via IEEE 754 bit decomposition. |
71 | 80 | /// |
72 | | -/// Every finite `f64` is exactly representable as a rational number (`m × 2^e`), |
73 | | -/// so this conversion is lossless. |
| 81 | +/// Every finite `f64` is exactly representable as `±m × 2^e` where `m` is a |
| 82 | +/// non-negative integer and `e` is an integer. This function extracts `(m, e)` |
| 83 | +/// directly from the IEEE 754 binary64 bit layout \[9\], strips trailing zeros |
| 84 | +/// from `m` so the resulting fraction is already in lowest terms, then |
| 85 | +/// constructs a `BigRational` via `new_raw` — bypassing the GCD reduction |
| 86 | +/// that `BigRational::from_float` performs internally. |
| 87 | +/// |
| 88 | +/// See `REFERENCES.md` \[9-10\] for the IEEE 754 standard and Goldberg's |
| 89 | +/// survey of floating-point representation. |
74 | 90 | /// |
75 | 91 | /// # Panics |
76 | 92 | /// Panics if `x` is NaN or infinite. |
77 | 93 | fn f64_to_bigrational(x: f64) -> BigRational { |
78 | | - BigRational::from_float(x).expect("non-finite matrix entry in exact determinant") |
| 94 | + let bits = x.to_bits(); |
| 95 | + let biased_exp = ((bits >> 52) & 0x7FF) as i32; |
| 96 | + let fraction = bits & 0x000F_FFFF_FFFF_FFFF; |
| 97 | + |
| 98 | + // ±0.0 |
| 99 | + if biased_exp == 0 && fraction == 0 { |
| 100 | + return BigRational::from_integer(BigInt::from(0)); |
| 101 | + } |
| 102 | + |
| 103 | + // NaN / Inf — callers must validate finiteness before reaching here. |
| 104 | + assert!(biased_exp != 0x7FF, "non-finite f64 in exact conversion"); |
| 105 | + |
| 106 | + let (mantissa, raw_exp) = if biased_exp == 0 { |
| 107 | + // Subnormal: (-1)^s × 0.fraction × 2^(-1022) |
| 108 | + // = (-1)^s × fraction × 2^(-1074) |
| 109 | + (fraction, -1074_i32) |
| 110 | + } else { |
| 111 | + // Normal: (-1)^s × 1.fraction × 2^(biased_exp - 1023) |
| 112 | + // = (-1)^s × (2^52 | fraction) × 2^(biased_exp - 1075) |
| 113 | + ((1u64 << 52) | fraction, biased_exp - 1075) |
| 114 | + }; |
| 115 | + |
| 116 | + // Strip trailing zeros so the fraction is already in lowest terms: |
| 117 | + // after stripping, mantissa is odd and the denominator (if any) is a |
| 118 | + // power of 2, so gcd(mantissa, denom) = 1. |
| 119 | + let tz = mantissa.trailing_zeros(); |
| 120 | + let mantissa = mantissa >> tz; |
| 121 | + let exponent = raw_exp + tz.cast_signed(); |
| 122 | + |
| 123 | + let is_negative = bits >> 63 != 0; |
| 124 | + let numer = if is_negative { |
| 125 | + -BigInt::from(mantissa) |
| 126 | + } else { |
| 127 | + BigInt::from(mantissa) |
| 128 | + }; |
| 129 | + |
| 130 | + if exponent >= 0 { |
| 131 | + BigRational::new_raw(numer << exponent.cast_unsigned(), BigInt::from(1u32)) |
| 132 | + } else { |
| 133 | + BigRational::new_raw(numer, BigInt::from(1u32) << (-exponent).cast_unsigned()) |
| 134 | + } |
79 | 135 | } |
80 | 136 |
|
81 | 137 | /// Compute the exact determinant of a `D×D` matrix using the Bareiss algorithm |
@@ -1158,6 +1214,132 @@ mod tests { |
1158 | 1214 | assert_eq!(gauss_solve(&a, &b), Err(LaError::Singular { pivot_col: 1 })); |
1159 | 1215 | } |
1160 | 1216 |
|
| 1217 | + // ----------------------------------------------------------------------- |
| 1218 | + // f64_to_bigrational tests |
| 1219 | + // ----------------------------------------------------------------------- |
| 1220 | + |
| 1221 | + #[test] |
| 1222 | + fn f64_to_bigrational_positive_zero() { |
| 1223 | + let r = f64_to_bigrational(0.0); |
| 1224 | + assert_eq!(r, BigRational::from_integer(BigInt::from(0))); |
| 1225 | + } |
| 1226 | + |
| 1227 | + #[test] |
| 1228 | + fn f64_to_bigrational_negative_zero() { |
| 1229 | + let r = f64_to_bigrational(-0.0); |
| 1230 | + assert_eq!(r, BigRational::from_integer(BigInt::from(0))); |
| 1231 | + } |
| 1232 | + |
| 1233 | + #[test] |
| 1234 | + fn f64_to_bigrational_one() { |
| 1235 | + let r = f64_to_bigrational(1.0); |
| 1236 | + assert_eq!(r, BigRational::from_integer(BigInt::from(1))); |
| 1237 | + } |
| 1238 | + |
| 1239 | + #[test] |
| 1240 | + fn f64_to_bigrational_negative_one() { |
| 1241 | + let r = f64_to_bigrational(-1.0); |
| 1242 | + assert_eq!(r, BigRational::from_integer(BigInt::from(-1))); |
| 1243 | + } |
| 1244 | + |
| 1245 | + #[test] |
| 1246 | + fn f64_to_bigrational_half() { |
| 1247 | + let r = f64_to_bigrational(0.5); |
| 1248 | + assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(2))); |
| 1249 | + } |
| 1250 | + |
| 1251 | + #[test] |
| 1252 | + fn f64_to_bigrational_quarter() { |
| 1253 | + let r = f64_to_bigrational(0.25); |
| 1254 | + assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(4))); |
| 1255 | + } |
| 1256 | + |
| 1257 | + #[test] |
| 1258 | + fn f64_to_bigrational_negative_three_and_a_half() { |
| 1259 | + // -3.5 = -7/2 |
| 1260 | + let r = f64_to_bigrational(-3.5); |
| 1261 | + assert_eq!(r, BigRational::new(BigInt::from(-7), BigInt::from(2))); |
| 1262 | + } |
| 1263 | + |
| 1264 | + #[test] |
| 1265 | + fn f64_to_bigrational_integer() { |
| 1266 | + let r = f64_to_bigrational(42.0); |
| 1267 | + assert_eq!(r, BigRational::from_integer(BigInt::from(42))); |
| 1268 | + } |
| 1269 | + |
| 1270 | + #[test] |
| 1271 | + fn f64_to_bigrational_power_of_two() { |
| 1272 | + let r = f64_to_bigrational(1024.0); |
| 1273 | + assert_eq!(r, BigRational::from_integer(BigInt::from(1024))); |
| 1274 | + } |
| 1275 | + |
| 1276 | + #[test] |
| 1277 | + fn f64_to_bigrational_subnormal() { |
| 1278 | + let tiny = 5e-324_f64; // smallest positive subnormal |
| 1279 | + assert!(tiny.is_subnormal()); |
| 1280 | + let r = f64_to_bigrational(tiny); |
| 1281 | + // 5e-324 = 1 × 2^(-1074) |
| 1282 | + assert_eq!( |
| 1283 | + r, |
| 1284 | + BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32) |
| 1285 | + ); |
| 1286 | + } |
| 1287 | + |
| 1288 | + #[test] |
| 1289 | + fn f64_to_bigrational_already_lowest_terms() { |
| 1290 | + // 0.5 should produce numer=1, denom=2 (already reduced). |
| 1291 | + let r = f64_to_bigrational(0.5); |
| 1292 | + assert_eq!(*r.numer(), BigInt::from(1)); |
| 1293 | + assert_eq!(*r.denom(), BigInt::from(2)); |
| 1294 | + } |
| 1295 | + |
| 1296 | + #[test] |
| 1297 | + fn f64_to_bigrational_round_trip() { |
| 1298 | + // -0.0 is excluded: it maps to BigRational(0) which round-trips |
| 1299 | + // to +0.0 (correct; tested separately in f64_to_bigrational_negative_zero). |
| 1300 | + let values = [ |
| 1301 | + 0.0, |
| 1302 | + 1.0, |
| 1303 | + -1.0, |
| 1304 | + 0.5, |
| 1305 | + 0.25, |
| 1306 | + 0.1, |
| 1307 | + 42.0, |
| 1308 | + -3.5, |
| 1309 | + 1e10, |
| 1310 | + 1e-10, |
| 1311 | + f64::MAX / 2.0, |
| 1312 | + f64::MIN_POSITIVE, |
| 1313 | + 5e-324, |
| 1314 | + ]; |
| 1315 | + for &v in &values { |
| 1316 | + let r = f64_to_bigrational(v); |
| 1317 | + let back = r.to_f64().expect("round-trip to_f64 failed"); |
| 1318 | + assert!( |
| 1319 | + v.to_bits() == back.to_bits(), |
| 1320 | + "round-trip failed for {v}: got {back}" |
| 1321 | + ); |
| 1322 | + } |
| 1323 | + } |
| 1324 | + |
| 1325 | + #[test] |
| 1326 | + #[should_panic(expected = "non-finite f64 in exact conversion")] |
| 1327 | + fn f64_to_bigrational_panics_on_nan() { |
| 1328 | + f64_to_bigrational(f64::NAN); |
| 1329 | + } |
| 1330 | + |
| 1331 | + #[test] |
| 1332 | + #[should_panic(expected = "non-finite f64 in exact conversion")] |
| 1333 | + fn f64_to_bigrational_panics_on_inf() { |
| 1334 | + f64_to_bigrational(f64::INFINITY); |
| 1335 | + } |
| 1336 | + |
| 1337 | + #[test] |
| 1338 | + #[should_panic(expected = "non-finite f64 in exact conversion")] |
| 1339 | + fn f64_to_bigrational_panics_on_neg_inf() { |
| 1340 | + f64_to_bigrational(f64::NEG_INFINITY); |
| 1341 | + } |
| 1342 | + |
1161 | 1343 | // ----------------------------------------------------------------------- |
1162 | 1344 | // validate_finite_vec tests |
1163 | 1345 | // ----------------------------------------------------------------------- |
|
0 commit comments