Random Number Generation with Seed

Hello, I am trying to generate random numbers with a seed so that I can have deterministic sequence for reproducibility. I am using the technique provided here: Random Numbers In Swift [+Generate Random Number, Set A Seed]

but it does not seem to work. This is my code:

class MyRandomNumberGenerator: RandomNumberGenerator {
// Random Numbers In Swift [+Generate Random Number, Set A Seed]
init(_ seed: Int) {
// Set the random seed
srand48(seed)
}

func next() -> UInt64 {
    // drand48() returns a Double, transform to UInt64
    return withUnsafeBytes(of: drand48()) { bytes in
        bytes.load(as: UInt64.self)
    }
}

}

class Random {

var rnd: MyRandomNumberGenerator

init(_ seed: Int) {
    // Set the random seed
    rnd = MyRandomNumberGenerator(seed)
}    

func nextFloat() -> Float {
    return Float.random(in: 0..<1, using: &rnd)
}

func nextDouble() -> Double {
    return Double.random(in: 0..<1, using: &rnd)
}

func next(_ x: UInt) -> UInt {
    return UInt.random(in: 0..<x, using: &rnd)
}

func nextInt() -> Int {
    return Int.random(in: Int.min..<Int.max, using: &rnd)
}

}

When I run it like this:

let seed = Int.random(in: Int.min..<Int.max)
let random = Random(seed)
for i in 0..<10 {
print(random.next(4))
}

I just get a sequence of 0s. Can anyone tell me what is wrong here and how to fix it? Thanks.

This is not a valid conformance to RandomNumberGenerator, because it does not produce a uniform random value in 0...UInt64.max. This is pretty clear if we print a few values in hex in the REPL:

1> for _ in 0 ..< 10 {
2.   print(String(next(), radix: 16))
3. }
3fd465765ea34540
3fec5d9f295dff80
3f8fe9e6a4b2b800
3fe2b0ddf945a2c0
3fc46630ef7b3480
3fd88ecd07503000
3fe61cb534223960
3fae22c169eaf400
3feccb9b42041fa0
3fc4ef12deace200

Because this is not a valid conformance, algorithms that transform its output do not behave as expected. In particular, because the value produced by next() is always less than 0x40..., generating random integers with an upper bound of 4 will always produce zero.

If you really want to build a conforming RNG out of drand48() specifically, you can do that as follows:

next() -> UInt64 {
  UInt64(drand48() * 0x1.0p64) ^ UInt64(drand48() * 0x1.0p16) 
} 

conducing the very simple test above then yields:

e43907dd08417f89
b5c43c88d3634952
dd6ba1ae7855acf0
755f99509397f5aa
c651209e55a96065
3a8817d7a30b5ac2
4ce1a81e343dab75
b80e317bb5bf90e2
d31028a4e81163ff
d19aab41bfb3d810

(Why 0x1.0p64 and 0x1.0p16? Because drand48() produces 48 bits scaled to fit in (0,1); multiplying by 2⁶⁴ before conversion gives us a random value filling the high-order 48 bits of the UInt64, multiplying by 2¹⁶ fills the low-order 16 bits (and lets the next 32 bits be chopped off, since conversion to UInt64 throws away any fractional bits).

7 Likes

thanks a lot!