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}