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}