-
-
Notifications
You must be signed in to change notification settings - Fork 455
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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) | ||
// 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> { | ||
|
@@ -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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment for |
||
float_impls! { f64_rand_impls, f64, 52, next_f64 } | ||
float_impls! { f32_rand_impls, f32, 23, next_f32 } | ||
|
||
impl Rand for char { | ||
#[inline] | ||
|
@@ -269,9 +269,38 @@ mod tests { | |
|
||
#[test] | ||
fn floating_point_edge_cases() { | ||
// the test for exact equality is correct here. | ||
assert!(ConstantRng(0xffff_ffff).gen::<f32>() != 1.0); | ||
assert!(ConstantRng(0xffff_ffff_ffff_ffff).gen::<f64>() != 1.0); | ||
const EPSILON32: f32 = 1.0 / (1u32 << 23) as f32; | ||
const EPSILON64: f64 = 1.0 / (1u64 << 52) as f64; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use ε is defined as the difference between 1.0 and the next largest representable number; since all numbers output (excepting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I first used |
||
|
||
let mut zeros = ConstantRng(0); | ||
let mut ones = ConstantRng(!0); | ||
|
||
let zero32 = zeros.gen::<f32>(); | ||
let zero64 = zeros.gen::<f64>(); | ||
let one32 = ones.gen::<f32>(); | ||
let one64 = ones.gen::<f64>(); | ||
assert_eq!(zero32, 0.0); | ||
assert_eq!(zero64, 0.0); | ||
assert!(1.0 - EPSILON32 <= one32 && one32 < 1.0); | ||
assert!(1.0 - EPSILON64 <= one64 && one64 < 1.0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As noted above, I believe this should be exactly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, |
||
|
||
let Closed01(closed_zero32) = zeros.gen::<Closed01<f32>>(); | ||
let Closed01(closed_zero64) = zeros.gen::<Closed01<f64>>(); | ||
let Closed01(closed_one32) = ones.gen::<Closed01<f32>>(); | ||
let Closed01(closed_one64) = ones.gen::<Closed01<f64>>(); | ||
assert_eq!(closed_zero32, 0.0); | ||
assert_eq!(closed_zero64, 0.0); | ||
assert_eq!(closed_one32, 1.0); | ||
assert_eq!(closed_one64, 1.0); | ||
|
||
let Open01(open_zero32) = zeros.gen::<Open01<f32>>(); | ||
let Open01(open_zero64) = zeros.gen::<Open01<f64>>(); | ||
let Open01(open_one32) = ones.gen::<Open01<f32>>(); | ||
let Open01(open_one64) = ones.gen::<Open01<f64>>(); | ||
assert!(0.0 < open_zero32 && open_zero32 <= EPSILON32); | ||
assert!(0.0 < open_zero64 && open_zero64 <= EPSILON64); | ||
assert!(1.0 - EPSILON32 <= open_one32 && open_one32 < 1.0); | ||
assert!(1.0 - EPSILON64 <= open_one64 && open_one64 < 1.0); | ||
} | ||
|
||
#[test] | ||
|
There was a problem hiding this comment.
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 have0.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 near1.0
. At least, it works when the floating point rounding mode is set to nearest, and not up.There was a problem hiding this comment.
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) into0.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: