|
| 1 | +/- |
| 2 | +Copyright (c) 2026 Gaëtan Serré. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Authors: Gaëtan Serré |
| 5 | +-/ |
| 6 | +module |
| 7 | + |
| 8 | +public import Mathlib.Control.Random |
| 9 | +public import RandomDo.NumLean.SeedSequence |
| 10 | + |
| 11 | +/-! |
| 12 | +# The PCG-64 pseudo-random number generator |
| 13 | +
|
| 14 | +This file provides `PCG64`, a drop-in replacement for `StdGen` implementing the |
| 15 | +`PCG-XSL-RR 128/64` variant of the permuted congruential generator family of O'Neill, i.e. the |
| 16 | +generator known as `pcg64` in the reference C implementation and as `numpy.random.PCG64` in numpy. |
| 17 | +
|
| 18 | +The generator is a linear congruential generator on 128 bits, |
| 19 | +`state ← state * multiplier + increment`, whose state is scrambled down to 64 bits by the |
| 20 | +`XSL-RR` output permutation (xor the two halves together, then rotate by the top 6 bits of the |
| 21 | +state). The 128-bit arithmetic is emulated with pairs of `UInt64`, so every operation compiles to |
| 22 | +native machine arithmetic. |
| 23 | +
|
| 24 | +## Main definitions |
| 25 | +
|
| 26 | +* `PCG64`: the generator state, and its `RandomGen` instance |
| 27 | +* `PCG64.seedWords` / `PCG64.seed` / `mkPCG64`: seeding, following the reference |
| 28 | + `pcg_setseq_128_srandom_r` |
| 29 | +
|
| 30 | +## References |
| 31 | +
|
| 32 | +* M. E. O'Neill, *PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for |
| 33 | + Random Number Generation*, 2014. <https://www.pcg-random.org/paper.html> |
| 34 | +* The reference implementation: <https://github.com/imneme/pcg-c> |
| 35 | +* numpy's vendored copy: `numpy/random/src/pcg64/pcg64.h` |
| 36 | +-/ |
| 37 | + |
| 38 | +@[expose] public section |
| 39 | + |
| 40 | +namespace NumLean |
| 41 | + |
| 42 | +/-- The state of a PCG-64 generator: a 128-bit LCG state and a 128-bit increment (which selects the |
| 43 | +stream and must be odd), each stored as a pair of 64-bit words. -/ |
| 44 | +structure PCG64 where |
| 45 | + /-- High 64 bits of the LCG state. -/ |
| 46 | + stateHi : UInt64 |
| 47 | + /-- Low 64 bits of the LCG state. -/ |
| 48 | + stateLo : UInt64 |
| 49 | + /-- High 64 bits of the increment. -/ |
| 50 | + incHi : UInt64 |
| 51 | + /-- Low 64 bits of the increment; it is odd for any generator built through the API below. -/ |
| 52 | + incLo : UInt64 |
| 53 | + deriving Repr, DecidableEq |
| 54 | + |
| 55 | +namespace PCG64 |
| 56 | + |
| 57 | +/-- High 64 bits of the PCG-64 multiplier `0x2360ED051FC65DA44385DF649FCCF645`. -/ |
| 58 | +def multHi : UInt64 := 0x2360ED051FC65DA4 |
| 59 | + |
| 60 | +/-- Low 64 bits of the PCG-64 multiplier `0x2360ED051FC65DA44385DF649FCCF645`. -/ |
| 61 | +def multLo : UInt64 := 0x4385DF649FCCF645 |
| 62 | + |
| 63 | +/-- The default stream, i.e. the reference default increment `0x5851F42D4C957F2D14057B7EF767814F` |
| 64 | +divided by two, since `seed` turns a stream `s` into the increment `2 * s + 1`. -/ |
| 65 | +def defaultStream : Nat := 0x2C28FA16A64ABF968A02BDBF7BB3C0A7 |
| 66 | + |
| 67 | +/-- The high 64 bits of the 128-bit product `a * b`, obtained from the four 32-bit limb products. -/ |
| 68 | +@[inline] def mulHi (a b : UInt64) : UInt64 := |
| 69 | + let mask : UInt64 := 0xFFFFFFFF |
| 70 | + let a₀ := a &&& mask |
| 71 | + let a₁ := a >>> 32 |
| 72 | + let b₀ := b &&& mask |
| 73 | + let b₁ := b >>> 32 |
| 74 | + let t := a₁ * b₀ + (a₀ * b₀) >>> 32 |
| 75 | + a₁ * b₁ + t >>> 32 + (a₀ * b₁ + (t &&& mask)) >>> 32 |
| 76 | + |
| 77 | +/-- Rotate the 64-bit word `x` right by `r` bits. Only the low 6 bits of `r` are used. -/ |
| 78 | +@[inline] def rotr (x r : UInt64) : UInt64 := (x >>> r) ||| (x <<< (64 - r)) |
| 79 | + |
| 80 | +/-- One step of the underlying 128-bit LCG, `state ← state * multiplier + increment`. -/ |
| 81 | +@[inline] def step (g : PCG64) : PCG64 := |
| 82 | + let lo := g.stateLo * multLo |
| 83 | + let hi := mulHi g.stateLo multLo + g.stateLo * multHi + g.stateHi * multLo |
| 84 | + let lo' := lo + g.incLo |
| 85 | + -- unsigned addition wraps, so `lo' < lo` exactly when the low half carried |
| 86 | + let carry : UInt64 := if lo' < lo then 1 else 0 |
| 87 | + { g with stateHi := hi + g.incHi + carry, stateLo := lo' } |
| 88 | + |
| 89 | +/-- The `XSL-RR` output permutation: fold the 128-bit state onto 64 bits by xoring its two halves, |
| 90 | +then rotate the result right by the 6 most significant bits of the state. -/ |
| 91 | +@[inline] def output (g : PCG64) : UInt64 := |
| 92 | + rotr (g.stateHi ^^^ g.stateLo) (g.stateHi >>> 58) |
| 93 | + |
| 94 | +/-- The next 64-bit output, together with the advanced generator. As in the reference |
| 95 | +implementation, the state is stepped before the output permutation is applied. -/ |
| 96 | +@[inline] def nextUInt64 (g : PCG64) : UInt64 × PCG64 := |
| 97 | + let g := g.step |
| 98 | + (g.output, g) |
| 99 | + |
| 100 | +/-- Seed a generator from four 64-bit words, following the reference |
| 101 | +`pcg_setseq_128_srandom_r`: `stateHi:stateLo` is the initial state and `seqHi:seqLo` selects the |
| 102 | +stream, whose increment is `2 * seq + 1`. -/ |
| 103 | +def seedWords (stateHi stateLo seqHi seqLo : UInt64) : PCG64 := |
| 104 | + let g : PCG64 := |
| 105 | + { stateHi := 0, stateLo := 0, |
| 106 | + incHi := (seqHi <<< 1) ||| (seqLo >>> 63), incLo := (seqLo <<< 1) ||| 1 } |
| 107 | + let g := g.step |
| 108 | + let lo := g.stateLo + stateLo |
| 109 | + let carry : UInt64 := if lo < g.stateLo then 1 else 0 |
| 110 | + step { g with stateHi := g.stateHi + stateHi + carry, stateLo := lo } |
| 111 | + |
| 112 | +/-- Seed a generator from a `SeedSequence`, as numpy's `PCG64` constructor does: the first two |
| 113 | +words drawn from the mixer give the initial state and the next two the stream. -/ |
| 114 | +def ofSeedSequence (s : SeedSequence) : PCG64 := |
| 115 | + let w := s.generateState 4 |
| 116 | + seedWords w[0]! w[1]! w[2]! w[3]! |
| 117 | + |
| 118 | +/-- Seed a generator from an initial state. -/ |
| 119 | +def seed (n : Nat) : PCG64 := ofSeedSequence (SeedSequence.ofNat n) |
| 120 | + |
| 121 | +/-- The `SeedSequence` whose entropy is `n` outputs taken from `g`, split into 32-bit words, the |
| 122 | +advanced generator being returned alongside. Used to derive further generators from an existing |
| 123 | +one. Two outputs are enough by default: they fill the mixing pool exactly, and no amount of extra |
| 124 | +entropy would make it carry more than its `SeedSequence.poolSize` words. -/ |
| 125 | +def toSeedSequence (g : PCG64) (n : Nat := 2) : SeedSequence × PCG64 := |
| 126 | + let (words, g) := Id.run do |
| 127 | + let mut g := g |
| 128 | + let mut words := Array.emptyWithCapacity (2 * n) |
| 129 | + for _ in List.range n do |
| 130 | + let (x, g') := g.nextUInt64 |
| 131 | + g := g' |
| 132 | + words := (words.push x.toUInt32).push (x >>> 32).toUInt32 |
| 133 | + return (words, g) |
| 134 | + (SeedSequence.ofWords words #[], g) |
| 135 | + |
| 136 | +/-- The range of values returned by `PCG64`, namely all of `[0, 2 ^ 64 - 1]`. -/ |
| 137 | +def range : Nat × Nat := (0, UInt64.size - 1) |
| 138 | + |
| 139 | +/-- Derive `n` generators from one, together with the parent advanced past the outputs used as |
| 140 | +entropy. The children are the `SeedSequence.spawn` children of the entropy drawn from the parent, |
| 141 | +so they are obtained exactly as any other family of generators in this library. -/ |
| 142 | +def spawn (g : PCG64) (n : Nat) : Array PCG64 × PCG64 := |
| 143 | + let (s, g) := g.toSeedSequence |
| 144 | + ((s.spawn n).1.map ofSeedSequence, g) |
| 145 | + |
| 146 | +/-- Derive two generators from one: the first is the current one advanced by two steps, the second |
| 147 | +is the first `SeedSequence.spawn` child of those two outputs. Splitting is not part of the PCG |
| 148 | +specification and nothing here establishes that the two streams are independent; going through the |
| 149 | +mixer only removes the direct algebraic tie between the child's initial state and two consecutive |
| 150 | +states of the parent. -/ |
| 151 | +def split (g : PCG64) : PCG64 × PCG64 := |
| 152 | + let (s, g) := g.toSeedSequence |
| 153 | + (g, ofSeedSequence (s.spawn 1).1[0]!) |
| 154 | + |
| 155 | +/-- The first `n` outputs of `g`. -/ |
| 156 | +def take (g : PCG64) (n : Nat) : Array UInt64 := Id.run do |
| 157 | + let mut g := g |
| 158 | + let mut out := Array.emptyWithCapacity n |
| 159 | + for _ in [:n] do |
| 160 | + let (x, g') := g.nextUInt64 |
| 161 | + g := g' |
| 162 | + out := out.push x |
| 163 | + return out |
| 164 | + |
| 165 | +end PCG64 |
| 166 | + |
| 167 | +/-- Returns a PCG-64 generator seeded with `s`, on the default stream. The analogue of |
| 168 | +`mkStdGen`. -/ |
| 169 | +def mkPCG64 (s : Nat := 0) : PCG64 := PCG64.seed s |
| 170 | + |
| 171 | +instance : Inhabited PCG64 := ⟨mkPCG64⟩ |
| 172 | + |
| 173 | +instance : RandomGen PCG64 where |
| 174 | + range _ := PCG64.range |
| 175 | + next g := let (x, g) := g.nextUInt64; (x.toNat, g) |
| 176 | + split := PCG64.split |
| 177 | + |
| 178 | +/-- A monad transformer to generate random objects using the generator type `PCG64`. |
| 179 | +`RandPCG m α` should be thought of a random value in `m α`. -/ |
| 180 | +abbrev RandPCG := RandGT PCG64 |
| 181 | + |
| 182 | +end NumLean |
| 183 | + |
| 184 | +namespace IO |
| 185 | + |
| 186 | +open NumLean |
| 187 | + |
| 188 | +/-- A global reference to a PCG-64 generator, seeded from the system's random source. -/ |
| 189 | +initialize PCG64Ref : Ref PCG64 ← |
| 190 | + let seed := UInt64.toNat (ByteArray.toUInt64LE! (← IO.getRandomBytes 8)) |
| 191 | + IO.mkRef (mkPCG64 seed) |
| 192 | + |
| 193 | +variable {m : Type* → Type*} {m₀ : Type → Type} |
| 194 | +variable [Monad m] [MonadLiftT (ST RealWorld) m₀] [ULiftable m₀ m] |
| 195 | + |
| 196 | +set_option autoImplicit true |
| 197 | + |
| 198 | +/-- Execute `RandPCG m α` using the global `PCG64Ref` as RNG. -/ |
| 199 | +def runRandPCG (cmd : RandPCG m α) : m α := do |
| 200 | + let PCG64 ← ULiftable.up (PCG64Ref.get : m₀ _) |
| 201 | + let (res, new) ← StateT.run cmd PCG64 |
| 202 | + let _ ← ULiftable.up (PCG64Ref.set new.down : m₀ _) |
| 203 | + pure res |
| 204 | + |
| 205 | +/-- Execute `RandPCG m α` using the global `PCG64Ref` as RNG and the given `seed`. -/ |
| 206 | +def runRandPCGWith (seed : Nat) (cmd : RandPCG m α) : m α := do |
| 207 | + pure <| (← cmd.run (ULift.up <| mkPCG64 seed)).1 |
| 208 | + |
| 209 | +end IO |
0 commit comments