commonware_utils/
rational.rs

1//! Utilities for working with `num_rational::BigRational`.
2
3use num_bigint::BigInt;
4use num_integer::Integer;
5use num_rational::BigRational;
6use num_traits::{One, ToPrimitive, Zero};
7
8/// Extension trait adding convenience constructors for [`BigRational`].
9pub trait BigRationalExt {
10    /// Creates a [`BigRational`] from a `u64` numerator with denominator `1`.
11    fn from_u64(value: u64) -> Self;
12
13    /// Creates a [`BigRational`] from a `u64` numerator and denominator.
14    ///
15    /// # Panics
16    ///
17    /// Panics if the denominator is zero.
18    fn from_frac_u64(numerator: u64, denominator: u64) -> Self;
19
20    /// Creates a [`BigRational`] from a `u128` numerator with denominator `1`.
21    fn from_u128(value: u128) -> Self;
22
23    /// Creates a [`BigRational`] from a `u128` numerator and denominator.
24    ///
25    /// # Panics
26    ///
27    /// Panics if the denominator is zero.
28    fn from_frac_u128(numerator: u128, denominator: u128) -> Self;
29
30    /// Returns the ceiling of the rational value as `u128`, saturating and treating negative values as zero.
31    fn ceil_to_u128(&self) -> Option<u128>;
32
33    /// Creates a [`BigRational`] from a `usize` numerator with denominator `1`.
34    fn from_usize(value: usize) -> Self;
35
36    /// Creates a [`BigRational`] from a `usize` numerator and denominator.
37    ///
38    /// # Panics
39    ///
40    /// Panics if the denominator is zero.
41    fn from_frac_usize(numerator: usize, denominator: usize) -> Self;
42
43    /// Computes the ceiling of log2 of this rational number with specified binary precision.
44    ///
45    /// Returns log2(x) rounded up to the nearest value representable with `binary_digits`
46    /// fractional bits in binary representation.
47    ///
48    /// # Panics
49    ///
50    /// Panics if the rational number is non-positive.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use num_rational::BigRational;
56    /// use commonware_utils::rational::BigRationalExt;
57    ///
58    /// let x = BigRational::from_frac_u64(3, 1); // 3
59    /// let result = x.log2_ceil(4);
60    /// // log2(3) ≈ 1.585, the algorithm computes a ceiling approximation
61    /// assert!(result >= BigRational::from_u64(1));
62    /// assert!(result <= BigRational::from_u64(2));
63    /// ```
64    fn log2_ceil(&self, binary_digits: usize) -> BigRational;
65}
66
67impl BigRationalExt for BigRational {
68    fn from_u64(value: u64) -> Self {
69        Self::from_integer(BigInt::from(value))
70    }
71
72    fn from_frac_u64(numerator: u64, denominator: u64) -> Self {
73        Self::new(BigInt::from(numerator), BigInt::from(denominator))
74    }
75
76    fn from_u128(value: u128) -> Self {
77        Self::from_integer(BigInt::from(value))
78    }
79
80    fn from_frac_u128(numerator: u128, denominator: u128) -> Self {
81        Self::new(BigInt::from(numerator), BigInt::from(denominator))
82    }
83
84    fn ceil_to_u128(&self) -> Option<u128> {
85        if self < &Self::zero() {
86            return Some(0);
87        }
88
89        let den = self.denom();
90        if den.is_zero() {
91            return None;
92        }
93
94        let (quot, rem) = self.numer().div_rem(den);
95        let mut result = quot.to_u128().unwrap_or(u128::MAX);
96        if !rem.is_zero() {
97            result = result.saturating_add(1);
98        }
99        Some(result)
100    }
101
102    fn from_usize(value: usize) -> Self {
103        Self::from_integer(BigInt::from(value))
104    }
105
106    fn from_frac_usize(numerator: usize, denominator: usize) -> Self {
107        Self::new(BigInt::from(numerator), BigInt::from(denominator))
108    }
109
110    fn log2_ceil(&self, binary_digits: usize) -> BigRational {
111        if self <= &Self::zero() {
112            panic!("log2 undefined for non-positive numbers");
113        }
114
115        // Step 1: Extract numerator and denominator as unsigned integers for efficient computation.
116        let numer = self.numer().to_biguint().expect("positive");
117        let denom = self.denom().to_biguint().expect("positive");
118
119        // Step 2: Compute the integer part of log2(numer/denom) by comparing bit lengths.
120        // Since log2(numer/denom) = log2(numer) - log2(denom), and bits() gives us
121        // floor(log2(x)) + 1, we can compute the integer part directly.
122        let numer_bits = numer.bits();
123        let denom_bits = denom.bits();
124        let mut integer_part = numer_bits as i128 - denom_bits as i128;
125
126        // Step 3: Align the most significant bits of numerator and denominator to bring
127        // the ratio into the range [1, 2). By shifting both values to have the same bit
128        // length, we normalize the ratio in a single operation.
129        let mut normalized_numer = numer;
130        if denom_bits > numer_bits {
131            normalized_numer <<= denom_bits - numer_bits;
132        }
133        let mut normalized_denom = denom;
134        if numer_bits > denom_bits {
135            normalized_denom <<= numer_bits - denom_bits;
136        }
137
138        // After alignment, we may need one additional shift to ensure normalized value is in [1, 2).
139        if normalized_numer < normalized_denom {
140            normalized_numer <<= 1;
141            integer_part -= 1;
142        }
143        assert!(
144            normalized_numer >= normalized_denom && normalized_numer < (&normalized_denom << 1)
145        );
146
147        // Step 4: Handle the special case where the value is exactly a power of 2.
148        // In this case, log2(x) is exact and has no fractional component.
149        if normalized_numer == normalized_denom {
150            let numerator = BigInt::from(integer_part) << binary_digits;
151            let denominator = BigInt::one() << binary_digits;
152            return Self::new(numerator, denominator);
153        }
154
155        // Step 5: Extract binary fractional digits using the square-and-compare method.
156        // At this point, normalized is in (1, 2), so log2(normalized) is in (0, 1).
157        // We use integer-only arithmetic to avoid BigRational division overhead:
158        // Instead of squaring the rational and comparing to 2, we square the numerator
159        // and denominator separately and check if numer^2 >= 2 * denom^2.
160        let mut fractional_bits = BigInt::zero();
161        let one = BigInt::one();
162
163        for _ in 0..binary_digits {
164            // Square both numerator and denominator to shift the next binary digit into position.
165            let numer_squared = &normalized_numer * &normalized_numer;
166            let denom_squared = &normalized_denom * &normalized_denom;
167
168            // Left-shift the fractional bits accumulator to make room for the new bit.
169            fractional_bits <<= 1;
170
171            // If squared value >= 2, the next binary digit is 1.
172            // We renormalize by dividing by 2, which is equivalent to multiplying the denominator by 2.
173            let two_denom_squared = &denom_squared << 1;
174            if numer_squared >= two_denom_squared {
175                fractional_bits |= &one;
176                normalized_numer = numer_squared;
177                normalized_denom = two_denom_squared;
178            } else {
179                normalized_numer = numer_squared;
180                normalized_denom = denom_squared;
181            }
182        }
183
184        // Step 6: Combine integer and fractional parts, then apply ceiling operation.
185        // We need to return a single rational number with denominator 2^binary_digits.
186        // By left-shifting the integer part, we convert it to the same "units" as fractional_bits,
187        // allowing us to add them: numerator = (integer_part * 2^binary_digits) + fractional_bits.
188        // This represents: integer_part + fractional_bits / (2^binary_digits)
189        let mut numerator = (BigInt::from(integer_part) << binary_digits) + fractional_bits;
190
191        // If there's any leftover mass in the normalized value after extracting all digits,
192        // we need to round up (ceiling operation). This happens when normalized_numer > normalized_denom.
193        if normalized_numer > normalized_denom {
194            numerator += &one;
195        }
196
197        let denominator = one << binary_digits;
198        Self::new(numerator, denominator)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::BigRationalExt;
205    use num_bigint::BigInt;
206    use num_rational::BigRational;
207
208    #[test]
209    fn converts_from_u64() {
210        let rational = BigRational::from_u64(42);
211        assert_eq!(rational.numer(), &BigInt::from(42u64));
212        assert_eq!(rational.denom(), &BigInt::from(1u32));
213    }
214
215    #[test]
216    fn converts_from_frac_u64() {
217        let rational = BigRational::from_frac_u64(6, 8);
218        assert_eq!(rational.numer(), &BigInt::from(3u32));
219        assert_eq!(rational.denom(), &BigInt::from(4u32));
220    }
221
222    #[test]
223    fn converts_from_u128() {
224        let value = (u64::MAX as u128) + 10;
225        let rational = BigRational::from_u128(value);
226        assert_eq!(rational.numer(), &BigInt::from(value));
227        assert_eq!(rational.denom(), &BigInt::from(1u32));
228    }
229
230    #[test]
231    fn converts_from_frac_u128() {
232        let rational = BigRational::from_frac_u128(10, 4);
233        assert_eq!(rational.numer(), &BigInt::from(5u32));
234        assert_eq!(rational.denom(), &BigInt::from(2u32));
235    }
236
237    #[test]
238    fn converts_from_usize() {
239        let value = usize::MAX;
240        let rational = BigRational::from_usize(value);
241        assert_eq!(rational.numer(), &BigInt::from(value));
242        assert_eq!(rational.denom(), &BigInt::from(1u32));
243    }
244
245    #[test]
246    fn converts_from_frac_usize() {
247        let rational = BigRational::from_frac_usize(48, 18);
248        assert_eq!(rational.numer(), &BigInt::from(8u32));
249        assert_eq!(rational.denom(), &BigInt::from(3u32));
250    }
251
252    #[test]
253    fn ceiling_handles_positive_fraction() {
254        let value = BigRational::new(BigInt::from(5u32), BigInt::from(2u32));
255        assert_eq!(value.ceil_to_u128(), Some(3));
256    }
257
258    #[test]
259    fn ceiling_handles_negative() {
260        let value = BigRational::new(BigInt::from(-3i32), BigInt::from(2u32));
261        assert_eq!(value.ceil_to_u128(), Some(0));
262    }
263
264    #[test]
265    fn ceiling_handles_large_values() {
266        let value = BigRational::from_u128(u128::MAX - 1);
267        assert_eq!(value.ceil_to_u128(), Some(u128::MAX - 1));
268    }
269
270    #[test]
271    #[should_panic(expected = "log2 undefined for non-positive numbers")]
272    fn log2_ceil_negative_panics() {
273        <BigRational as num_traits::FromPrimitive>::from_i64(-1)
274            .unwrap()
275            .log2_ceil(8);
276    }
277
278    #[test]
279    fn log2_ceil_exact_powers_of_two() {
280        // Test exact powers of 2: log2(2^n) = n
281        let value = BigRational::from_u64(1); // 2^0
282        assert_eq!(value.log2_ceil(4), BigRational::from_u64(0));
283
284        let value = BigRational::from_u64(2); // 2^1
285        assert_eq!(value.log2_ceil(4), BigRational::from_u64(1));
286
287        let value = BigRational::from_u64(8); // 2^3
288        assert_eq!(value.log2_ceil(4), BigRational::from_u64(3));
289
290        let value = BigRational::from_u64(1024); // 2^10
291        assert_eq!(value.log2_ceil(4), BigRational::from_u64(10));
292    }
293
294    #[test]
295    fn log2_ceil_fractional_powers_of_two() {
296        // Test fractional powers of 2: log2(1/2) = -1, log2(1/4) = -2
297        let value = BigRational::from_frac_u64(1, 2); // 2^(-1)
298        let result = value.log2_ceil(4);
299        assert_eq!(result, BigRational::from_integer(BigInt::from(-1)));
300
301        let value = BigRational::from_frac_u64(1, 4); // 2^(-2)
302        let result = value.log2_ceil(4);
303        assert_eq!(result, BigRational::from_integer(BigInt::from(-2)));
304
305        let value = BigRational::from_frac_u64(3, 8); // 3/8 = 3 * 2^(-3)
306        let result = value.log2_ceil(4);
307        assert_eq!(result, BigRational::new(BigInt::from(-11), BigInt::from(8)));
308    }
309
310    #[test]
311    fn log2_ceil_simple_values() {
312        // log2(3) ≈ 1.585, with binary_digits=0 we get integer part
313        let value = BigRational::from_u64(3);
314        let result = value.log2_ceil(0);
315        assert_eq!(result, BigRational::from_u64(2));
316
317        // log2(5) ≈ 2.322, with binary_digits=0 we get integer part
318        let value = BigRational::from_u64(5);
319        let result = value.log2_ceil(0);
320        assert_eq!(result, BigRational::from_u64(3));
321
322        // With 4 bits precision, algorithm should provide fractional results
323        let value = BigRational::from_u64(3);
324        let result = value.log2_ceil(4);
325        assert_eq!(result, BigRational::from_frac_u64(13, 8));
326    }
327
328    #[test]
329    fn log2_ceil_rational_values() {
330        // Test with some basic fractional values
331        let value = BigRational::from_frac_u64(3, 2);
332        let result = value.log2_ceil(4);
333        assert_eq!(result, BigRational::from_frac_u64(5, 8));
334
335        let value = BigRational::from_frac_u64(7, 4);
336        let result = value.log2_ceil(4);
337        assert_eq!(result, BigRational::from_frac_u64(13, 16));
338    }
339
340    #[test]
341    fn log2_ceil_different_precisions() {
342        let value = BigRational::from_u64(3);
343
344        // Test different precisions give reasonable results
345        let result0 = value.log2_ceil(0);
346        let result1 = value.log2_ceil(1);
347        let result4 = value.log2_ceil(4);
348        let result8 = value.log2_ceil(8);
349
350        assert_eq!(result0, BigRational::from_u64(2));
351        assert_eq!(result1, BigRational::from_u64(2));
352        assert_eq!(result4, BigRational::from_frac_u64(13, 8));
353        assert_eq!(
354            result8,
355            BigRational::new(BigInt::from(203), BigInt::from(128))
356        );
357    }
358
359    #[test]
360    fn log2_ceil_large_values() {
361        // Test with larger numbers
362        let value = BigRational::from_u64(1000);
363        let result = value.log2_ceil(4);
364        assert_eq!(result, BigRational::from_u64(10));
365    }
366
367    #[test]
368    fn log2_ceil_very_small_values() {
369        // Test with very small fractions
370        let value = BigRational::from_frac_u64(1, 1000);
371        let result = value.log2_ceil(4);
372        assert_eq!(
373            result,
374            BigRational::new(BigInt::from(-159), BigInt::from(16))
375        );
376    }
377
378    #[test]
379    fn log2_ceil_edge_cases() {
380        // -- Just above a power of two (small positive, should round up to a tiny dyadic)
381        // log2(17/16) ≈ 0.087462, k=8 → 0.087462 * 256 ≈ 22.39 ⇒ ceil = 23 → 23/256
382        let x = BigRational::from_frac_u64(17, 16);
383        assert_eq!(x.log2_ceil(8), BigRational::from_frac_u64(23, 256));
384
385        // log2(129/128) ≈ 0.011227, k=8 → 0.011227 * 256 ≈ 2.874 ⇒ ceil = 3 → 3/256
386        let x = BigRational::from_frac_u64(129, 128);
387        assert_eq!(x.log2_ceil(8), BigRational::from_frac_u64(3, 256));
388
389        // log2(33/32) ≈ 0.044394, k=10 → 0.044394 * 1024 ≈ 45.45 ⇒ ceil = 46 → 46/1024
390        let x = BigRational::from_frac_u64(33, 32);
391        assert_eq!(x.log2_ceil(10), BigRational::from_frac_u64(46, 1024));
392
393        // -- Just below a power of two (negative, but tiny in magnitude)
394        // log2(255/256) ≈ −0.00565, k=8 → −0.00565 * 256 ≈ −1.44 ⇒ ceil = −1 → −1/256
395        let x = BigRational::from_frac_u64(255, 256);
396        assert_eq!(x.log2_ceil(8), BigRational::new((-1).into(), 256u32.into()));
397
398        // log2(1023/1024) ≈ −0.00141, k=9 → −0.00141 * 512 ≈ −0.72 ⇒ ceil = 0 → 0/512
399        let x = BigRational::from_frac_u64(1023, 1024);
400        assert_eq!(x.log2_ceil(9), BigRational::new(0.into(), 512u32.into()));
401
402        // -- k = 0 (integer ceiling of log2)
403        // log2(3/2) ≈ 0.585 ⇒ ceil = 1
404        let x = BigRational::from_frac_u64(3, 2);
405        assert_eq!(x.log2_ceil(0), BigRational::from_integer(1.into()));
406
407        // log2(3/4) ≈ −0.415 ⇒ ceil = 0
408        let x = BigRational::from_frac_u64(3, 4);
409        assert_eq!(x.log2_ceil(0), BigRational::from_integer(0.into()));
410
411        // -- x < 1 with fractional bits (negative dyadic output)
412        // log2(3/4) ≈ −0.415, k=4 → −0.415 * 16 ≈ −6.64 => ceil = −6 → −6/16
413        let x = BigRational::from_frac_u64(3, 4);
414        assert_eq!(x.log2_ceil(4), BigRational::new((-6).into(), 16u32.into()));
415
416        // -- Monotonic with k: increasing k refines the dyadic upwards
417        // For 257/256: k=8 → 0.00563*256 ≈ 1.44 ⇒ ceil=2 → 2/256
418        //              k=9 → 0.00563*512 ≈ 2.88 ⇒ ceil=3 → 3/512
419        let x = BigRational::from_frac_u64(257, 256);
420        assert_eq!(x.log2_ceil(8), BigRational::new(2.into(), 256u32.into()));
421        assert_eq!(x.log2_ceil(9), BigRational::new(3.into(), 512u32.into()));
422
423        // -- Scale invariance (multiply numerator and denominator by same factor, result unchanged)
424        // (17/16) * (2^30 / 2^30) has the same log2, the dyadic result should match 23/256 at k=8.
425        let num = BigInt::from(17u32) << 30;
426        let den = BigInt::from(16u32) << 30;
427        let x = BigRational::new(num, den);
428        assert_eq!(x.log2_ceil(8), BigRational::from_frac_u64(23, 256));
429    }
430}