Skip to content

Commit 756917c

Browse files
authored
Merge pull request #69 from acgetchell/perf/63-custom-f64-to-bigrational
perf: custom f64 → BigRational via IEEE 754 bit decomposition (#63)
2 parents 0771fdc + 0a8ce5b commit 756917c

2 files changed

Lines changed: 208 additions & 4 deletions

File tree

‎REFERENCES.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,25 @@ elimination [7] in `BigRational`. See `src/exact.rs` for the full architecture d
7070
[DOI](https://doi.org/10.1007/PL00009321) ·
7171
[PDF](https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf)
7272
Also: Technical Report CMU-CS-96-140, Carnegie Mellon University, May 1996.
73+
74+
### f64 → BigRational conversion (`f64_to_bigrational`)
75+
76+
`f64_to_bigrational` converts an f64 to an exact `BigRational` by decomposing the IEEE 754
77+
binary64 bit representation into its sign, exponent, and significand fields. Because every
78+
finite f64 is exactly `±m × 2^e` (where `m` is an integer), the rational can be constructed
79+
directly via `BigRational::new_raw` without GCD normalization — trailing zeros in the
80+
significand are stripped first so the fraction is already in lowest terms.
81+
82+
See references [9-10] below.
83+
84+
9. IEEE Computer Society. "IEEE Standard for Floating-Point Arithmetic." *IEEE Std 754-2019*
85+
(Revision of IEEE 754-2008), 2019.
86+
[DOI](https://doi.org/10.1109/IEEESTD.2019.8766229)
87+
Section 3.4 (binary64 format): 1 sign bit, 11 exponent bits (bias 1023), 52 trailing
88+
significand bits; subnormals have biased exponent 0 with implicit leading 0.
89+
10. Goldberg, David. "What Every Computer Scientist Should Know About Floating-Point
90+
Arithmetic." *ACM Computing Surveys* 23.1 (1991): 5–48.
91+
[DOI](https://doi.org/10.1145/103162.103163) ·
92+
[PDF](https://www.validlab.com/goldberg/paper.pdf)
93+
Comprehensive survey of IEEE 754 representation, rounding, and exact rational
94+
reconstruction of floating-point values.

‎src/exact.rs‎

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
//! Since all arithmetic is exact, any non-zero pivot gives the correct result
2828
//! (there is no numerical stability concern). Every finite `f64` is exactly
2929
//! 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`.
3039
3140
use num_bigint::{BigInt, Sign};
3241
use num_rational::BigRational;
@@ -67,15 +76,62 @@ fn validate_finite_vec<const D: usize>(v: &Vector<D>) -> Result<(), LaError> {
6776
Ok(())
6877
}
6978

70-
/// Convert an `f64` to an exact `BigRational`.
79+
/// Convert an `f64` to an exact `BigRational` via IEEE 754 bit decomposition.
7180
///
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.
7490
///
7591
/// # Panics
7692
/// Panics if `x` is NaN or infinite.
7793
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+
}
79135
}
80136

81137
/// Compute the exact determinant of a `D×D` matrix using the Bareiss algorithm
@@ -1158,6 +1214,132 @@ mod tests {
11581214
assert_eq!(gauss_solve(&a, &b), Err(LaError::Singular { pivot_col: 1 }));
11591215
}
11601216

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+
11611343
// -----------------------------------------------------------------------
11621344
// validate_finite_vec tests
11631345
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)