Skip to content

Commit 7c8b1e5

Browse files
pitdickersicking
authored andcommitted
Implement HighPrecision01 distribution
1 parent a74cd6a commit 7c8b1e5

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed

benches/distributions.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ distr_float!(distr_open01_f32, f32, Open01);
109109
distr_float!(distr_open01_f64, f64, Open01);
110110
distr_float!(distr_openclosed01_f32, f32, OpenClosed01);
111111
distr_float!(distr_openclosed01_f64, f64, OpenClosed01);
112+
distr_float!(distr_high_precision_f32, f32, HighPrecision01);
113+
distr_float!(distr_high_precision_f64, f64, HighPrecision01);
112114

113115
// distributions
114116
distr_float!(distr_exp, f64, Exp::new(1.23 * 4.56));

src/distributions/float.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,35 @@ impl<F: HPFloatHelper> HighPrecision<F> {
9797
}
9898
}
9999

100+
/// Generate a floating point number in the half-open interval `[0, 1)` with a
101+
/// uniform distribution.
102+
///
103+
/// This is different from `Uniform` in that it uses all 32 bits of an RNG for a
104+
/// `f32`, instead of only 23, the number of bits that fit in a floats fraction
105+
/// (or 64 instead of 52 bits for a `f64`).
106+
///
107+
/// The smallest interval between values that can be generated is 2^-32
108+
/// (2.3283064e-10) for `f32`, and 2^-64 (5.421010862427522e-20) for `f64`.
109+
/// But this interval increases further away from zero because of limitations of
110+
/// the floating point format. Close to 1.0 the interval is 2^-24 (5.9604645e-8)
111+
/// for `f32`, and 2^-53 (1.1102230246251565) for `f64`. Compare this with
112+
/// `Uniform`, which has a fixed interval of 2^23 and 2^-52 respectively.
113+
///
114+
/// Note: in the future this may change change to request even more bits from
115+
/// the RNG if the value gets very close to 0.0, so it always has as many digits
116+
/// of precision as the float can represent.
117+
///
118+
/// # Example
119+
/// ```rust
120+
/// use rand::{NewRng, SmallRng, Rng};
121+
/// use rand::distributions::HighPrecision01;
122+
///
123+
/// let val: f32 = SmallRng::new().sample(HighPrecision01);
124+
/// println!("f32 from [0,1): {}", val);
125+
/// ```
126+
#[derive(Clone, Copy, Debug)]
127+
pub struct HighPrecision01;
128+
100129
pub(crate) trait IntoFloat {
101130
type F;
102131

@@ -203,6 +232,64 @@ float_impls! { f64x8, u64x8, f64, u64, 52, 1023 }
203232

204233
macro_rules! high_precision_float_impls {
205234
($ty:ty, $uty:ty, $ity:ty, $fraction_bits:expr, $exponent_bits:expr) => {
235+
impl Distribution<$ty> for HighPrecision01 {
236+
/// Generate a floating point number in the half-open interval
237+
/// `[0, 1)` with a uniform distribution.
238+
///
239+
/// This is different from `Uniform` in that it uses all 32 bits
240+
/// of an RNG for a `f32`, instead of only 23, the number of bits
241+
/// that fit in a floats fraction (or 64 instead of 52 bits for a
242+
/// `f64`).
243+
///
244+
/// # Example
245+
/// ```rust
246+
/// use rand::{NewRng, SmallRng, Rng};
247+
/// use rand::distributions::HighPrecision01;
248+
///
249+
/// let val: f32 = SmallRng::new().sample(HighPrecision01);
250+
/// println!("f32 from [0,1): {}", val);
251+
/// ```
252+
///
253+
/// # Algorithm
254+
/// (Note: this description used values that apply to `f32` to
255+
/// illustrate the algorithm).
256+
///
257+
/// The trick to generate a uniform distribution over [0,1) is to
258+
/// set the exponent to the -log2 of the remaining random bits. A
259+
/// simpler alternative to -log2 is to count the number of trailing
260+
/// zero's of the random bits.
261+
///
262+
/// Each exponent is responsible for a piece of the distribution
263+
/// between [0,1). The exponent -1 fills the part [0.5,1). -2 fills
264+
/// [0.25,0.5). The lowest exponent we can get is -10. So a problem
265+
/// with this method is that we can not fill the part between zero
266+
/// and the part from -10. The solution is to treat numbers with an
267+
/// exponent of -10 as if they have -9 as exponent, and substract
268+
/// 2^-9 (implemented in the `fallback` function).
269+
#[inline]
270+
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> $ty {
271+
#[inline(never)]
272+
fn fallback(fraction: $uty) -> $ty {
273+
let float_size = (mem::size_of::<$ty>() * 8) as i32;
274+
let min_exponent = $fraction_bits as i32 - float_size;
275+
let adjust = // 2^MIN_EXPONENT
276+
(0 as $uty).into_float_with_exponent(min_exponent);
277+
fraction.into_float_with_exponent(min_exponent) - adjust
278+
}
279+
280+
let fraction_mask = (1 << $fraction_bits) - 1;
281+
let value: $uty = rng.gen();
282+
283+
let fraction = value & fraction_mask;
284+
let remaining = value >> $fraction_bits;
285+
// If `remaing ==0` we end up in the lowest exponent, which
286+
// needs special treatment.
287+
if remaining == 0 { return fallback(fraction) }
288+
let exp = remaining.trailing_zeros() as i32 + 1;
289+
fraction.into_float_with_exponent(-exp)
290+
}
291+
}
292+
206293
impl HPFloatHelper for $ty {
207294
type SignedInt = $ity;
208295
type SignedIntDistribution = <$ity as SampleUniform>::Sampler;
@@ -630,4 +717,18 @@ mod tests {
630717
]);
631718
}
632719
}
720+
721+
#[test]
722+
fn high_precision_01_edge_cases() {
723+
// Test that the distribution is a half-open range over [0,1).
724+
// These constants happen to generate the lowest and highest floats in
725+
// the range.
726+
let mut zeros = StepRng::new(0, 0);
727+
assert_eq!(zeros.sample::<f32, _>(HighPrecision01), 0.0);
728+
assert_eq!(zeros.sample::<f64, _>(HighPrecision01), 0.0);
729+
730+
let mut ones = StepRng::new(0xffff_ffff_ffff_ffff, 0);
731+
assert_eq!(ones.sample::<f32, _>(HighPrecision01), 0.99999994);
732+
assert_eq!(ones.sample::<f64, _>(HighPrecision01), 0.9999999999999999);
733+
}
633734
}

src/distributions/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ use Rng;
175175

176176
#[doc(inline)] pub use self::other::Alphanumeric;
177177
#[doc(inline)] pub use self::uniform::Uniform;
178-
#[doc(inline)] pub use self::float::{OpenClosed01, Open01, HighPrecision};
178+
#[doc(inline)] pub use self::float::{OpenClosed01, Open01, HighPrecision, HighPrecision01};
179179
#[cfg(feature="alloc")]
180180
#[doc(inline)] pub use self::weighted::WeightedIndex;
181181
#[cfg(feature="std")]

0 commit comments

Comments
 (0)