Skip to main content

buff_rs/
precision.rs

1//! Precision bound computation for bounded floating-point values.
2//!
3//! This module implements the core algorithm for determining the minimum number
4//! of bits needed to represent bounded floating-point values within a given
5//! precision tolerance.
6
7/// Mask for extracting the exponent bits from an IEEE 754 double.
8const EXP_MASK: u64 = 0x7FF0_0000_0000_0000;
9
10/// Mask for the sign bit (most significant bit).
11const FIRST_ONE: u64 = 0x8000_0000_0000_0000;
12
13/// All ones (for complement operations).
14const NEG_ONE: u64 = 0xFFFF_FFFF_FFFF_FFFF;
15
16/// Precision bound calculator for floating-point values.
17///
18/// Given a precision tolerance (e.g., 0.00005 for 4 decimal places),
19/// this struct computes the minimum bits needed to represent values
20/// within that tolerance, and provides methods to convert floats to
21/// their fixed-point representation.
22#[derive(Debug, Clone)]
23pub struct PrecisionBound {
24    /// Current bit position during bound calculation.
25    position: u64,
26    /// The precision tolerance (e.g., 0.00005).
27    precision: f64,
28    /// Exponent of the precision (derived from IEEE 754 representation).
29    precision_exp: i32,
30    /// Number of bits needed for the integer part.
31    int_length: u64,
32    /// Number of bits needed for the decimal part.
33    decimal_length: u64,
34}
35
36impl PrecisionBound {
37    /// Create a new precision bound calculator.
38    ///
39    /// # Arguments
40    /// * `precision` - The precision tolerance (e.g., 0.00005 for 4 decimal places)
41    ///
42    /// # Example
43    /// ```
44    /// use buff_rs::precision::PrecisionBound;
45    ///
46    /// let bound = PrecisionBound::new(0.00005);
47    /// ```
48    pub fn new(precision: f64) -> Self {
49        let xu = precision.to_bits();
50        let precision_exp = ((xu & EXP_MASK) >> 52) as i32 - 1023;
51
52        PrecisionBound {
53            position: 0,
54            precision,
55            precision_exp,
56            int_length: 0,
57            decimal_length: 0,
58        }
59    }
60
61    /// Compute the precision-bounded representation of a float.
62    ///
63    /// This finds the minimum precision representation of `orig` that
64    /// is within the precision tolerance.
65    pub fn precision_bound(&mut self, orig: f64) -> f64 {
66        let mask = !0u64;
67        let origu = orig.to_bits();
68
69        let mut curu = origu & (mask << self.position) | (1u64 << self.position);
70        let mut cur = f64::from_bits(curu);
71        let mut pre = cur;
72        let bounded = self.is_bounded(orig, cur);
73
74        if bounded {
75            // Find first bit where it's not bounded
76            loop {
77                if self.position == 52 {
78                    return pre;
79                }
80                self.position += 1;
81                curu = origu & (mask << self.position) | (1u64 << self.position);
82                cur = f64::from_bits(curu);
83                if !self.is_bounded(orig, cur) {
84                    break;
85                }
86                pre = cur;
87            }
88        } else {
89            // Find the first bit where it's bounded
90            loop {
91                if self.position == 0 {
92                    break;
93                }
94                self.position -= 1;
95                curu = origu & (mask << self.position) | (1u64 << self.position);
96                cur = f64::from_bits(curu);
97                if self.is_bounded(orig, cur) {
98                    pre = cur;
99                    break;
100                }
101            }
102        }
103        pre
104    }
105
106    /// Calculate and update the required bit lengths for a precision-bounded value.
107    pub fn cal_length(&mut self, x: f64) {
108        let xu = x.to_bits();
109        let trailing_zeros = xu.trailing_zeros();
110        let exp = ((xu & EXP_MASK) >> 52) as i32 - 1023;
111
112        let mut dec_length = 0u64;
113        if trailing_zeros >= 52 {
114            if exp < 0 {
115                dec_length = (-exp) as u64;
116                if exp < self.precision_exp {
117                    dec_length = 0;
118                }
119            }
120        } else if (52 - trailing_zeros as i32) > exp {
121            dec_length = ((52 - trailing_zeros as i32) - exp) as u64;
122        }
123
124        if exp >= 0 && (exp + 1) as u64 > self.int_length {
125            self.int_length = (exp + 1) as u64;
126        }
127        if dec_length > self.decimal_length {
128            self.decimal_length = dec_length;
129        }
130    }
131
132    /// Get the computed integer and decimal bit lengths.
133    pub fn get_length(&self) -> (u64, u64) {
134        (self.int_length, self.decimal_length)
135    }
136
137    /// Set the integer and decimal bit lengths manually.
138    #[inline]
139    pub fn set_length(&mut self, ilen: u64, dlen: u64) {
140        self.int_length = ilen;
141        self.decimal_length = dlen;
142    }
143
144    /// Fetch the integer and decimal components of a precision-bounded float.
145    #[inline]
146    pub fn fetch_components(&self, bd: f64) -> (i64, u64) {
147        let bdu = bd.to_bits();
148        let exp = ((bdu & EXP_MASK) >> 52) as i32 - 1023;
149        let sign = bdu & FIRST_ONE;
150        let mut int_part = 0u64;
151        let mut dec_part: u64;
152
153        if exp >= 0 {
154            dec_part = bdu << (12 + exp) as u64;
155            int_part = ((bdu << 11) | FIRST_ONE) >> (63 - exp) as u64;
156            if sign != 0 {
157                int_part = !int_part;
158                dec_part = (!dec_part).wrapping_add(1);
159            }
160        } else if exp < self.precision_exp {
161            dec_part = 0u64;
162            if sign != 0 {
163                int_part = NEG_ONE;
164                dec_part = !dec_part;
165            }
166        } else {
167            dec_part = ((bdu << 11) | FIRST_ONE) >> ((-exp - 1) as u64);
168            if sign != 0 {
169                int_part = NEG_ONE;
170                dec_part = !dec_part;
171            }
172        }
173
174        let signed_int = int_part as i64;
175        (signed_int, dec_part >> (64u64 - self.decimal_length))
176    }
177
178    /// Fetch the byte-aligned fixed-point representation.
179    ///
180    /// This converts a float to its fixed-point representation suitable
181    /// for byte-sliced storage.
182    #[inline]
183    pub fn fetch_fixed_aligned(&self, bd: f64) -> i64 {
184        let bdu = bd.to_bits();
185        let exp = ((bdu & EXP_MASK) >> 52) as i32 - 1023;
186        let sign = bdu & FIRST_ONE;
187        let mut fixed: u64;
188
189        if exp < self.precision_exp {
190            fixed = 0u64;
191        } else {
192            fixed = ((bdu << 11) | FIRST_ONE) >> (63 - exp - self.decimal_length as i32) as u64;
193            if sign != 0 {
194                fixed = !(fixed.wrapping_sub(1));
195            }
196        }
197
198        fixed as i64
199    }
200
201    /// Check if two values are within the precision tolerance.
202    #[inline]
203    pub fn is_bounded(&self, a: f64, b: f64) -> bool {
204        (a - b).abs() < self.precision
205    }
206}
207
208/// Get the precision bound value for a given number of decimal places.
209///
210/// # Arguments
211/// * `precision` - Number of decimal places (e.g., 4 for 0.0001 precision)
212///
213/// # Returns
214/// The precision tolerance value (e.g., 0.000049 for 4 decimal places)
215pub fn get_precision_bound(precision: i32) -> f64 {
216    if precision <= 0 {
217        return 0.49;
218    }
219
220    let mut s = String::from("0.");
221    for _ in 0..precision {
222        s.push('0');
223    }
224    s.push_str("49");
225    s.parse().unwrap_or(0.49)
226}
227
228/// Precomputed decimal lengths for common precision values.
229///
230/// Maps precision (decimal places) to the number of bits needed for the decimal part.
231pub const PRECISION_MAP: [(i32, u64); 16] = [
232    (0, 0),
233    (1, 4),
234    (2, 7),
235    (3, 10),
236    (4, 14),
237    (5, 17),
238    (6, 20),
239    (7, 24),
240    (8, 27),
241    (9, 30),
242    (10, 34),
243    (11, 37),
244    (12, 40),
245    (13, 44),
246    (14, 47),
247    (15, 50),
248];
249
250/// Get the decimal length for a given precision.
251pub fn get_decimal_length(precision: i32) -> u64 {
252    PRECISION_MAP
253        .iter()
254        .find(|(p, _)| *p == precision)
255        .map(|(_, len)| *len)
256        .unwrap_or(0)
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_precision_bound_creation() {
265        let bound = PrecisionBound::new(0.00005);
266        assert!(bound.precision > 0.0);
267    }
268
269    #[test]
270    fn test_get_precision_bound() {
271        let pb = get_precision_bound(4);
272        assert!(pb > 0.0 && pb < 0.001);
273    }
274
275    #[test]
276    fn test_is_bounded() {
277        let bound = PrecisionBound::new(0.05);
278        assert!(bound.is_bounded(1.0, 1.01));
279        assert!(!bound.is_bounded(1.0, 1.1));
280    }
281
282    #[test]
283    fn test_precision_map() {
284        assert_eq!(get_decimal_length(0), 0);
285        assert_eq!(get_decimal_length(4), 14);
286        assert_eq!(get_decimal_length(10), 34);
287    }
288
289    #[test]
290    fn test_fetch_fixed_aligned() {
291        let mut bound = PrecisionBound::new(0.00005);
292        bound.set_length(4, 14);
293
294        let fixed = bound.fetch_fixed_aligned(3.14159);
295        assert!(fixed > 0);
296    }
297
298    #[test]
299    fn test_fetch_fixed_aligned_negative() {
300        let mut bound = PrecisionBound::new(0.00005);
301        bound.set_length(4, 14);
302
303        let fixed = bound.fetch_fixed_aligned(-3.14159);
304        assert!(fixed < 0);
305    }
306
307    #[test]
308    fn test_fetch_fixed_aligned_zero() {
309        let mut bound = PrecisionBound::new(0.00005);
310        bound.set_length(4, 14);
311
312        let fixed = bound.fetch_fixed_aligned(0.0);
313        assert_eq!(fixed, 0);
314    }
315
316    #[test]
317    fn test_fetch_fixed_aligned_small() {
318        let mut bound = PrecisionBound::new(0.00005);
319        bound.set_length(4, 14);
320
321        // Very small value below precision threshold
322        let fixed = bound.fetch_fixed_aligned(0.00001);
323        // Should be within precision bounds
324        assert!(fixed.abs() <= 1);
325    }
326
327    #[test]
328    fn test_get_length() {
329        let mut bound = PrecisionBound::new(0.00005);
330        bound.set_length(8, 16);
331
332        let (ilen, dlen) = bound.get_length();
333        assert_eq!(ilen, 8);
334        assert_eq!(dlen, 16);
335    }
336
337    #[test]
338    fn test_cal_length() {
339        let mut bound = PrecisionBound::new(0.00005);
340
341        // Large value should increase int_length
342        bound.cal_length(1000.0);
343        let (ilen, _dlen) = bound.get_length();
344        assert!(ilen > 0);
345
346        // Small decimal should affect decimal_length
347        let mut bound2 = PrecisionBound::new(0.00005);
348        bound2.cal_length(0.123456);
349        let (_, dlen) = bound2.get_length();
350        assert!(dlen > 0);
351    }
352
353    #[test]
354    fn test_fetch_components() {
355        let mut bound = PrecisionBound::new(0.00005);
356        bound.set_length(4, 14);
357
358        let (int_part, dec_part) = bound.fetch_components(3.5);
359        assert_eq!(int_part, 3);
360        assert!(dec_part > 0);
361    }
362
363    #[test]
364    fn test_fetch_components_negative() {
365        let mut bound = PrecisionBound::new(0.00005);
366        bound.set_length(4, 14);
367
368        let (int_part, _dec_part) = bound.fetch_components(-3.5);
369        assert!(int_part < 0);
370    }
371
372    #[test]
373    fn test_fetch_components_small_value() {
374        let mut bound = PrecisionBound::new(0.00005);
375        bound.set_length(0, 14);
376
377        // Value smaller than precision threshold
378        let (int_part, dec_part) = bound.fetch_components(0.001);
379        assert_eq!(int_part, 0);
380        assert!(dec_part >= 0);
381    }
382
383    #[test]
384    fn test_precision_bound_method() {
385        let mut bound = PrecisionBound::new(0.005);
386
387        // Should find bounded representation
388        let result = bound.precision_bound(3.14159);
389        assert!((result - 3.14159).abs() < 0.01);
390    }
391
392    #[test]
393    fn test_precision_bound_exact() {
394        let mut bound = PrecisionBound::new(0.05);
395
396        // Value that's already at a good precision boundary
397        let result = bound.precision_bound(1.0);
398        assert!((result - 1.0).abs() < 0.1);
399    }
400
401    #[test]
402    fn test_get_precision_bound_zero() {
403        let pb = get_precision_bound(0);
404        assert!((pb - 0.49).abs() < 0.01);
405    }
406
407    #[test]
408    fn test_get_precision_bound_negative() {
409        let pb = get_precision_bound(-5);
410        assert!((pb - 0.49).abs() < 0.01);
411    }
412
413    #[test]
414    fn test_get_precision_bound_high() {
415        let pb = get_precision_bound(10);
416        assert!(pb < 0.0000000001);
417        assert!(pb > 0.0);
418    }
419
420    #[test]
421    fn test_get_decimal_length_unknown() {
422        // Precision value not in the map
423        let len = get_decimal_length(99);
424        assert_eq!(len, 0);
425    }
426
427    #[test]
428    fn test_get_decimal_length_all_values() {
429        // Test all values in PRECISION_MAP
430        for (prec, expected_len) in PRECISION_MAP {
431            assert_eq!(get_decimal_length(prec), expected_len);
432        }
433    }
434
435    #[test]
436    fn test_precision_bound_debug() {
437        let bound = PrecisionBound::new(0.00005);
438        let debug_str = format!("{:?}", bound);
439        assert!(debug_str.contains("PrecisionBound"));
440    }
441
442    #[test]
443    fn test_precision_bound_clone() {
444        let bound = PrecisionBound::new(0.00005);
445        let cloned = bound.clone();
446        assert_eq!(bound.precision, cloned.precision);
447    }
448
449    #[test]
450    fn test_cal_length_negative_exponent() {
451        let mut bound = PrecisionBound::new(0.0005);
452
453        // Value with negative exponent (very small)
454        bound.cal_length(0.00123);
455        let (_, dlen) = bound.get_length();
456        assert!(dlen > 0);
457    }
458
459    #[test]
460    fn test_cal_length_large_exponent() {
461        let mut bound = PrecisionBound::new(0.0005);
462
463        // Value with large exponent
464        bound.cal_length(1000000.0);
465        let (ilen, _) = bound.get_length();
466        assert!(ilen > 10); // 2^20 ≈ 1M, so need at least 20 bits
467    }
468
469    #[test]
470    fn test_cal_length_power_of_two() {
471        let mut bound = PrecisionBound::new(0.0005);
472
473        // Exact power of two
474        bound.cal_length(1024.0);
475        let (ilen, _) = bound.get_length();
476        assert!(ilen > 0);
477    }
478
479    #[test]
480    fn test_fetch_components_power_of_two() {
481        let mut bound = PrecisionBound::new(0.00005);
482        bound.set_length(16, 14);
483
484        let (int_part, dec_part) = bound.fetch_components(256.0);
485        assert_eq!(int_part, 256);
486        assert_eq!(dec_part, 0);
487    }
488
489    #[test]
490    fn test_fetch_components_with_decimal() {
491        let mut bound = PrecisionBound::new(0.00005);
492        bound.set_length(4, 14);
493
494        let (int_part, dec_part) = bound.fetch_components(5.5);
495        assert_eq!(int_part, 5);
496        assert!(dec_part > 0); // Should have decimal part
497    }
498
499    #[test]
500    fn test_precision_bound_very_tight() {
501        let mut bound = PrecisionBound::new(0.000001); // Very tight precision
502
503        let result = bound.precision_bound(1.23456789);
504        assert!((result - 1.23456789).abs() < 0.00001);
505    }
506
507    #[test]
508    fn test_precision_bound_loose() {
509        let mut bound = PrecisionBound::new(0.5); // Loose precision
510
511        let result = bound.precision_bound(1.7);
512        assert!((result - 1.7).abs() < 1.0);
513    }
514
515    #[test]
516    fn test_fetch_fixed_aligned_large_value() {
517        let mut bound = PrecisionBound::new(0.00005);
518        bound.set_length(20, 14);
519
520        let fixed = bound.fetch_fixed_aligned(1000000.123);
521        assert!(fixed > 0);
522    }
523
524    #[test]
525    fn test_fetch_fixed_aligned_very_small() {
526        let mut bound = PrecisionBound::new(0.00005);
527        bound.set_length(0, 14);
528
529        // Value smaller than precision - should be 0
530        let fixed = bound.fetch_fixed_aligned(0.0000001);
531        assert_eq!(fixed, 0);
532    }
533
534    #[test]
535    fn test_is_bounded_same_value() {
536        let bound = PrecisionBound::new(0.05);
537        assert!(bound.is_bounded(5.0, 5.0));
538    }
539
540    #[test]
541    fn test_is_bounded_at_boundary() {
542        let bound = PrecisionBound::new(0.05);
543        // Exactly at the boundary
544        assert!(bound.is_bounded(1.0, 1.049));
545        assert!(!bound.is_bounded(1.0, 1.06));
546    }
547
548    #[test]
549    fn test_get_precision_bound_various() {
550        for prec in 1..=15 {
551            let pb = get_precision_bound(prec);
552            assert!(pb > 0.0);
553            assert!(pb < 1.0);
554        }
555    }
556
557    #[test]
558    fn test_precision_map_coverage() {
559        // Verify all precision map entries are valid
560        for (prec, len) in PRECISION_MAP {
561            assert!(prec >= 0);
562            assert!(len <= 64);
563        }
564    }
565
566    #[test]
567    fn test_fetch_components_negative_exponent_below_precision() {
568        let mut bound = PrecisionBound::new(0.005); // precision_exp around -8
569        bound.set_length(0, 10);
570
571        // Value with exponent below precision threshold
572        let (int_part, dec_part) = bound.fetch_components(0.0000001);
573        assert_eq!(int_part, 0);
574        // dec_part should be 0 or very small
575        assert!(dec_part == 0 || dec_part < 10);
576    }
577
578    #[test]
579    fn test_fetch_components_negative_value_small() {
580        let mut bound = PrecisionBound::new(0.005);
581        bound.set_length(0, 10);
582
583        let (int_part, dec_part) = bound.fetch_components(-0.001);
584        // For small negative values below precision
585        assert!(int_part <= 0 || dec_part > 0);
586    }
587
588    #[test]
589    fn test_cal_length_trailing_zeros() {
590        let mut bound = PrecisionBound::new(0.0005);
591
592        // Value with many trailing zeros in binary representation
593        bound.cal_length(8.0); // 8 = 2^3, has trailing zeros
594        let (ilen, _) = bound.get_length();
595        assert!(ilen >= 4); // 2^4 > 8
596    }
597
598    #[test]
599    fn test_precision_bound_position_52() {
600        // Test where position reaches 52 (line 78)
601        let mut bound = PrecisionBound::new(1.0); // Very loose precision
602
603        // Use a value where most bits don't matter
604        let result = bound.precision_bound(1.0);
605        assert!((result - 1.0).abs() < 2.0);
606    }
607
608    #[test]
609    fn test_precision_bound_not_bounded_initially() {
610        // Test the else branch (lines 91-102) where initial value is not bounded
611        let mut bound = PrecisionBound::new(0.0000001); // Very tight precision
612
613        // Force position to be non-zero by calling precision_bound multiple times
614        let _ = bound.precision_bound(1.5);
615        // Now call with a value that's not bounded at current position
616        let result = bound.precision_bound(2.123456789);
617        assert!((result - 2.123456789).abs() < 0.001);
618    }
619
620    #[test]
621    fn test_precision_bound_position_zero() {
622        // Test where position reaches 0 (line 92)
623        let mut bound = PrecisionBound::new(0.0000000001); // Extremely tight
624
625        let result = bound.precision_bound(1.123456789012345);
626        assert!((result - 1.123456789012345).abs() < 0.0001);
627    }
628
629    #[test]
630    fn test_fetch_components_negative_exp_below_precision() {
631        // Test the branch where exp < precision_exp (lines 161-166)
632        let mut bound = PrecisionBound::new(0.01); // precision_exp around -7
633        bound.set_length(0, 14);
634
635        // Use a very small negative value
636        let (int_part, dec_part) = bound.fetch_components(-0.0001);
637        // Should trigger the exp < precision_exp branch
638        assert!(int_part < 0 || dec_part > 0);
639    }
640
641    #[test]
642    fn test_fetch_components_negative_sign_handling() {
643        // Test negative value handling in fetch_components (lines 169-170)
644        let mut bound = PrecisionBound::new(0.00005);
645        bound.set_length(4, 14);
646
647        let (int_part, dec_part) = bound.fetch_components(-0.5);
648        // Negative value with negative exponent
649        assert!(int_part < 0 || dec_part > 0);
650    }
651
652    #[test]
653    fn test_fetch_fixed_aligned_negative_exp_above_precision() {
654        // Test where exp is negative but >= precision_exp (line 193)
655        let mut bound = PrecisionBound::new(0.01);
656        bound.set_length(0, 14);
657
658        let fixed = bound.fetch_fixed_aligned(0.25); // exp = -2, which is > -7
659        assert!(fixed != 0);
660    }
661
662    #[test]
663    fn test_fetch_fixed_aligned_negative_sign() {
664        // Test negative value handling in fetch_fixed_aligned (line 195)
665        let mut bound = PrecisionBound::new(0.00005);
666        bound.set_length(4, 14);
667
668        let fixed = bound.fetch_fixed_aligned(-0.25);
669        assert!(fixed < 0);
670    }
671
672    #[test]
673    fn test_cal_length_exp_negative_above_precision() {
674        // Test cal_length where exp < 0 and exp >= precision_exp (lines 115-117)
675        let mut bound = PrecisionBound::new(0.01); // precision_exp ~ -7
676
677        bound.cal_length(0.25); // exp = -2, which is > -7
678        let (_, dlen) = bound.get_length();
679        assert!(dlen > 0);
680    }
681
682    #[test]
683    fn test_cal_length_trailing_zeros_with_decimal() {
684        // Test cal_length where trailing_zeros >= 52 and exp < 0 (line 115)
685        let mut bound = PrecisionBound::new(0.1);
686
687        // Power of 2 fraction: 0.5 = 2^-1
688        bound.cal_length(0.5);
689        let (_, dlen) = bound.get_length();
690        assert!(dlen >= 0);
691    }
692
693    #[test]
694    fn test_precision_bound_finds_bounded_then_not() {
695        // Test where we start bounded, then find unbounded (line 84-85)
696        let mut bound = PrecisionBound::new(0.1);
697
698        let result = bound.precision_bound(3.14159);
699        // Should find a bounded representation
700        assert!((result - 3.14159).abs() < 0.2);
701    }
702
703    #[test]
704    fn test_precision_bound_finds_unbounded_then_bounded() {
705        // Test where we start unbounded, find bounded (lines 98-100)
706        let mut bound = PrecisionBound::new(0.001);
707        bound.position = 10; // Start at a higher position
708
709        let result = bound.precision_bound(1.5);
710        assert!((result - 1.5).abs() < 0.01);
711    }
712}