Is Random.lcg okay?
ceedubs opened this issue · comments
When I run the following code I always get exactly 250 1s, 250 2s, 250 3s, and 250 4s, even if I change the seed. I don't see the same for splitmix. Is there a bug with the lcg implementation in base or is this expected from lcg?
> lcg 71327 do
withInitialValue Map.empty do
Each.run do
repeat 1000
n = Random.natIn 1 5
Store.modify (Map.putWith (Nat.+) n 1)
Store.get |> Map.toList
I looked into this a bit and the implementation looks correct to me. I thought that maybe we were hitting something related to using unsigned nats in an algorithm intended for signed integers, but as far as I can tell that's not the case.
I think this is just an artifact of how our implementation of LCG interacts with natIn
.
Specifically, our LCG uses a modulus of 2^64. And NatIn 1 5
will use a modulus of 4, so it's only considering the 2 lowest bits of the generator. Basically if the LCG is at all periodic in the 2 lowest order bits, I'd expect this behavior. SplitMix is going to be more appropriate for this use case.
Here's the implementation of natIn
:
abilities.Random.natIn : Nat -> Nat ->{Random} Nat
abilities.Random.natIn start stopExclusive =
if Universal.lt start stopExclusive then
use Nat + -
Nat.mod !Random.nat (stopExclusive - start) + start
else bug ("Random.natIn start must be < stop", start, stopExclusive)
We could potentially get better results with the following re-implementation:
abilities.Random.natIn : Nat -> Nat -> {Random} Nat
abilities.Random.natIn start stopExclusive =
if Universal.lt start stopExclusive then
use Float / * + toNat
use Nat toFloat
m = 18446744073709551616.0 -- 2^64
max = toFloat stopExclusive
min = toFloat start
lcgOutput = toFloat !Random.nat
scaled = ((lcgOutput / m) * (max Float.- min)) + min
match toNat scaled with
Some natValue -> natValue
None -> bug "Number too big or too small for Nat"
else bug ("Random.natIn start must be < stop", start, stopExclusive)
OK, this implementation of natIn
is now in main
.