Skip to main content

rand_float/
pekkizen.rs

1// Derived from the uniFloats wiki https://github.com/pekkizen/prng (functions
2// Float64_64, Float64_117 and Float64full), distributed under the following
3// license:
4//
5// Copyright (c) 2020, Pekka Pulkkinen
6//
7// Copying and distribution of this article, ideas and source code are permitted
8// worldwide, without royalty, in any medium, provided the copyright notice is
9// preserved and a reference to this article is given.
10
11//! Pekka Pulkkinen’s leading-zeros technique.
12//!
13//! A Rust port of `Float64_64`, `Float64_117` and `Float64full` from
14//! [Pekka Pulkkinen’s uniFloats wiki]. One 64-bit word is
15//! interpreted as a uniform fixed-point real in [0 . . 1): the count of leading
16//! zeros picks the binade (a geometric distribution), and the remaining bits
17//! are shifted into the mantissa.
18//!
19//! The three variants differ in how far down the fixed point extends:
20//!
21//! - [`f64_64`] stops at the first word: a uniform real rounded down to the
22//!   2⁻⁶⁴ grid, complete (every float reachable) in [2⁻¹² . . 1);
23//! - [`f64_117`] draws a second word when the first has 12 or more leading
24//!   zeros (probability 2⁻¹²), extending completeness to [2⁻⁶⁵ . . 1)
25//!   with a 2⁻¹¹⁷ grid below;
26//! - [`f64_full`] additionally keeps drawing words while they are zero,
27//!   reaching every float in [0 . . 1), subnormals included.
28//!
29//! All variants consume one word per call with probability 1 − 2⁻¹² and, in
30//! the typical case, cost only a couple of operations more than
31//! [`division`] scaling.
32//!
33//! [Pekka Pulkkinen’s uniFloats wiki]: https://github.com/pekkizen/prng/wiki/uniFloats
34//! [`division`]: crate::division
35
36/// Returns a random `f64` distributed as a uniform 64-bit fixed-point real
37/// in [0 . . 1) rounded down to the nearest representable value: every float
38/// in [2⁻¹² . . 1), and the 2⁵² multiples of 2⁻⁶⁴ below 2⁻¹².
39#[inline]
40pub fn f64_64(mut bits: impl FnMut() -> u64) -> f64 {
41    let u = bits();
42    if u == 0 {
43        return 0.0;
44    }
45    let z = u.leading_zeros() as u64;
46    // The Go original defines z as the leading-zeros count plus one and
47    // computes `u << z` with z possibly 64, which Go defines as 0; Rust
48    // declares 64-bit shifts overflow, so here z is the plain leading-zeros
49    // count (≤ 63, as u ≠ 0) and the last bit of the shift, which drops the
50    // implicit leading one, is applied separately.
51    f64::from_bits((1022 - z) << 52 | ((u << z) << 1) >> 12)
52}
53
54/// Returns 2⁻ⁿ as an `f64`; `n` must be at most 1022.
55#[inline]
56const fn two_to_minus(n: u64) -> f64 {
57    debug_assert!(n <= 1022);
58    f64::from_bits((1023 - n) << 52)
59}
60
61/// Returns a random `f64` distributed as a uniform 117-bit fixed-point real
62/// in [0 . . 1) rounded down to the nearest representable value: every
63/// float in [2⁻⁶⁵ . . 1), and the multiples of 2⁻¹¹⁷ below 2⁻⁶⁵.
64///
65/// A port of `Float64_117`: as [`f64_64`], except that when the first word
66/// has 12 or more leading zeros (probability 2⁻¹²) a second word extends the
67/// fixed point to 117 bits.
68#[inline]
69pub fn f64_117(mut bits: impl FnMut() -> u64) -> f64 {
70    let u = bits();
71    let z = u.leading_zeros() as u64;
72    if z <= 11 {
73        // 99.975% of cases; see [`f64_64`] for the shift structure.
74        return f64::from_bits((1022 - z) << 52 | ((u << z) << 1) >> 12);
75    }
76    // Kluge; see [`crate::cold::cold_barrier`].
77    crate::cold::cold_barrier();
78    // The Go original computes `u << z` with z possibly 64 (first word 0),
79    // which Go defines as 0; `checked_shl` + `unwrap_or` mirrors that
80    // (as would `unbounded_shl`, which however requires Rust 1.87).
81    let u = u.checked_shl(z as u32).unwrap_or(0) | bits() >> (64 - z);
82    (u >> 11) as f64 * two_to_minus(53 + z)
83}
84
85/// Returns a random `f64` distributed as a uniform real in [0 . . 1)
86/// rounded down to the nearest representable value: every float in
87/// [0 . . 1) is reachable, subnormals and 0 included, with probability
88/// equal to the measure of the reals that round down to it.
89///
90/// A port of `Float64full`: as [`f64_117`], except that zero words keep the
91/// zoom going, 64 binades at a time, down to the bottom of the subnormals.
92#[inline]
93pub fn f64_full(mut bits: impl FnMut() -> u64) -> f64 {
94    let mut u = bits();
95    let mut z = u.leading_zeros() as u64;
96    if z <= 11 {
97        // 99.975% of cases; see [`f64_64`] for the shift structure.
98        return f64::from_bits((1022 - z) << 52 | ((u << z) << 1) >> 12);
99    }
100    // Kluge; see [`crate::cold::cold_barrier`].
101    crate::cold::cold_barrier();
102    let mut exp = 0u64;
103    while u == 0 {
104        u = bits();
105        z = u.leading_zeros() as u64;
106        exp += 64;
107        if exp + z >= 1074 {
108            return 0.0;
109        }
110    }
111    // The Go original computes `bits() >> (64 - z)` with z possibly 0 after
112    // the loop, which Go defines as 0; `checked_shr` + `unwrap_or` mirrors
113    // that (as would `unbounded_shr`, which however requires Rust 1.87).
114    let u = u << z | bits().checked_shr(64 - z as u32).unwrap_or(0);
115    exp += z;
116    if exp < 1022 {
117        return f64::from_bits((1022 - exp) << 52 | (u << 1) >> 12);
118    }
119    // The 2⁵² subnormal floats.
120    f64::from_bits(u >> (exp - 1022) >> 12)
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::sources::Weyl;
127
128    /// The bit-building form must agree with the wiki’s division form,
129    /// `float64(u << z >> 11) / 2^53 / 2^z` with z = leadingZeros(u).
130    #[test]
131    fn test_matches_reference_division_form() {
132        let division_form = |u: u64| {
133            let z = u.leading_zeros();
134            let m = ((u << z) >> 11) as f64;
135            m / (1u64 << 53) as f64 / (1u64 << z) as f64
136        };
137        let mut rng = Weyl(7);
138        for _ in 0..100_000 {
139            let u = rng.0.wrapping_add(0x9E3779B97F4A7C15);
140            assert_eq!(f64_64(|| rng.next_u64()), division_form(u));
141        }
142    }
143
144    #[test]
145    fn test_range() {
146        let mut rng = Weyl(42);
147        for _ in 0..100_000 {
148            let x = f64_64(|| rng.next_u64());
149            assert!((0.0..1.0).contains(&x), "f64_64: {x}");
150        }
151    }
152
153    /// Extremes of the leading-zeros count.
154    #[test]
155    fn test_edge_cases() {
156        assert_eq!(f64_64(|| 1 << 63), 0.5);
157        assert_eq!(f64_64(|| u64::MAX), f64::from_bits(1.0f64.to_bits() - 1));
158        assert_eq!(f64_64(|| 1), f64::from_bits((1023 - 64) << 52)); // 2^-64
159        assert_eq!(f64_64(|| 0), 0.0);
160    }
161    /// A source that replays a fixed sequence of words, then panics.
162    fn replay(words: &[u64]) -> impl FnMut() -> u64 + '_ {
163        let mut iter = words.iter();
164        move || *iter.next().expect("source exhausted")
165    }
166
167    /// Golden values produced by the verbatim Go code of the wiki, one per
168    /// branch (fast path, slow path, extreme leading-zero counts, zero
169    /// words).
170    #[test]
171    fn test_known_values_117() {
172        for (words, expected) in [
173            (&[0x8000000000000000, 0][..], 0x3fe0000000000000u64),
174            (&[0xDEADBEEFDEADBEEF, 0][..], 0x3febd5b7ddfbd5b7),
175            (&[1 << 52, 0][..], 0x3f30000000000000),
176            (&[1 << 51, 0xC0FFEE0DDF00D5ED][..], 0x3f20000000000001),
177            (&[1, 0xC0FFEE0DDF00D5ED][..], 0x3bfc0ffee0ddf00d),
178            (&[3, 0xFFFFFFFFFFFFFFFF][..], 0x3c0fffffffffffff),
179            (&[0, 0xC0FFEE0DDF00D5ED][..], 0x3be81ffdc1bbe01a),
180            (&[0, 0][..], 0),
181        ] {
182            assert_eq!(
183                f64_117(replay(words)).to_bits(),
184                expected,
185                "words {words:x?}"
186            );
187        }
188    }
189
190    /// Golden values produced by the verbatim Go code of the wiki; the deep
191    /// sequences exercise the zero-word zoom, the subnormal construction and
192    /// the all-zeros cutoff (which fires before the final exponent add).
193    #[test]
194    fn test_known_values_full() {
195        for (words, expected) in [
196            (vec![0x8000000000000000], 0x3fe0000000000000u64),
197            (vec![0xDEADBEEFDEADBEEF], 0x3febd5b7ddfbd5b7),
198            (vec![1 << 52], 0x3f30000000000000),
199            (vec![1 << 51, 0xC0FFEE0DDF00D5ED], 0x3f20000000000001),
200            (vec![1, 0xC0FFEE0DDF00D5ED], 0x3bfc0ffee0ddf00d),
201            (vec![3, 0xFFFFFFFFFFFFFFFF], 0x3c0fffffffffffff),
202            (vec![0, 0xC0FFEE0DDF00D5ED, 0xABCD], 0x3be81ffdc1bbe01a),
203            (vec![0, 0, 0xC0FFEE0DDF00D5ED, 0xAB], 0x37e81ffdc1bbe01a),
204            (
205                [vec![0; 15], vec![2, u64::MAX]].concat(),
206                0x000bffffffffffff,
207            ),
208            (
209                [vec![0; 15], vec![1, u64::MAX]].concat(),
210                0x0007ffffffffffff,
211            ),
212            (
213                [vec![0; 16], vec![1 << 50, 0xFFFFFFFFFFFFF]].concat(),
214                0x0000001000000000,
215            ),
216            ([vec![0; 16], vec![3, 0]].concat(), 0),
217            (vec![0; 18], 0),
218        ] {
219            assert_eq!(
220                f64_full(replay(&words)).to_bits(),
221                expected,
222                "words {words:x?}"
223            );
224        }
225    }
226
227    /// XOR-rotate hash over 10⁶ draws, cross-checked against the verbatim Go
228    /// code of the wiki driven by the same Weyl source. The two variants
229    /// agree on any zero-free source stream.
230    #[test]
231    fn test_matches_go_reference_hash() {
232        let mut rng = Weyl(42);
233        let mut h = 0u64;
234        for _ in 0..1_000_000 {
235            h = h.rotate_left(1) ^ f64_117(|| rng.next_u64()).to_bits();
236        }
237        assert_eq!(h, 0xf9b6db6017240be7);
238
239        let mut rng = Weyl(42);
240        let mut h = 0u64;
241        for _ in 0..1_000_000 {
242            h = h.rotate_left(1) ^ f64_full(|| rng.next_u64()).to_bits();
243        }
244        assert_eq!(h, 0xf9b6db6017240be7);
245    }
246
247    #[test]
248    fn test_range_117_full() {
249        let mut rng = Weyl(42);
250        for _ in 0..100_000 {
251            let x = f64_117(|| rng.next_u64());
252            assert!((0.0..1.0).contains(&x), "f64_117: {x}");
253            let x = f64_full(|| rng.next_u64());
254            assert!((0.0..1.0).contains(&x), "f64_full: {x}");
255        }
256    }
257}