-
Notifications
You must be signed in to change notification settings - Fork 2
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
Clusivity, coverage and performance #113
Comments
ClusivityWith noninclusive intervals immediately arise problem of empty intervals: With floating point numbers same approach could be used: CoverageIs full coverage necessary? Even for floats minimal normal number is 1.17549435e-38. And if generating number takes 1ns it will be generated with probability ~1e-21/CPU·year. For doubles we're speaking about heat death of Universe googol times over. I think it's quite sensible to truncate coverage at some point. |
Yep, those seem like important points to consider.
I am not sure this holds in the strange world of floating point numbers. Here's my thinking: The current approach is to take the unit interval and translate it into the target interval, i.e. Now letting It is possible that I really hope I'm wrong, but to the best of my knowledge, that's how weird floating point numbers are. If this is correct, it makes changing clusivity of floating point ranges a pain.
In #105, I've run a few experiments to demonstrate that the coverage of simple algorithms is really poor, e.g. the one we currently use only hits 8e6 out of 1e9 representable floats in the unit interval. Depending on how the randomnes-consuming code is set up, it may not take all that long to see that it's always getting the same 8e6 numbers. |
Excellent question! I think it should work as long
Operative word is full coverage. Question is how much coverage we need. Number of reachable numbers is poor metric I think. Most of representable numbers are in small neighborhood of 0 and won't be sampled in any reasonable time frame anyway. One possible metric is to test how likely that we're able to notice sampling bias. if we take constant exponent method then whole [0,1] interval has incomplete coverage! If we take mwc-random's method. We have 2^32 uniformly distributed values. We have 9 extra bit, so only 2^{-9} ≈ 2e-3 of unit interval is undersampled |
@christoph-conrads, please give your thoughts on this whole discussion. As for this topic, I am also aware of the following:
|
So it seems that What I suggest is to add proper input validation and error handling for the
I expect this will add slight overhead to performance, but at least it will be correct. |
Yes, throwing exception is probably inevitable when IEEE754 is concerned. If mentioned numbers are not enough here is another gem: Another question is whether intervals |
One of course should think less and throw such problems at quickcheck more. Even easiest test ( prop1 :: Float -> Float -> Property
prop1 (abs -> a0) (abs -> b0)
= id
$ counterexample ("a = " ++ show a)
$ counterexample ("b = " ++ show b)
$ counterexample ("b' = " ++ show (a + (b - a)))
$ a + (b - a) == b
where
a = min a0 b0
b = max a0 b0 And here is failure where result may go out of the interval:
I think it's time to read something about linear interpolation and |
We get a bit of a chicken and egg problem with this approach, since QuickCheck uses
In general, though, randomized testing is a great tool and with all the work we are doing here we'll make it even better ;) |
@Shimuuar wrote:
That's exactly right - hence the proposal to do away with the constant exponent method (
I'm not sure what you mean by "we have 2^32 uniformly distributed values". Out of the 2^30.0 (1065353218) representable Floats in the unit interval, mwc-random's |
@lehins wrote:
Do you have a suggestion for how failure could be handled in the pure API for uniformR :: (RandomGen g, UniformRange a) => g -> (a, a) -> (a, g) |
mwc-random's methods generates number of the form: n/2^32 with 0 =< n < 2^32. Those are distributed uniformly. Catch is many of them map to single Float
Why? When it comes to testing hypothesis QC could use specially trained coin flipping hamsters and it will work. Point is QC is very good at demolishing unfounded assumptions |
If you insist on this approach, you do not need to discuss intricacies of interval boundaries because the rounding error makes the discussion moot.
Floating-point numbers were designed to
This behavior is in no way helpful when computing random numbers. Instead for random number generation, its best to think of FP numbers as integer pairs on the real line with special spacing. An FP number is composed of a sign bit, an exponent The figure below shows the ranges that are covered by FP numbers with different exponents; covered means that a real value from this range is best approximated by a float with the exponent shown. Zero is on the left-hand side, positive infinity is on the right-hand side, and the vertical bar denotes the start of an interval where the best FP approximation of a real value has exponent All real numbers in
Let's say you picked an exponent
What this means for random number generation:
Let's write FP numbers as the combination of sign, exponent, and significand they are. Let's draw random numbers in the half-open interval Let's say
[Note that Based on the triple composed of sign, exponent, significand you can start deriving the rules for random number generation in arbitrary intervals. |
@curiousleo Very simple, it also becomes monadic with uniformR :: (RandomGen g, UniformRange a, MonadThrow m) => g -> (a, a) -> m (a, g)
uniformR g r = runGenStateT g (uniformRM r) In case of I created a draft PR that shows how |
In the interest of keeping the discussion in one place, I'm responding to #102 (comment) here. Your explanations are very much appreciated @christoph-conrads!
Just to be really clear, does "as if it had randomly drawn and rounded real values" here mean "as if it had drawn real values and rounded towards zero"? With this interpretation, I have a few follow-up questions. My understanding is that if However, if In other words, if I also wonder if this relates to your point about symmetry:
|
@Shimuuar wrote:
mwc-random uses multiplication, not division. In real-valued arithmetic, this would give the same result. In floating-point arithmetic, it does not. In #105 there is a comparison of the images of mwc-random's Excerpt:
See #105 for the details.
lol
👍 |
Yes.
I remembered incorrectly. RFPL returns values in
Most random number generation APIs promise to return random numbers in the half-open interval The Rademacher FPL is stuck between a hard place and a rock because it draws random numbers from
NB: Users should pass closed intervals |
@christoph-conrads wrote:
Just to be clear: is this meant as advice to someone in the position of designing an API for generating random floating point numbers in a range, i.e. "if you have the freedom to decide what to return, make it a closed interval"? If so, I would be interested in your thoughts on why that makes for a better API than the alternatives. |
Yes, this is meant as an API advice. Downey's algorithm computes random values in closed intervals, my algorithm computes random values in half-open intervals with the closed boundary always having the smaller modulus (
There are situations where you really want half-open intervals though, even on a computer. This weekend I needed random complex numbers in polar representation. The angle must be in |
Hey all, this is my attempt at bringing the discussions about clusivity and full-coverage floating point generation together. DefinitionsA few definitions to make sure we're on the same page:
OptionsWhat are our options regarding floating point generation? The main ones are:
One alternative I'm leaving out here is the DiscussionThe guarantees we can give connect the floating point generation discussion with the clusivity discussion. The translation method gives no strong guarantees (concrete examples here):
Depending on the use case, this is not necessarily a show-stopper. For most values, the translation method generates a value within the given bounds. But the honest semantics of the translation method is not "inclusive" or "exclusive", it is "whatever the result of The clusivity API is a way to communicate specific guarantees, i.e. which numbers can be generated and which cannot. As shown above, with the translation method, we don't really know. In the vast majority of cases, the method will work as expected, but not always. As such, I think that a clusivity API only makes sense if we can give guarantees, i.e. if we use a full-coverage method. This leaves the question whether we should implement a full-coverage method.
An assumption here is that we expose either the translation method or a full-coverage method, but cannot easily do both. ProposalAs a result of these thoughts, here is my proposal:
In addition, I intend to turn my prototype into a library that allows users to generate floating point numbers with full coverage. While this is the less interesting path for |
I want to point out that I am specifying pseudocode for a method (
|
@curiousleo I support you suggestion. Just to reiterate:
I believe, it should be a fine approach as long as we document this behavior, especially for the first iteration at getting new With regards to:
Instead of a separate library, it might be better to add this functionality to the next version of |
@lehins yes, thanks for the clarifications. The bounds for integral and floating point numbers should be as you said.
My main goal was to clarify the scope for v1.2. My prototype implementation of the full coverage algorithm is in fact in a branch of this repo - the idea of putting it into a separate library was really just about separating that work from our goal of getting v1.2 out there. A future release of |
Great resource, thanks for pointing it out @peteroupc! |
Recent discussions about pseudo-random floating point numbers have revealed potential shortcomings of the current API in two aspects.
Clusivity describes whether a range is inclusive or exclusive in its bounds.
Coverage describes how many representable values in a range can be reached.
Integral types
For integral types, coverage is not a problem: all
Uniform
andUniformRange
instances for integral types we currently have generate all representable values in the relevant ranges.Clusivity is also simpler for integral types. It is straightforward to implement express exclusive ranges in terms of inclusive ranges and vice versa. Concretely,
(a, b) == [a+1, b-1]
, for example.Non-integral types
For non-integral types, full coverage for arbitrary ranges is difficult. The only project I know of that achieves this is rademacher-fpl.
We can improve coverage by generating a floating point number in the unit interval with full coverage and then translating it into the target range. This is the approach taken in #102, more details in #105.
The performance overhead to improve coverage in this way is small but non-zero, see #102.
Clusivity is also more complicated for non-integral types.
We have
[a, b) == (a, b] + x
and(a, b] == [a, b) - x
for somex
. To go from an inclusive-inclusive range to any other range, I believe that rejection sampling is necessary.In addition, I know of no simple method (without full coverage) that generates an inclusive-inclusive range.
To summarise which methods generate which clusivity:
As a result, clusivity and coverage appear to be linked for non-integral types.
Proposals and questions
Q: Do users benefit from being able to control coverage? Reduced coverage in itself is never useful, but some users may want the performance boost. See #102 for benchmark results.
#104 as it currently stands exposes clusivity in the API.
#104 (comment) (snippet no. 2) suggests a more general way to parameterise ranges. AFAICT this could be used to expose different coverages too, as well as more general "intervals" like open / closed complex disk.
The text was updated successfully, but these errors were encountered: