unisonweb / base

Unison base libraries

Home Page:https://share.unison-lang.org/@unison/base

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.

commented

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.

commented

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)
commented

OK, this implementation of natIn is now in main.