Skip to content

Commit 9aea640

Browse files
pitdickersicking
authored andcommitted
Implement HighPrecision01 distribution
1 parent acecef8 commit 9aea640

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed

benches/distributions.rs

+2
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

+101
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

@@ -201,6 +230,64 @@ float_impls! { f64x8, u64x8, f64, u64, 52, 1023 }
201230

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

src/distributions/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ use Rng;
172172

173173
#[doc(inline)] pub use self::other::Alphanumeric;
174174
#[doc(inline)] pub use self::uniform::Uniform;
175-
#[doc(inline)] pub use self::float::{OpenClosed01, Open01, HighPrecision};
175+
#[doc(inline)] pub use self::float::{OpenClosed01, Open01, HighPrecision, HighPrecision01};
176176
#[cfg(feature="std")]
177177
#[doc(inline)] pub use self::gamma::{Gamma, ChiSquared, FisherF, StudentT};
178178
#[cfg(feature="std")]

0 commit comments

Comments
 (0)