Interesting approach!
I've done some quick testing and think I've found an issue: For some closed ranges it will never return the value of the upper bound.
And, of course (because of how the implementation works), this means that the corresponding open range has the corresponding issue.
It will behave as expected for eg Float(0.03125) ... Float(0.0625)
, but for eg
Float(0) ... Float(0.0625)
it will never return 0.0625
, even though that value (being a binade) should be twice as common as the values preceding it.
Here's a demonstration program:
func test() {
var prng = WyRand()
let range = Float(0.0) ... Float(0.0625)
print("range.lowerBound.bitPattern:", range.lowerBound.bitPattern)
print("range.upperBound.bitPattern:", range.upperBound.bitPattern)
var hist = [UInt32 : UInt64]()
var minValue = Float.infinity
var maxValue = -Float.infinity
for _ in 0 ..< 1024*1024*512 {
let f = Float.uniformRandom(in: range, using: &prng)
precondition(f >= range.lowerBound)
precondition(f <= range.upperBound)
minValue = min(minValue, f)
maxValue = max(maxValue, f)
// Store only the greatest 8 possible values in the histogram:
if f.bitPattern > range.upperBound.bitPattern - 8 {
hist[f.bitPattern, default: 0] &+= 1
}
}
print("minValue:", minValue)
print("maxValue:", maxValue)
for key in hist.keys.sorted() {
let v = hist[key]!
print("bin for bit pattern \(key) has \(v) samples")
}
}
test()
It will (after some time) print something like:
range.lowerBound.bitPattern: 0
range.upperBound.bitPattern: 1031798784
minValue: 6.0394516e-11
maxValue: 0.062499996
bin for bit pattern 1031798777 has 38 samples
bin for bit pattern 1031798778 has 33 samples
bin for bit pattern 1031798779 has 41 samples
bin for bit pattern 1031798780 has 30 samples
bin for bit pattern 1031798781 has 29 samples
bin for bit pattern 1031798782 has 27 samples
bin for bit pattern 1031798783 has 31 samples
Note that no samples at all ended up in the bin for Float(0.0625).bitPattern == 1031798784
, although there should be on average 64.
(In this test 1024*1024*512
values are returned, so on average the number 0.0625
should be returned:
(Float(0.0625).ulp*1024*1024*512)/0.0625 == 64
times.
and the values preceding it should be returned:
(Float(0.0625).nextDown.ulp*1024*1024*512)/Float(0.0625).nextDown == 32
times.)
I've run this program many times, and used the system generator too, and the value 0.0625
has never been returned.
And I first noticed this issue while using my Float8
type for the testing. Using that type I can perform tests like this much faster and more exhaustively, although I don't fully trust it yet. But as demonstrated here, the issue seems to exist with Float
too.
The test uses
the WyRand generator
/// The wyrand generator, translated from:
/// https://github.com/wangyi-fudan/wyhash
struct WyRand : RandomNumberGenerator {
typealias State = UInt64
private (set) var state: State
init(seed: UInt64? = nil) {
self.state = seed ?? UInt64.random(in: .min ... .max)
}
init(state: State) {
// Every UInt64 value is a valid WeRand state, so no need for this
// initializer to be failable, it will satisfy the requirement anyway.
self.state = state
}
mutating func next() -> UInt64 {
state &+= 0xa0761d6478bd642f
let mul = state.multipliedFullWidth(by: state ^ 0xe7037ed1a0b428db)
return mul.high ^ mul.low
}
}
because we'd have to wait many times longer for the system generator.