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

Closed01: distribution that generates floats in the range [0, 1] #1361

Open
abonander opened this issue Dec 6, 2023 · 4 comments
Open

Closed01: distribution that generates floats in the range [0, 1] #1361

abonander opened this issue Dec 6, 2023 · 4 comments

Comments

@abonander
Copy link

Background

What is your motivation?

My current work project involves a networking and utility library that is primarily targeted at Unity developers, with a core written in Rust.

Without going into details currently under NDA, we're trying to provide a higher quality/more featureful substitute for Unity's Random class, which has the idiosyncrasy that it generates floats in the closed range [0, 1]: https://docs.unity3d.com/ScriptReference/Random-value.html

Of course, it's relatively simple to implement this with Uniform::new_inclusive(0f64, 1f64), but its constructor does non-trivial work which means we either need to keep an instance cached somewhere (which is what we're currently doing) or pay the cost every time we want to generate a float.

What type of application is this? (E.g. cryptography, game, numerical simulation)

Games.

Feature request

Provide a new distribution, Closed01, similar to Open01 and OpenClosed01, that generates f32s and f64s in the range [0, 1].

This would complete the set of distributions covering this range:

  • Standard: sampling in [0, 1)
  • Open01: sampling in (0, 1)
  • OpenClosed01: sampling in (0, 1]
  • (proposed) Closed01, sampling in [0, 1]
@josephlr
Copy link
Member

josephlr commented Dec 8, 2023

One thing to consider here is that a "fully accurate" implementation of sampling on [0,1] essentially will never select 0, so in practice OpenClosed01 should be OK. See #1346 for a longer discussion.

For example, if we select a real number x uniformly at random from the interval [0,1], the probability x <= f64::MIN_POSITIVE is 2^(-1022), so it should literally never happen.

In my opinion, there should probably only be two distributions for selecting floats between 0 and 1, based on the following two (idealized) processes:

  1. Select a real number uniformly at random from [0,1], then round to the nearest floating point number. This is what's described in Higher quality (0, 1] floats #1346 and would sometimes select 1 and ~never select 0, so it would work like OpenClosed01.
  2. Select a real number uniformly at random from [0,1], then round down to a floating point number. This would never select 1 and ~never select 0, so it would work like Open01.

Standard would then be an alias for one of these distributions.

@abonander
Copy link
Author

Hmm, the documentation for Unity's Random.value states:

Any given float value between 0.0 and 1.0, including both 0.0 and 1.0, will appear on average approximately once every ten million random samples.

It sounds like they're making a significant sacrifice in precision in order to guarantee full coverage of the range.

That may make sense for an implementation specialized for games but I can appreciate why it wouldn't necessarily be useful for a general-purpose library.

@josephlr
Copy link
Member

josephlr commented Dec 8, 2023

Hmm, the documentation for Unity's Random.value states:

Any given float value between 0.0 and 1.0, including both 0.0 and 1.0, will appear on average approximately once every ten million random samples.

It sounds like they're making a significant sacrifice in precision in order to guarantee full coverage of the range.

Unity's documentation is definitely wrong here, as:

  1. The floats between 0 and 1 are not evenly spaced
  2. There are more than 10 million f32 values between 0 and 1

EDIT: My guess is that they are generating a random float in the range [1,2] (of which there are 2^23 + 1 = 8.4 Million of) and then subtracting 1. This does not hit every value in [0,1], but isn't the worst algorithm in the world. We do something similar in the Uniform implementation.

@abonander
Copy link
Author

abonander commented Dec 8, 2023

Okay, I'm actually starting to grok the problem now.

For one, I for some reason thought that Random.value returned a double (f64) which is why I thought it was sacrificing precision, but float/f32 makes more sense for game engines.

Anyway, I think any non-expert would see "uniformly distributed between 0 and 1 (inclusive)" and interpret that as "just as likely to be in the range [0.9, 1.0] as [0, 0.1] (or anywhere in between)".

The fact that the actual set of numbers is not evenly spaced is an idiosyncrasy of floating-point representation that I think most people won't really care or want to think about.

IMO, a specialized version of Uniform::new_inclusive(0.0, 1.0) with 23/52 bits of precision is perfectly fine for most intents and purposes. If someone needs more precision than that, well, they probably shouldn't be using floating-point in the first place.

FWIW, we ended up deciding not to implement this exact behavior at the Rust level anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants