Skip to content

Commit 1801a5e

Browse files
committed
Add Numpy's RNG
1 parent 1eee785 commit 1801a5e

6 files changed

Lines changed: 452 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@
2828
*.synctex.gz
2929
*.synctex.gz(busy)
3030
*.pdfsync
31+
test_data/

‎RandomDo.lean‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ public import RandomDo.Monad.ForInInstances
66
public import RandomDo.Monad.Instances
77
public import RandomDo.Monad.MeasurableSpace
88
public import RandomDo.Monad.Notation
9+
public import RandomDo.NumLean.Distributions
10+
public import RandomDo.NumLean.PCG64
11+
public import RandomDo.NumLean.SeedSequence
912
public import RandomDo.Tactic.Deriving
1013
public import RandomDo.Tactic.Elab
1114
public import RandomDo.Tactic.ForInStep
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 RandomDo.NumLean.PCG64
9+
public meta import RandomDo.NumLean.PCG64
10+
11+
/-!
12+
# Sample from specific distributions using the PCG-64 generator.
13+
14+
This files provides samplers for specific distributions using the PCG-64 generator.
15+
16+
## Main definitions
17+
* `randUInt64`: sample a `UInt64`.
18+
* `random`: sample a `Float` in `[0, 1)`.
19+
-/
20+
21+
@[expose] public section
22+
23+
namespace NumLean
24+
25+
/-- Sample a `UInt64` from a PCG-64 generator. -/
26+
def randUInt64 : RandPCG IO UInt64 := do
27+
let (x, g) := (← get).down.nextUInt64
28+
set (ULift.up g)
29+
return x
30+
31+
/-- Sample a `Float` in `[0, 1)` from a PCG-64 generator. -/
32+
def random : RandPCG IO Float := do
33+
let x ← randUInt64
34+
return (x >>> 11).toFloat * (Float.ofBits <| 0x3CA <<< (52 : UInt64))
35+
36+
end NumLean

‎RandomDo/NumLean/PCG64.lean‎

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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

Comments
 (0)