Skip to main content

rand_float/
badizadegan.rs

1// Derived from the fp-rand reference implementations (fp_rand.hpp /
2// fpRand.go), at https://github.com/specbranch/fp-rand/, distributed under
3// the following license:
4//
5// MIT License
6//
7// Copyright (c) 2025 Nima Badizadegan
8//
9// Permission is hereby granted, free of charge, to any person obtaining a copy
10// of this software and associated documentation files (the "Software"), to deal
11// in the Software without restriction, including without limitation the rights
12// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13// copies of the Software, and to permit persons to whom the Software is
14// furnished to do so, subject to the following conditions:
15//
16// The above copyright notice and this permission notice shall be included in all
17// copies or substantial portions of the Software.
18//
19// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25// SOFTWARE.
26
27//! Nima Badizadegan's “perfect” conversion.
28//!
29//! A Rust port of the round-down variant of [Nima Badizadegan's
30//! algorithm](https://github.com/specbranch/fp-rand/).
31//!
32//! The functions in this module return a value distributed exactly as if a real
33//! number had been drawn uniformly from (0 . . 1) and then rounded down (toward
34//! −∞) to a representable floating-point value. Every float in [0 . . 1),
35//! including every subnormal and 0 itself, is returned with probability equal
36//! to the measure of the interval of reals that rounds down to it.
37//!
38//! ```
39//! let mut src = rand_float::sources::Weyl(42);
40//!
41//! let x = rand_float::badizadegan::f64_down(|| src.next_u64());
42//! assert!((0.0..1.0).contains(&x));
43//! let y = rand_float::badizadegan::f32_down(|| src.next_u64());
44//! assert!((0.0..1.0).contains(&y));
45//! ```
46
47/// Number of explicit mantissa bits of an IEEE 754 binary64.
48const F64_MBITS: u32 = 52;
49/// Exponent bias of an IEEE 754 binary64.
50const F64_EBIAS: u32 = 1023;
51
52/// Number of explicit mantissa bits of an IEEE 754 binary32.
53const F32_MBITS: u32 = 23;
54/// Exponent bias of an IEEE 754 binary32.
55const F32_EBIAS: u32 = 127;
56
57/// A buffer of random bits drawn from a 64-bit entropy source.
58///
59/// [`get_bits`](Self::get_bits) hands out `n` bits at a time, drawing a new
60/// 64-bit word from the source only when the buffered bits run out; the
61/// remainder of each draw is kept for subsequent requests. A pool lives for
62/// a single top-level generation call.
63struct EntropyPool<F> {
64    src: F,
65    pool: u64,
66    nbits: u32,
67}
68
69impl<F: FnMut() -> u64> EntropyPool<F> {
70    #[inline]
71    fn new(mut src: F) -> Self {
72        // Draw the first word eagerly: every conversion starts by requesting
73        // more bits than an empty pool holds, so this draw is never wasted,
74        // and it makes the refill in [`get_bits`](Self::get_bits) a genuinely
75        // rare path, as its cold hint promises.
76        let pool = src();
77        Self {
78            src,
79            pool,
80            nbits: 64,
81        }
82    }
83
84    /// Returns `n` (< 64) uniform random bits in the low bits of the result.
85    #[inline]
86    fn get_bits(&mut self, n: u32) -> u64 {
87        debug_assert!(n < 64);
88        let mut result = self.pool;
89
90        if self.nbits < n {
91            // Kluge; see [`crate::cold::cold_barrier`].
92            crate::cold::cold_barrier();
93            let needed = n - self.nbits;
94            self.pool = (self.src)();
95            result |= self.pool << self.nbits;
96            self.pool >>= needed;
97            self.nbits = 64 - needed;
98        } else {
99            self.nbits -= n;
100            self.pool >>= n;
101        }
102
103        result & ((1u64 << n) - 1)
104    }
105}
106
107/// Stage 1 for `f64`: locates the result’s binade and produces the partial
108/// result.
109///
110/// Returns the IEEE 754 representation of the partial result and the number
111/// of vacant trailing significand bits that stage 2 must backfill.
112#[inline]
113fn seek64<F: FnMut() -> u64>(pool: &mut EntropyPool<F>) -> (u64, u32) {
114    let mut a = pool.get_bits(F64_MBITS);
115    let mut base = 1.0f64.to_bits();
116    let mut nb = 0;
117
118    // Zoom down, one 52-binade window at a time, while the mantissa is zero.
119    while a == 0 {
120        if base < ((F64_MBITS as u64) << F64_MBITS) {
121            // Bottom window, reaching the subnormals: only EBIAS mod MBITS
122            // bits remain, drawn aligned to the top of the mantissa field.
123            const B: u32 = F64_EBIAS % F64_MBITS;
124            nb = F64_MBITS - B;
125            a = pool.get_bits(B) << nb;
126            break;
127        }
128
129        a = pool.get_bits(F64_MBITS);
130        base -= (F64_MBITS as u64) << F64_MBITS;
131    }
132
133    // Add the bits to the base as an integer, subtract the base in floating
134    // point: the difference is the (renormalized) partial result, and the
135    // exponent drop tells how many trailing bits were left vacant.
136    a += base;
137    let b = f64::from_bits(a) - f64::from_bits(base);
138    nb += (base >> F64_MBITS) as u32 - (b.to_bits() >> F64_MBITS) as u32;
139    (b.to_bits(), nb)
140}
141
142/// Stage 1 for `f32`; see [`seek64`].
143#[inline]
144fn seek32<F: FnMut() -> u64>(pool: &mut EntropyPool<F>) -> (u32, u32) {
145    let mut a = pool.get_bits(F32_MBITS) as u32;
146    let mut base = 1.0f32.to_bits();
147    let mut nb = 0;
148
149    while a == 0 {
150        if base < (F32_MBITS << F32_MBITS) {
151            const B: u32 = F32_EBIAS % F32_MBITS;
152            nb = F32_MBITS - B;
153            a = (pool.get_bits(B) as u32) << nb;
154            break;
155        }
156
157        a = pool.get_bits(F32_MBITS) as u32;
158        base -= F32_MBITS << F32_MBITS;
159    }
160
161    a += base;
162    let b = f32::from_bits(a) - f32::from_bits(base);
163    nb += (base >> F32_MBITS) - (b.to_bits() >> F32_MBITS);
164    (b.to_bits(), nb)
165}
166
167/// Returns a random `f64` distributed as a uniform real in (0 . . 1) rounded
168/// **down** (toward −∞) to the nearest representable value.
169///
170/// The result lies in [0 . . 1); every `f64` in that range, including every
171/// subnormal and 0, is returned with probability equal to the measure of
172/// the reals that round down to it. See the [module documentation](self)
173/// for the algorithm.
174///
175/// `bits` must return independent uniform random 64-bit words. Exactly one
176/// word is consumed with probability ≈ 1 − 2⁻¹²; the expected number of
177/// words per call is 1 + 2⁻¹² + O(2⁻⁵²).
178///
179/// To call it repeatedly on the same source, pass the source by mutable
180/// reference:
181///
182/// ```
183/// // A Weyl sequence: statistically very poor, for illustration only.
184/// let mut src = rand_float::sources::Weyl(1);
185/// let mut next = || src.next_u64();
186/// let x = rand_float::badizadegan::f64_down(&mut next);
187/// let y = rand_float::badizadegan::f64_down(&mut next);
188/// assert!(x != y);
189/// ```
190#[inline]
191pub fn f64_down(bits: impl FnMut() -> u64) -> f64 {
192    let mut pool = EntropyPool::new(bits);
193    let (partial, nb) = seek64(&mut pool);
194    // Round down: backfill the vacant trailing bits with random bits. The
195    // low nb bits of `partial` are zero, so the addition never carries.
196    f64::from_bits(pool.get_bits(nb) + partial)
197}
198
199/// Returns a random `f32` distributed as a uniform real in (0 . . 1) rounded
200/// **down** (toward −∞) to the nearest representable value.
201///
202/// The `f32` counterpart of [`f64_down`]; the result lies in [0 . . 1) and
203/// every `f32` in that range is reachable, including subnormals and 0.
204#[inline]
205pub fn f32_down(bits: impl FnMut() -> u64) -> f32 {
206    let mut pool = EntropyPool::new(bits);
207    let (partial, nb) = seek32(&mut pool);
208    f32::from_bits(pool.get_bits(nb) as u32 + partial)
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::sources::Weyl;
215
216    /// A source that replays a fixed sequence of words, then panics.
217    fn replay(words: &[u64]) -> impl FnMut() -> u64 + '_ {
218        let mut iter = words.iter();
219        move || *iter.next().expect("source exhausted")
220    }
221
222    #[test]
223    fn test_known_values_f64() {
224        // Mantissa 2^51 -> partial 0.5, one vacant bit, backfilled with the
225        // leftover bit 52 of the same word.
226        assert_eq!(f64_down(replay(&[1 << 51])), 0.5);
227        assert_eq!(
228            f64_down(replay(&[(1 << 51) | (1 << 52)])),
229            f64::from_bits(0.5f64.to_bits() + 1)
230        );
231        // Full mantissa: partial is 1 - 2^-52, i.e. the largest f64 below 1,
232        // with one vacant bit... which is bit 52 of the word.
233        assert_eq!(
234            f64_down(replay(&[(1 << 52) - 1])),
235            f64::from_bits(1.0f64.to_bits() - 2)
236        );
237        assert_eq!(
238            f64_down(replay(&[(1 << 53) - 1])),
239            f64::from_bits(1.0f64.to_bits() - 1)
240        );
241        // Mantissa 1: partial 2^-52, 52 vacant bits backfilled from the 12
242        // leftover bits plus 40 bits of a second word.
243        assert_eq!(
244            f64_down(replay(&[1, 0])),
245            f64::from_bits(0x3CB0000000000000)
246        );
247    }
248
249    #[test]
250    fn test_all_zeros_terminates_and_yields_zero() {
251        // A broken all-zero source must walk down all exponent windows and
252        // come out with exactly 0.0 (and must not loop forever).
253        let mut n_calls = 0u32;
254        let zero_src = || {
255            n_calls += 1;
256            assert!(n_calls < 64, "zoom loop does not terminate");
257            0u64
258        };
259        assert_eq!(f64_down(zero_src), 0.0);
260    }
261
262    #[test]
263    fn test_all_zeros_terminates_and_yields_zero_f32() {
264        assert_eq!(f32_down(replay(&[0, 0, 0])), 0.0);
265    }
266
267    #[test]
268    fn test_all_ones_source() {
269        let x = f64_down(replay(&[!0u64]));
270        assert_eq!(x, f64::from_bits(1.0f64.to_bits() - 1));
271    }
272
273    #[test]
274    fn test_range_and_moments_f64() {
275        let mut src = Weyl(0xDEADBEEF);
276        let n = 1_000_000;
277        let mut sum = 0.0;
278        let mut top_binade = 0u32;
279        for _ in 0..n {
280            let x = f64_down(|| src.next_u64());
281            assert!((0.0..1.0).contains(&x), "out of range: {x}");
282            sum += x;
283            if x >= 0.5 {
284                top_binade += 1;
285            }
286        }
287        let mean = sum / n as f64;
288        // Standard error of the mean is ~2.9e-4; 5 sigma.
289        assert!((mean - 0.5).abs() < 1.5e-3, "mean {mean}");
290        // P(x >= 1/2) = 1/2; 5 sigma is ~2.5e-3.
291        let frac = top_binade as f64 / n as f64;
292        assert!((frac - 0.5).abs() < 2.5e-3, "top-binade fraction {frac}");
293    }
294
295    #[test]
296    fn test_range_and_moments_f32() {
297        let mut src = Weyl(0xC0FFEE);
298        let n = 1_000_000;
299        let mut sum = 0.0f64;
300        for _ in 0..n {
301            let x = f32_down(|| src.next_u64());
302            assert!((0.0..1.0).contains(&x), "out of range: {x}");
303            sum += x as f64;
304        }
305        let mean = sum / n as f64;
306        assert!((mean - 0.5).abs() < 1.5e-3, "mean {mean}");
307    }
308
309    #[test]
310    fn test_low_binades_are_reachable_and_correctly_distributed() {
311        // With 10^6 samples, P(x < 2^-10) should be ~2^-10 (~977 hits).
312        let mut src = Weyl(42);
313        let n = 1_000_000;
314        let threshold = 1.0 / 1024.0;
315        let mut hits = 0u32;
316        for _ in 0..n {
317            if f64_down(|| src.next_u64()) < threshold {
318                hits += 1;
319            }
320        }
321        // Binomial(10^6, 2^-10): mean ~977, sigma ~31; allow 5 sigma.
322        assert!((820..1140).contains(&hits), "hits {hits}");
323    }
324
325    #[test]
326    fn test_entropy_pool_bit_accounting() {
327        // 64 bits requested 8 at a time must reproduce the word exactly.
328        let word = 0x0123_4567_89AB_CDEFu64;
329        let words = [word];
330        let mut pool = EntropyPool::new(replay(&words));
331        for i in 0..8 {
332            assert_eq!(pool.get_bits(8), (word >> (8 * i)) & 0xFF);
333        }
334        // Zero-width requests are free.
335        assert_eq!(pool.get_bits(0), 0);
336    }
337}