Skip to main content

commonware_utils/
rational.rs

1//! Utilities for working with `num_rational::BigRational`.
2
3use num_bigint::{BigInt, BigUint};
4use num_integer::Integer;
5use num_rational::BigRational;
6use num_traits::{One, ToPrimitive, Zero};
7
8/// Computes log2 of a rational number with specified binary precision.
9///
10/// Returns `(numerator, has_remainder)` where the result is `numerator / 2^binary_digits`
11/// and `has_remainder` indicates whether there's additional precision beyond what was computed.
12fn log2(numer: BigUint, denom: BigUint, binary_digits: usize) -> (BigInt, bool) {
13    // Compute the integer part of log2(numer/denom) by comparing bit lengths.
14    // Since log2(numer/denom) = log2(numer) - log2(denom), and bits() gives us
15    // floor(log2(x)) + 1, we can compute the integer part directly.
16    let numer_bits = numer.bits();
17    let denom_bits = denom.bits();
18    let mut integer_part = numer_bits as i128 - denom_bits as i128;
19
20    // Align the most significant bits of numerator and denominator to bring
21    // the ratio into the range [1, 2). By shifting both values to have the same bit
22    // length, we normalize the ratio in a single operation.
23    let mut normalized_numer = numer;
24    if denom_bits > numer_bits {
25        normalized_numer <<= denom_bits - numer_bits;
26    }
27    let mut normalized_denom = denom;
28    if numer_bits > denom_bits {
29        normalized_denom <<= numer_bits - denom_bits;
30    }
31
32    // After alignment, we may need one additional shift to ensure normalized value is in [1, 2).
33    if normalized_numer < normalized_denom {
34        normalized_numer <<= 1;
35        integer_part -= 1;
36    }
37
38    // Handle the special case where the value is exactly a power of 2.
39    // In this case, log2(x) is exact and has no fractional component.
40    if normalized_numer == normalized_denom {
41        return (BigInt::from(integer_part) << binary_digits, false);
42    }
43
44    // Extract binary fractional digits using the square-and-compare method.
45    // At this point, normalized is in (1, 2), so log2(normalized) is in (0, 1).
46    // We use integer-only arithmetic to avoid BigRational division overhead:
47    // Instead of squaring the rational and comparing to 2, we square the numerator
48    // and denominator separately and check if numer^2 >= 2 * denom^2.
49    let mut fractional_bits = BigInt::zero();
50    let one = BigInt::one();
51
52    for _ in 0..binary_digits {
53        // Square both numerator and denominator to shift the next binary digit into position.
54        let numer_squared = &normalized_numer * &normalized_numer;
55        let denom_squared = &normalized_denom * &normalized_denom;
56
57        // Left-shift the fractional bits accumulator to make room for the new bit.
58        fractional_bits <<= 1;
59
60        // If squared value >= 2, the next binary digit is 1.
61        // We renormalize by dividing by 2, which is equivalent to multiplying the denominator by 2.
62        let two_denom_squared = &denom_squared << 1;
63        if numer_squared >= two_denom_squared {
64            fractional_bits |= &one;
65            normalized_numer = numer_squared;
66            normalized_denom = two_denom_squared;
67        } else {
68            normalized_numer = numer_squared;
69            normalized_denom = denom_squared;
70        }
71    }
72
73    // Combine integer and fractional parts.
74    // We return a single rational number with denominator 2^binary_digits.
75    // By left-shifting the integer part, we convert it to the same "units" as fractional_bits,
76    // allowing us to add them: numerator = (integer_part * 2^binary_digits) + fractional_bits.
77    // This represents: integer_part + fractional_bits / (2^binary_digits)
78    let numerator = (BigInt::from(integer_part) << binary_digits) + fractional_bits;
79
80    // has_remainder is true if there's any leftover mass in the normalized value
81    // after extracting all digits. This happens when normalized_numer > normalized_denom.
82    let has_remainder = normalized_numer > normalized_denom;
83    (numerator, has_remainder)
84}
85
86/// Extension trait adding convenience constructors for [`BigRational`].
87pub trait BigRationalExt {
88    /// Creates a [`BigRational`] from a `u64` numerator with denominator `1`.
89    fn from_u64(value: u64) -> Self;
90
91    /// Creates a [`BigRational`] from a `u64` numerator and denominator.
92    ///
93    /// # Panics
94    ///
95    /// Panics if the denominator is zero.
96    fn from_frac_u64(numerator: u64, denominator: u64) -> Self;
97
98    /// Creates a [`BigRational`] from a `u128` numerator with denominator `1`.
99    fn from_u128(value: u128) -> Self;
100
101    /// Creates a [`BigRational`] from a `u128` numerator and denominator.
102    ///
103    /// # Panics
104    ///
105    /// Panics if the denominator is zero.
106    fn from_frac_u128(numerator: u128, denominator: u128) -> Self;
107
108    /// Returns the ceiling of the rational value as `u128`, saturating and treating negative values as zero.
109    fn ceil_to_u128(&self) -> Option<u128>;
110
111    /// Creates a [`BigRational`] from a `usize` numerator with denominator `1`.
112    fn from_usize(value: usize) -> Self;
113
114    /// Creates a [`BigRational`] from a `usize` numerator and denominator.
115    ///
116    /// # Panics
117    ///
118    /// Panics if the denominator is zero.
119    fn from_frac_usize(numerator: usize, denominator: usize) -> Self;
120
121    /// Computes the ceiling of log2 of this rational number with specified binary precision.
122    ///
123    /// Returns log2(x) rounded up to the nearest value representable with `binary_digits`
124    /// fractional bits in binary representation.
125    ///
126    /// # Panics
127    ///
128    /// Panics if the rational number is non-positive.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use num_rational::BigRational;
134    /// use commonware_utils::rational::BigRationalExt;
135    ///
136    /// let x = BigRational::from_frac_u64(3, 1); // 3
137    /// let result = x.log2_ceil(4);
138    /// // log2(3) ≈ 1.585, the algorithm computes a ceiling approximation
139    /// assert!(result >= BigRational::from_u64(1));
140    /// assert!(result <= BigRational::from_u64(2));
141    /// ```
142    fn log2_ceil(&self, binary_digits: usize) -> BigRational;
143
144    /// Computes the floor of log2 of this rational number with specified binary precision.
145    ///
146    /// Returns log2(x) rounded down to the nearest value representable with `binary_digits`
147    /// fractional bits in binary representation.
148    ///
149    /// # Panics
150    ///
151    /// Panics if the rational number is non-positive.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use num_rational::BigRational;
157    /// use commonware_utils::rational::BigRationalExt;
158    ///
159    /// let x = BigRational::from_frac_u64(3, 1); // 3
160    /// let result = x.log2_floor(4);
161    /// // log2(3) ≈ 1.585, the algorithm computes a floor approximation
162    /// assert!(result >= BigRational::from_u64(1));
163    /// assert!(result <= BigRational::from_u64(2));
164    /// ```
165    fn log2_floor(&self, binary_digits: usize) -> BigRational;
166}
167
168impl BigRationalExt for BigRational {
169    fn from_u64(value: u64) -> Self {
170        Self::from_integer(BigInt::from(value))
171    }
172
173    fn from_frac_u64(numerator: u64, denominator: u64) -> Self {
174        Self::new(BigInt::from(numerator), BigInt::from(denominator))
175    }
176
177    fn from_u128(value: u128) -> Self {
178        Self::from_integer(BigInt::from(value))
179    }
180
181    fn from_frac_u128(numerator: u128, denominator: u128) -> Self {
182        Self::new(BigInt::from(numerator), BigInt::from(denominator))
183    }
184
185    fn ceil_to_u128(&self) -> Option<u128> {
186        if self < &Self::zero() {
187            return Some(0);
188        }
189
190        let den = self.denom();
191        if den.is_zero() {
192            return None;
193        }
194
195        let (quot, rem) = self.numer().div_rem(den);
196        let mut result = quot.to_u128().unwrap_or(u128::MAX);
197        if !rem.is_zero() {
198            result = result.saturating_add(1);
199        }
200        Some(result)
201    }
202
203    fn from_usize(value: usize) -> Self {
204        Self::from_integer(BigInt::from(value))
205    }
206
207    fn from_frac_usize(numerator: usize, denominator: usize) -> Self {
208        Self::new(BigInt::from(numerator), BigInt::from(denominator))
209    }
210
211    fn log2_ceil(&self, binary_digits: usize) -> BigRational {
212        if self <= &Self::zero() {
213            panic!("log2 undefined for non-positive numbers");
214        }
215
216        let numer = self.numer().to_biguint().expect("positive");
217        let denom = self.denom().to_biguint().expect("positive");
218        let (mut numerator, has_remainder) = log2(numer, denom, binary_digits);
219        if has_remainder {
220            numerator += 1;
221        }
222        let denominator = BigInt::one() << binary_digits;
223        Self::new(numerator, denominator)
224    }
225
226    fn log2_floor(&self, binary_digits: usize) -> BigRational {
227        if self <= &Self::zero() {
228            panic!("log2 undefined for non-positive numbers");
229        }
230
231        let numer = self.numer().to_biguint().expect("positive");
232        let denom = self.denom().to_biguint().expect("positive");
233        let (numerator, _) = log2(numer, denom, binary_digits);
234        let denominator = BigInt::one() << binary_digits;
235        Self::new(numerator, denominator)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::BigRationalExt;
242    use num_bigint::BigInt;
243    use num_rational::BigRational;
244
245    #[test]
246    fn converts_from_u64() {
247        let rational = BigRational::from_u64(42);
248        assert_eq!(rational.numer(), &BigInt::from(42u64));
249        assert_eq!(rational.denom(), &BigInt::from(1u32));
250    }
251
252    #[test]
253    fn converts_from_frac_u64() {
254        let rational = BigRational::from_frac_u64(6, 8);
255        assert_eq!(rational.numer(), &BigInt::from(3u32));
256        assert_eq!(rational.denom(), &BigInt::from(4u32));
257    }
258
259    #[test]
260    fn converts_from_u128() {
261        let value = (u64::MAX as u128) + 10;
262        let rational = BigRational::from_u128(value);
263        assert_eq!(rational.numer(), &BigInt::from(value));
264        assert_eq!(rational.denom(), &BigInt::from(1u32));
265    }
266
267    #[test]
268    fn converts_from_frac_u128() {
269        let rational = BigRational::from_frac_u128(10, 4);
270        assert_eq!(rational.numer(), &BigInt::from(5u32));
271        assert_eq!(rational.denom(), &BigInt::from(2u32));
272    }
273
274    #[test]
275    fn converts_from_usize() {
276        let value = usize::MAX;
277        let rational = BigRational::from_usize(value);
278        assert_eq!(rational.numer(), &BigInt::from(value));
279        assert_eq!(rational.denom(), &BigInt::from(1u32));
280    }
281
282    #[test]
283    fn converts_from_frac_usize() {
284        let rational = BigRational::from_frac_usize(48, 18);
285        assert_eq!(rational.numer(), &BigInt::from(8u32));
286        assert_eq!(rational.denom(), &BigInt::from(3u32));
287    }
288
289    #[test]
290    fn ceiling_handles_positive_fraction() {
291        let value = BigRational::new(BigInt::from(5u32), BigInt::from(2u32));
292        assert_eq!(value.ceil_to_u128(), Some(3));
293    }
294
295    #[test]
296    fn ceiling_handles_negative() {
297        let value = BigRational::new(BigInt::from(-3i32), BigInt::from(2u32));
298        assert_eq!(value.ceil_to_u128(), Some(0));
299    }
300
301    #[test]
302    fn ceiling_handles_large_values() {
303        let value = BigRational::from_u128(u128::MAX - 1);
304        assert_eq!(value.ceil_to_u128(), Some(u128::MAX - 1));
305    }
306
307    #[test]
308    #[should_panic(expected = "log2 undefined for non-positive numbers")]
309    fn log2_ceil_negative_panics() {
310        <BigRational as num_traits::FromPrimitive>::from_i64(-1)
311            .unwrap()
312            .log2_ceil(8);
313    }
314
315    #[test]
316    fn log2_ceil_exact_powers_of_two() {
317        // Test exact powers of 2: log2(2^n) = n
318        let value = BigRational::from_u64(1); // 2^0
319        assert_eq!(value.log2_ceil(4), BigRational::from_u64(0));
320
321        let value = BigRational::from_u64(2); // 2^1
322        assert_eq!(value.log2_ceil(4), BigRational::from_u64(1));
323
324        let value = BigRational::from_u64(8); // 2^3
325        assert_eq!(value.log2_ceil(4), BigRational::from_u64(3));
326
327        let value = BigRational::from_u64(1024); // 2^10
328        assert_eq!(value.log2_ceil(4), BigRational::from_u64(10));
329    }
330
331    #[test]
332    fn log2_ceil_fractional_powers_of_two() {
333        // Test fractional powers of 2: log2(1/2) = -1, log2(1/4) = -2
334        let value = BigRational::from_frac_u64(1, 2); // 2^(-1)
335        let result = value.log2_ceil(4);
336        assert_eq!(result, BigRational::from_integer(BigInt::from(-1)));
337
338        let value = BigRational::from_frac_u64(1, 4); // 2^(-2)
339        let result = value.log2_ceil(4);
340        assert_eq!(result, BigRational::from_integer(BigInt::from(-2)));
341
342        let value = BigRational::from_frac_u64(3, 8); // 3/8 = 3 * 2^(-3)
343        let result = value.log2_ceil(4);
344        assert_eq!(result, BigRational::new(BigInt::from(-11), BigInt::from(8)));
345    }
346
347    #[test]
348    fn log2_ceil_simple_values() {
349        // log2(3) ≈ 1.585, with binary_digits=0 we get integer part
350        let value = BigRational::from_u64(3);
351        let result = value.log2_ceil(0);
352        assert_eq!(result, BigRational::from_u64(2));
353
354        // log2(5) ≈ 2.322, with binary_digits=0 we get integer part
355        let value = BigRational::from_u64(5);
356        let result = value.log2_ceil(0);
357        assert_eq!(result, BigRational::from_u64(3));
358
359        // With 4 bits precision, algorithm should provide fractional results
360        let value = BigRational::from_u64(3);
361        let result = value.log2_ceil(4);
362        assert_eq!(result, BigRational::from_frac_u64(13, 8));
363    }
364
365    #[test]
366    fn log2_ceil_rational_values() {
367        // Test with some basic fractional values
368        let value = BigRational::from_frac_u64(3, 2);
369        let result = value.log2_ceil(4);
370        assert_eq!(result, BigRational::from_frac_u64(5, 8));
371
372        let value = BigRational::from_frac_u64(7, 4);
373        let result = value.log2_ceil(4);
374        assert_eq!(result, BigRational::from_frac_u64(13, 16));
375    }
376
377    #[test]
378    fn log2_ceil_different_precisions() {
379        let value = BigRational::from_u64(3);
380
381        // Test different precisions give reasonable results
382        let result0 = value.log2_ceil(0);
383        let result1 = value.log2_ceil(1);
384        let result4 = value.log2_ceil(4);
385        let result8 = value.log2_ceil(8);
386
387        assert_eq!(result0, BigRational::from_u64(2));
388        assert_eq!(result1, BigRational::from_u64(2));
389        assert_eq!(result4, BigRational::from_frac_u64(13, 8));
390        assert_eq!(
391            result8,
392            BigRational::new(BigInt::from(203), BigInt::from(128))
393        );
394    }
395
396    #[test]
397    fn log2_ceil_large_values() {
398        // Test with larger numbers
399        let value = BigRational::from_u64(1000);
400        let result = value.log2_ceil(4);
401        assert_eq!(result, BigRational::from_u64(10));
402    }
403
404    #[test]
405    fn log2_ceil_very_small_values() {
406        // Test with very small fractions
407        let value = BigRational::from_frac_u64(1, 1000);
408        let result = value.log2_ceil(4);
409        assert_eq!(
410            result,
411            BigRational::new(BigInt::from(-159), BigInt::from(16))
412        );
413    }
414
415    #[test]
416    fn log2_ceil_edge_cases() {
417        // -- Just above a power of two (small positive, should round up to a tiny dyadic)
418        // log2(17/16) ≈ 0.087462, k=8 → 0.087462 * 256 ≈ 22.39 ⇒ ceil = 23 → 23/256
419        let x = BigRational::from_frac_u64(17, 16);
420        assert_eq!(x.log2_ceil(8), BigRational::from_frac_u64(23, 256));
421
422        // log2(129/128) ≈ 0.011227, k=8 → 0.011227 * 256 ≈ 2.874 ⇒ ceil = 3 → 3/256
423        let x = BigRational::from_frac_u64(129, 128);
424        assert_eq!(x.log2_ceil(8), BigRational::from_frac_u64(3, 256));
425
426        // log2(33/32) ≈ 0.044394, k=10 → 0.044394 * 1024 ≈ 45.45 ⇒ ceil = 46 → 46/1024
427        let x = BigRational::from_frac_u64(33, 32);
428        assert_eq!(x.log2_ceil(10), BigRational::from_frac_u64(46, 1024));
429
430        // -- Just below a power of two (negative, but tiny in magnitude)
431        // log2(255/256) ≈ −0.00565, k=8 → −0.00565 * 256 ≈ −1.44 ⇒ ceil = −1 → −1/256
432        let x = BigRational::from_frac_u64(255, 256);
433        assert_eq!(x.log2_ceil(8), BigRational::new((-1).into(), 256u32.into()));
434
435        // log2(1023/1024) ≈ −0.00141, k=9 → −0.00141 * 512 ≈ −0.72 ⇒ ceil = 0 → 0/512
436        let x = BigRational::from_frac_u64(1023, 1024);
437        assert_eq!(x.log2_ceil(9), BigRational::new(0.into(), 512u32.into()));
438
439        // -- k = 0 (integer ceiling of log2)
440        // log2(3/2) ≈ 0.585 ⇒ ceil = 1
441        let x = BigRational::from_frac_u64(3, 2);
442        assert_eq!(x.log2_ceil(0), BigRational::from_integer(1.into()));
443
444        // log2(3/4) ≈ −0.415 ⇒ ceil = 0
445        let x = BigRational::from_frac_u64(3, 4);
446        assert_eq!(x.log2_ceil(0), BigRational::from_integer(0.into()));
447
448        // -- x < 1 with fractional bits (negative dyadic output)
449        // log2(3/4) ≈ −0.415, k=4 → −0.415 * 16 ≈ −6.64 => ceil = −6 → −6/16
450        let x = BigRational::from_frac_u64(3, 4);
451        assert_eq!(x.log2_ceil(4), BigRational::new((-6).into(), 16u32.into()));
452
453        // -- Monotonic with k: increasing k refines the dyadic upwards
454        // For 257/256: k=8 → 0.00563*256 ≈ 1.44 ⇒ ceil=2 → 2/256
455        //              k=9 → 0.00563*512 ≈ 2.88 ⇒ ceil=3 → 3/512
456        let x = BigRational::from_frac_u64(257, 256);
457        assert_eq!(x.log2_ceil(8), BigRational::new(2.into(), 256u32.into()));
458        assert_eq!(x.log2_ceil(9), BigRational::new(3.into(), 512u32.into()));
459
460        // -- Scale invariance (multiply numerator and denominator by same factor, result unchanged)
461        // (17/16) * (2^30 / 2^30) has the same log2, the dyadic result should match 23/256 at k=8.
462        let num = BigInt::from(17u32) << 30;
463        let den = BigInt::from(16u32) << 30;
464        let x = BigRational::new(num, den);
465        assert_eq!(x.log2_ceil(8), BigRational::from_frac_u64(23, 256));
466    }
467
468    #[test]
469    #[should_panic(expected = "log2 undefined for non-positive numbers")]
470    fn log2_floor_negative_panics() {
471        <BigRational as num_traits::FromPrimitive>::from_i64(-1)
472            .unwrap()
473            .log2_floor(8);
474    }
475
476    #[test]
477    fn log2_floor_exact_powers_of_two() {
478        // Test exact powers of 2: log2(2^n) = n (same as ceil)
479        let value = BigRational::from_u64(1); // 2^0
480        assert_eq!(value.log2_floor(4), BigRational::from_u64(0));
481
482        let value = BigRational::from_u64(2); // 2^1
483        assert_eq!(value.log2_floor(4), BigRational::from_u64(1));
484
485        let value = BigRational::from_u64(8); // 2^3
486        assert_eq!(value.log2_floor(4), BigRational::from_u64(3));
487
488        let value = BigRational::from_u64(1024); // 2^10
489        assert_eq!(value.log2_floor(4), BigRational::from_u64(10));
490    }
491
492    #[test]
493    fn log2_floor_fractional_powers_of_two() {
494        // Test fractional powers of 2: log2(1/2) = -1, log2(1/4) = -2 (same as ceil)
495        let value = BigRational::from_frac_u64(1, 2); // 2^(-1)
496        let result = value.log2_floor(4);
497        assert_eq!(result, BigRational::from_integer(BigInt::from(-1)));
498
499        let value = BigRational::from_frac_u64(1, 4); // 2^(-2)
500        let result = value.log2_floor(4);
501        assert_eq!(result, BigRational::from_integer(BigInt::from(-2)));
502
503        // log2(3/8) ≈ -1.415, floor(-1.415 * 16) / 16 = -23/16
504        let value = BigRational::from_frac_u64(3, 8);
505        let result = value.log2_floor(4);
506        assert_eq!(
507            result,
508            BigRational::new(BigInt::from(-23), BigInt::from(16))
509        );
510    }
511
512    #[test]
513    fn log2_floor_simple_values() {
514        // log2(3) ≈ 1.585, with binary_digits=0 we get floor(1.585) = 1
515        let value = BigRational::from_u64(3);
516        let result = value.log2_floor(0);
517        assert_eq!(result, BigRational::from_u64(1));
518
519        // log2(5) ≈ 2.322, with binary_digits=0 we get floor(2.322) = 2
520        let value = BigRational::from_u64(5);
521        let result = value.log2_floor(0);
522        assert_eq!(result, BigRational::from_u64(2));
523
524        // With 4 bits precision
525        // log2(3) ≈ 1.585, floor(1.585 * 16) / 16 = 25/16
526        let value = BigRational::from_u64(3);
527        let result = value.log2_floor(4);
528        assert_eq!(result, BigRational::from_frac_u64(25, 16));
529    }
530
531    #[test]
532    fn log2_floor_rational_values() {
533        // log2(3/2) ≈ 0.585, floor(0.585 * 16) / 16 = 9/16
534        let value = BigRational::from_frac_u64(3, 2);
535        let result = value.log2_floor(4);
536        assert_eq!(result, BigRational::from_frac_u64(9, 16));
537
538        // log2(7/4) ≈ 0.807, floor(0.807 * 16) / 16 = 12/16
539        let value = BigRational::from_frac_u64(7, 4);
540        let result = value.log2_floor(4);
541        assert_eq!(result, BigRational::from_frac_u64(12, 16));
542    }
543
544    #[test]
545    fn log2_floor_different_precisions() {
546        let value = BigRational::from_u64(3);
547
548        // Test different precisions give reasonable results
549        let result0 = value.log2_floor(0);
550        let result1 = value.log2_floor(1);
551        let result4 = value.log2_floor(4);
552        let result8 = value.log2_floor(8);
553
554        assert_eq!(result0, BigRational::from_u64(1));
555        assert_eq!(result1, BigRational::from_frac_u64(3, 2));
556        assert_eq!(result4, BigRational::from_frac_u64(25, 16));
557        assert_eq!(
558            result8,
559            BigRational::new(BigInt::from(405), BigInt::from(256))
560        );
561    }
562
563    #[test]
564    fn log2_floor_large_values() {
565        // log2(1000) ≈ 9.966, floor = 9
566        let value = BigRational::from_u64(1000);
567        let result = value.log2_floor(4);
568        assert_eq!(result, BigRational::from_frac_u64(159, 16));
569    }
570
571    #[test]
572    fn log2_floor_very_small_values() {
573        // log2(1/1000) ≈ -9.966
574        let value = BigRational::from_frac_u64(1, 1000);
575        let result = value.log2_floor(4);
576        assert_eq!(
577            result,
578            BigRational::new(BigInt::from(-160), BigInt::from(16))
579        );
580    }
581
582    #[test]
583    fn log2_floor_edge_cases() {
584        // -- Just above a power of two
585        // log2(17/16) ≈ 0.087462, k=8 → floor(0.087462 * 256) = 22 → 22/256
586        let x = BigRational::from_frac_u64(17, 16);
587        assert_eq!(x.log2_floor(8), BigRational::from_frac_u64(22, 256));
588
589        // log2(129/128) ≈ 0.011227, k=8 → floor(0.011227 * 256) = 2 → 2/256
590        let x = BigRational::from_frac_u64(129, 128);
591        assert_eq!(x.log2_floor(8), BigRational::from_frac_u64(2, 256));
592
593        // log2(33/32) ≈ 0.044394, k=10 → floor(0.044394 * 1024) = 45 → 45/1024
594        let x = BigRational::from_frac_u64(33, 32);
595        assert_eq!(x.log2_floor(10), BigRational::from_frac_u64(45, 1024));
596
597        // -- Just below a power of two (negative, but tiny in magnitude)
598        // log2(255/256) ≈ -0.00565, k=8 → floor(-0.00565 * 256) = -2 → -2/256
599        let x = BigRational::from_frac_u64(255, 256);
600        assert_eq!(
601            x.log2_floor(8),
602            BigRational::new((-2).into(), 256u32.into())
603        );
604
605        // log2(1023/1024) ≈ -0.00141, k=9 → floor(-0.00141 * 512) = -1 → -1/512
606        let x = BigRational::from_frac_u64(1023, 1024);
607        assert_eq!(
608            x.log2_floor(9),
609            BigRational::new((-1).into(), 512u32.into())
610        );
611
612        // -- k = 0 (integer floor of log2)
613        // log2(3/2) ≈ 0.585 ⇒ floor = 0
614        let x = BigRational::from_frac_u64(3, 2);
615        assert_eq!(x.log2_floor(0), BigRational::from_integer(0.into()));
616
617        // log2(3/4) ≈ -0.415 ⇒ floor = -1
618        let x = BigRational::from_frac_u64(3, 4);
619        assert_eq!(x.log2_floor(0), BigRational::from_integer((-1).into()));
620
621        // -- x < 1 with fractional bits (negative dyadic output)
622        // log2(3/4) ≈ -0.415, k=4 → floor(-0.415 * 16) = -7 → -7/16
623        let x = BigRational::from_frac_u64(3, 4);
624        assert_eq!(x.log2_floor(4), BigRational::new((-7).into(), 16u32.into()));
625
626        // -- Monotonic with k: increasing k refines the dyadic downwards
627        // For 257/256: k=8 → floor(0.00563*256) = 1 → 1/256
628        //              k=9 → floor(0.00563*512) = 2 → 2/512
629        let x = BigRational::from_frac_u64(257, 256);
630        assert_eq!(x.log2_floor(8), BigRational::new(1.into(), 256u32.into()));
631        assert_eq!(x.log2_floor(9), BigRational::new(2.into(), 512u32.into()));
632
633        // -- Scale invariance (multiply numerator and denominator by same factor, result unchanged)
634        // (17/16) * (2^30 / 2^30) has the same log2, the dyadic result should match 22/256 at k=8.
635        let num = BigInt::from(17u32) << 30;
636        let den = BigInt::from(16u32) << 30;
637        let x = BigRational::new(num, den);
638        assert_eq!(x.log2_floor(8), BigRational::from_frac_u64(22, 256));
639    }
640
641    #[test]
642    fn log2_floor_ceil_relationship() {
643        // For any non-power-of-2 value, floor < ceil
644        // For exact powers of 2, floor == ceil
645        let test_values = [
646            BigRational::from_u64(3),
647            BigRational::from_u64(5),
648            BigRational::from_frac_u64(3, 2),
649            BigRational::from_frac_u64(7, 8),
650            BigRational::from_frac_u64(1, 3),
651        ];
652
653        for value in test_values {
654            let floor = value.log2_floor(8);
655            let ceil = value.log2_ceil(8);
656            assert!(
657                floor < ceil,
658                "floor should be less than ceil for non-power-of-2"
659            );
660        }
661
662        // Exact powers of 2
663        let powers_of_two = [
664            BigRational::from_u64(1),
665            BigRational::from_u64(2),
666            BigRational::from_u64(4),
667            BigRational::from_frac_u64(1, 2),
668            BigRational::from_frac_u64(1, 4),
669        ];
670
671        for value in powers_of_two {
672            let floor = value.log2_floor(8);
673            let ceil = value.log2_ceil(8);
674            assert_eq!(floor, ceil, "floor should equal ceil for power of 2");
675        }
676    }
677}