Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak Open01 and Closed01 #237

Closed
wants to merge 3 commits into from
Closed
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/rand_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ macro_rules! float_impls {
mod $mod_name {
use {Rand, Rng, Open01, Closed01};

// 1.0 / epsilon
const SCALE: $ty = (1u64 << $mantissa_bits) as $ty;

impl Rand for $ty {
Expand All @@ -130,11 +131,10 @@ macro_rules! float_impls {
impl Rand for Open01<$ty> {
#[inline]
fn rand<R: Rng>(rng: &mut R) -> Open01<$ty> {
// add a small amount (specifically 2 bits below
// the precision of f64/f32 at 1.0), so that small
// numbers are larger than 0, but large numbers
// aren't pushed to/above 1.
Open01(rng.$method_name() + 0.25 / SCALE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was correct.
0.0 + 0.25 / SCALE will change the range to have 0.25 / SCALE as lower bound.
(1.0 - 1.0 / SCALE) + 0.25 / SCALE is still (1.0 - 1.0 / SCALE), thanks to the more limited precision near 1.0. At least, it works when the floating point rounding mode is set to nearest, and not up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little more subjective than for the other change (that's why I put it in a second commit). The change in the first commit (changing 24, 53 into 23, 52) is to make the code work as was intended. The change from 0.25 / SCALE (ɛ/4) into 0.5 / SCALE (ɛ/2) is to change the intended behavior. Adding ɛ/4 will change from the half-open range [0,1) into an open range (0,1), but it is not symmetric about 0.5, so it is biased. Also, the spacing between all the possible outputs is not equal any more, as I'll try to show below.

The value obtained from rng can have any of the possible value:

0, ɛ, 2ɛ, …, 0.5−ɛ, 0.5, 0.5+ɛ, …, 1−2ɛ, 1−ɛ.

Adding ɛ/4 (the intended current behavior) would give the following possible outputs:

0.25ɛ, 1.25ɛ, 2.25ɛ, …, 0.5−0.75ɛ, 0.5+0.25ɛ, 0.5+1.25ɛ, …, 1−1.75ɛ, 1−0.75ɛ,

but rounded. Rounding has the effect that 0.25ɛ is added to all values < 0.5, but nothing is added to values ≥ 0.5, as then the added bit does not fit in the precision and is rounded off, so the actual outputs are:

0.25ɛ, 1.25ɛ, 2.25ɛ, …, 0.5−0.75ɛ, 0.5, 0.5+ɛ, …, 1−2ɛ, 1−ɛ.

Adding ɛ/2 to the value from rng would give the following possible outputs instead:

0.5ɛ, 1.5ɛ, 2.5ɛ, …, 0.5−0.5ɛ, 0.5+0.5ɛ, 0.5+1.5ɛ, …, 1−1.5ɛ, 1−0.5ɛ.

All these values can be represented exactly, so no rounding takes place. I see two advantages to this change:

  1. The expected value (average) is now exactly 0.5, which is what I would expect from a random number in the open range (0,1).
  2. The gap between possible values is always equal to ɛ, unlike the current intended behavior where the gap is almost always equal to ɛ, but is 0.75ɛ in the middle (between 0.5−0.75ɛ and 0.5).

// add 0.5 * epsilon, so that smallest number is
// greater than 0, and largest number is still
// less than 1, specifically 1 - 0.5 * epsilon.
Open01(rng.$method_name() + 0.5 / SCALE)
}
}
impl Rand for Closed01<$ty> {
Expand All @@ -148,8 +148,8 @@ macro_rules! float_impls {
}
}
}
float_impls! { f64_rand_impls, f64, 53, next_f64 }
float_impls! { f32_rand_impls, f32, 24, next_f32 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Floats only have the number of mantissa bits as you corrected, and one implicit bit. But does the logic here not rely on that? Are you sure the SCALE calculated in the macro is correct now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment for Closed01 is "rescale so that 1.0 - epsilon becomes 1.0 precisely". The rescaling is done by multiplying by SCALE / (SCALE - 1.0). Now for SCALE / (SCALE - 1.0) to rescale 1.0 - EPSILON into 1.0, we need SCALE / (SCALE - 1.0) == 1.0 / (1.0 - EPSILON), which is true when SCALE == 1.0 / EPSILON. For f32 ɛ is 2−23, so 1/ɛ (SCALE) should be equal to 1<<23, not 1<<24. For f64 ɛ is 2−52, so 1/ɛ (SCALE) should be equal to 1<<52, not 1<<53.

float_impls! { f64_rand_impls, f64, 52, next_f64 }
float_impls! { f32_rand_impls, f32, 23, next_f32 }

impl Rand for char {
#[inline]
Expand Down