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}