Bit-exact Python reimplementation of Rust's rand_pcg::Pcg64 (LCG128-XSL-RR).
Produces identical sequences to Rust's rand_pcg v0.3 + rand v0.8 + rand_distr v0.4 for the same seed. Zero dependencies beyond the Python standard library.
When you have a Rust simulation that uses rand::Rng and you need to reproduce its exact random sequences in Python — for testing, debugging, analysis, or building a fast Python prototype that matches Rust output bit-for-bit.
NumPy's PCG64 uses the DXSM output permutation. Rust's Pcg64 uses XSL-RR. They are not interchangeable — same state, different output. This library bridges that gap.
pip install rust-pcg64from rust_pcg64 import RustPcg64
# Same as Rust's: let mut rng = Pcg64::seed_from_u64(42);
rng = RustPcg64.seed_from_u64(42)
val = rng.next_u64() # rng.gen::<u64>()
f = rng.gen_f64() # rng.gen::<f64>()
x = rng.gen_range_f64(0.0, 10.0) # rng.gen_range(0.0..10.0)
n = rng.gen_range_int(1, 6) # rng.gen_range(1..=6) (u64 by default)
# Distribution samplers (matching rand_distr)
z = rng.standard_normal() # StandardNormal
p = rng.poisson(5.0) # Poisson(5.0)
y = rng.lognormal(0.0, 1.0) # LogNormal(0.0, 1.0)from rust_pcg64 import RustRNG
rng = RustRNG(seed=42)
z = rng.standard_normal()
u = rng.uniform(0.0, 1.0)
n = rng.integers(0, 100) # [0, 100)
p = rng.poisson(5.0)
y = rng.lognormal(0.0, 1.0)
f = rng.random() # [0, 1)| Python | Rust equivalent | Notes |
|---|---|---|
next_u64() |
rng.gen::<u64>() |
Core output function |
gen_f64() |
rng.gen::<f64>() |
Standard distribution, [0, 1) |
gen_open01() |
rng.sample(Open01) |
(0, 1) |
gen_range_f64(lo, hi) |
rng.gen_range(lo..hi) |
Uses sample_single |
gen_range_int(lo, hi) |
rng.gen_range(lo..=hi) |
Supports u8/u16/u32/u64 |
standard_normal() |
StandardNormal |
Ziggurat (ZIGNOR) |
poisson(lam) |
Poisson::new(lam) |
Knuth / rejection |
lognormal(mu, sigma) |
LogNormal::new(mu, sigma) |
Via standard_normal |
seed_from_u64(seed) |
Pcg64::seed_from_u64(seed) |
PCG32-based expansion |
from_seed(bytes) |
Pcg64::from_seed(bytes) |
32-byte seed |
Rust uses different algorithms for different integer widths. The int_type parameter selects which variant to emulate:
rng.gen_range_int(0, 255, int_type='u8') # u8 range (modulus zone)
rng.gen_range_int(0, 999, int_type='u16') # u16 range (modulus zone)
rng.gen_range_int(0, 999, int_type='u32') # u32 range (bit-shift zone)
rng.gen_range_int(0, 999, int_type='u64') # u64 range (bit-shift zone, default)The core is a 128-bit Linear Congruential Generator with the XSL-RR (xorshift-low, rotate-right) output permutation:
state = state * MULT + INCREMENT (mod 2^128)
output = rotate_right_64((state >> 64) ^ state, state >> 122)
Seed expansion from u64 to 32 bytes uses a PCG32 generator (matching rand_core::SeedableRng::seed_from_u64).
Distribution samplers match Rust's rand_distr implementations:
- Normal: Ziggurat method (ZIGNOR variant) with 256-entry tables
- Poisson: Knuth's method for small lambda, rejection with Cauchy for large lambda
- LogNormal:
exp(normal * sigma + mu)
// Rust verification program
use rand::SeedableRng;
use rand::Rng;
use rand_pcg::Pcg64;
fn main() {
let mut rng = Pcg64::seed_from_u64(42);
println!("{}", rng.gen::<u64>()); // Compare with Python
println!("{}", rng.gen::<f64>());
println!("{}", rng.gen_range(0.0..10.0));
}# Python verification
from rust_pcg64 import RustPcg64
rng = RustPcg64.seed_from_u64(42)
print(rng.next_u64()) # Same value
print(rng.gen_f64()) # Same value
print(rng.gen_range_f64(0.0, 10.0)) # Same value| Rust crate | Version | Status |
|---|---|---|
rand_pcg |
0.3.x | Fully matched |
rand |
0.8.x | Fully matched |
rand_distr |
0.4.x | Fully matched |
This is a pure Python implementation optimized for correctness, not speed. For high-throughput generation, use it to verify sequences then switch to Rust. Typical throughput: ~500K u64/sec on modern hardware.
MIT