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