Skip to content

Commit 19404d6

Browse files
authored
Merge pull request #1202 from adamreichold/numpy-compat
Add Lcg128CmDxsm64 generator compatible with NumPy's PCG64DXSM
2 parents 1a880aa + f44ea42 commit 19404d6

File tree

10 files changed

+286
-29
lines changed

10 files changed

+286
-29
lines changed

benches/generators.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use rand::prelude::*;
2121
use rand::rngs::adapter::ReseedingRng;
2222
use rand::rngs::{mock::StepRng, OsRng};
2323
use rand_chacha::{ChaCha12Rng, ChaCha20Core, ChaCha20Rng, ChaCha8Rng};
24-
use rand_pcg::{Pcg32, Pcg64, Pcg64Mcg};
24+
use rand_pcg::{Pcg32, Pcg64, Pcg64Mcg, Pcg64Dxsm};
2525

2626
macro_rules! gen_bytes {
2727
($fnn:ident, $gen:expr) => {
@@ -44,6 +44,7 @@ gen_bytes!(gen_bytes_step, StepRng::new(0, 1));
4444
gen_bytes!(gen_bytes_pcg32, Pcg32::from_entropy());
4545
gen_bytes!(gen_bytes_pcg64, Pcg64::from_entropy());
4646
gen_bytes!(gen_bytes_pcg64mcg, Pcg64Mcg::from_entropy());
47+
gen_bytes!(gen_bytes_pcg64dxsm, Pcg64Dxsm::from_entropy());
4748
gen_bytes!(gen_bytes_chacha8, ChaCha8Rng::from_entropy());
4849
gen_bytes!(gen_bytes_chacha12, ChaCha12Rng::from_entropy());
4950
gen_bytes!(gen_bytes_chacha20, ChaCha20Rng::from_entropy());
@@ -73,6 +74,7 @@ gen_uint!(gen_u32_step, u32, StepRng::new(0, 1));
7374
gen_uint!(gen_u32_pcg32, u32, Pcg32::from_entropy());
7475
gen_uint!(gen_u32_pcg64, u32, Pcg64::from_entropy());
7576
gen_uint!(gen_u32_pcg64mcg, u32, Pcg64Mcg::from_entropy());
77+
gen_uint!(gen_u32_pcg64dxsm, u32, Pcg64Dxsm::from_entropy());
7678
gen_uint!(gen_u32_chacha8, u32, ChaCha8Rng::from_entropy());
7779
gen_uint!(gen_u32_chacha12, u32, ChaCha12Rng::from_entropy());
7880
gen_uint!(gen_u32_chacha20, u32, ChaCha20Rng::from_entropy());
@@ -85,6 +87,7 @@ gen_uint!(gen_u64_step, u64, StepRng::new(0, 1));
8587
gen_uint!(gen_u64_pcg32, u64, Pcg32::from_entropy());
8688
gen_uint!(gen_u64_pcg64, u64, Pcg64::from_entropy());
8789
gen_uint!(gen_u64_pcg64mcg, u64, Pcg64Mcg::from_entropy());
90+
gen_uint!(gen_u64_pcg64dxsm, u64, Pcg64Dxsm::from_entropy());
8891
gen_uint!(gen_u64_chacha8, u64, ChaCha8Rng::from_entropy());
8992
gen_uint!(gen_u64_chacha12, u64, ChaCha12Rng::from_entropy());
9093
gen_uint!(gen_u64_chacha20, u64, ChaCha20Rng::from_entropy());
@@ -109,6 +112,7 @@ macro_rules! init_gen {
109112
init_gen!(init_pcg32, Pcg32);
110113
init_gen!(init_pcg64, Pcg64);
111114
init_gen!(init_pcg64mcg, Pcg64Mcg);
115+
init_gen!(init_pcg64dxsm, Pcg64Dxsm);
112116
init_gen!(init_chacha, ChaCha20Rng);
113117

114118
const RESEEDING_BYTES_LEN: usize = 1024 * 1024;

rand_pcg/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
- Add `Lcg128CmDxsm64` generator compatible with NumPy's `PCG64DXSM` (#1202)
9+
710
## [0.3.1] - 2021-06-15
811
- Add `advance` methods to RNGs (#1111)
912
- Document dependencies between streams (#1122)

rand_pcg/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
#![no_std]
3939

4040
mod pcg128;
41+
mod pcg128cm;
4142
mod pcg64;
4243

4344
pub use self::pcg128::{Lcg128Xsl64, Mcg128Xsl64, Pcg64, Pcg64Mcg};
45+
pub use self::pcg128cm::{Lcg128CmDxsm64, Pcg64Dxsm};
4446
pub use self::pcg64::{Lcg64Xsh32, Pcg32};

rand_pcg/src/pcg128.rs

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
const MULTIPLIER: u128 = 0x2360_ED05_1FC6_5DA4_4385_DF64_9FCC_F645;
1515

1616
use core::fmt;
17-
use rand_core::{le, Error, RngCore, SeedableRng};
17+
use rand_core::{impls, le, Error, RngCore, SeedableRng};
1818
#[cfg(feature = "serde1")] use serde::{Deserialize, Serialize};
1919

2020
/// A PCG random number generator (XSL RR 128/64 (LCG) variant).
@@ -77,6 +77,9 @@ impl Lcg128Xsl64 {
7777

7878
/// Construct an instance compatible with PCG seed and stream.
7979
///
80+
/// Note that the highest bit of the `stream` parameter is discarded
81+
/// to simplify upholding internal invariants.
82+
///
8083
/// Note that two generators with different stream parameters may be closely
8184
/// correlated.
8285
///
@@ -116,11 +119,11 @@ impl fmt::Debug for Lcg128Xsl64 {
116119
}
117120
}
118121

119-
/// We use a single 255-bit seed to initialise the state and select a stream.
120-
/// One `seed` bit (lowest bit of `seed[8]`) is ignored.
121122
impl SeedableRng for Lcg128Xsl64 {
122123
type Seed = [u8; 32];
123124

125+
/// We use a single 255-bit seed to initialise the state and select a stream.
126+
/// One `seed` bit (lowest bit of `seed[8]`) is ignored.
124127
fn from_seed(seed: Self::Seed) -> Self {
125128
let mut seed_u64 = [0u64; 4];
126129
le::read_u64_into(&seed, &mut seed_u64);
@@ -146,7 +149,7 @@ impl RngCore for Lcg128Xsl64 {
146149

147150
#[inline]
148151
fn fill_bytes(&mut self, dest: &mut [u8]) {
149-
fill_bytes_impl(self, dest)
152+
impls::fill_bytes_via_next(self, dest)
150153
}
151154

152155
#[inline]
@@ -237,8 +240,7 @@ impl SeedableRng for Mcg128Xsl64 {
237240
// Read as if a little-endian u128 value:
238241
let mut seed_u64 = [0u64; 2];
239242
le::read_u64_into(&seed, &mut seed_u64);
240-
let state = u128::from(seed_u64[0]) |
241-
u128::from(seed_u64[1]) << 64;
243+
let state = u128::from(seed_u64[0]) | u128::from(seed_u64[1]) << 64;
242244
Mcg128Xsl64::new(state)
243245
}
244246
}
@@ -257,7 +259,7 @@ impl RngCore for Mcg128Xsl64 {
257259

258260
#[inline]
259261
fn fill_bytes(&mut self, dest: &mut [u8]) {
260-
fill_bytes_impl(self, dest)
262+
impls::fill_bytes_via_next(self, dest)
261263
}
262264

263265
#[inline]
@@ -278,19 +280,3 @@ fn output_xsl_rr(state: u128) -> u64 {
278280
let xsl = ((state >> XSHIFT) as u64) ^ (state as u64);
279281
xsl.rotate_right(rot)
280282
}
281-
282-
#[inline(always)]
283-
fn fill_bytes_impl<R: RngCore + ?Sized>(rng: &mut R, dest: &mut [u8]) {
284-
let mut left = dest;
285-
while left.len() >= 8 {
286-
let (l, r) = { left }.split_at_mut(8);
287-
left = r;
288-
let chunk: [u8; 8] = rng.next_u64().to_le_bytes();
289-
l.copy_from_slice(&chunk);
290-
}
291-
let n = left.len();
292-
if n > 0 {
293-
let chunk: [u8; 8] = rng.next_u64().to_le_bytes();
294-
left.copy_from_slice(&chunk[..n]);
295-
}
296-
}

rand_pcg/src/pcg128cm.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2018-2021 Developers of the Rand project.
2+
// Copyright 2017 Paul Dicker.
3+
// Copyright 2014-2017, 2019 Melissa O'Neill and PCG Project contributors
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
//! PCG random number generators
12+
13+
// This is the cheap multiplier used by PCG for 128-bit state.
14+
const MULTIPLIER: u64 = 15750249268501108917;
15+
16+
use core::fmt;
17+
use rand_core::{impls, le, Error, RngCore, SeedableRng};
18+
#[cfg(feature = "serde1")] use serde::{Deserialize, Serialize};
19+
20+
/// A PCG random number generator (CM DXSM 128/64 (LCG) variant).
21+
///
22+
/// Permuted Congruential Generator with 128-bit state, internal Linear
23+
/// Congruential Generator, and 64-bit output via "double xorshift multiply"
24+
/// output function.
25+
///
26+
/// This is a 128-bit LCG with explicitly chosen stream with the PCG-DXSM
27+
/// output function. This corresponds to `pcg_engines::cm_setseq_dxsm_128_64`
28+
/// from pcg_cpp and `PCG64DXSM` from NumPy.
29+
///
30+
/// Despite the name, this implementation uses 32 bytes (256 bit) space
31+
/// comprising 128 bits of state and 128 bits stream selector. These are both
32+
/// set by `SeedableRng`, using a 256-bit seed.
33+
///
34+
/// Note that while two generators with different stream parameter may be
35+
/// closely correlated, this is [mitigated][upgrading-pcg64] by the DXSM output function.
36+
///
37+
/// [upgrading-pcg64]: https://numpy.org/doc/stable/reference/random/upgrading-pcg64.html
38+
#[derive(Clone, PartialEq, Eq)]
39+
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
40+
pub struct Lcg128CmDxsm64 {
41+
state: u128,
42+
increment: u128,
43+
}
44+
45+
/// [`Lcg128CmDxsm64`] is also known as `PCG64DXSM`.
46+
pub type Pcg64Dxsm = Lcg128CmDxsm64;
47+
48+
impl Lcg128CmDxsm64 {
49+
/// Multi-step advance functions (jump-ahead, jump-back)
50+
///
51+
/// The method used here is based on Brown, "Random Number Generation
52+
/// with Arbitrary Stride,", Transactions of the American Nuclear
53+
/// Society (Nov. 1994). The algorithm is very similar to fast
54+
/// exponentiation.
55+
///
56+
/// Even though delta is an unsigned integer, we can pass a
57+
/// signed integer to go backwards, it just goes "the long way round".
58+
///
59+
/// Using this function is equivalent to calling `next_64()` `delta`
60+
/// number of times.
61+
#[inline]
62+
pub fn advance(&mut self, delta: u128) {
63+
let mut acc_mult: u128 = 1;
64+
let mut acc_plus: u128 = 0;
65+
let mut cur_mult = MULTIPLIER as u128;
66+
let mut cur_plus = self.increment;
67+
let mut mdelta = delta;
68+
69+
while mdelta > 0 {
70+
if (mdelta & 1) != 0 {
71+
acc_mult = acc_mult.wrapping_mul(cur_mult);
72+
acc_plus = acc_plus.wrapping_mul(cur_mult).wrapping_add(cur_plus);
73+
}
74+
cur_plus = cur_mult.wrapping_add(1).wrapping_mul(cur_plus);
75+
cur_mult = cur_mult.wrapping_mul(cur_mult);
76+
mdelta /= 2;
77+
}
78+
self.state = acc_mult.wrapping_mul(self.state).wrapping_add(acc_plus);
79+
}
80+
81+
/// Construct an instance compatible with PCG seed and stream.
82+
///
83+
/// Note that the highest bit of the `stream` parameter is discarded
84+
/// to simplify upholding internal invariants.
85+
///
86+
/// Note that while two generators with different stream parameter may be
87+
/// closely correlated, this is [mitigated][upgrading-pcg64] by the DXSM output function.
88+
///
89+
/// PCG specifies the following default values for both parameters:
90+
///
91+
/// - `state = 0xcafef00dd15ea5e5`
92+
/// - `stream = 0xa02bdbf7bb3c0a7ac28fa16a64abf96`
93+
///
94+
/// [upgrading-pcg64]: https://numpy.org/doc/stable/reference/random/upgrading-pcg64.html
95+
pub fn new(state: u128, stream: u128) -> Self {
96+
// The increment must be odd, hence we discard one bit:
97+
let increment = (stream << 1) | 1;
98+
Self::from_state_incr(state, increment)
99+
}
100+
101+
#[inline]
102+
fn from_state_incr(state: u128, increment: u128) -> Self {
103+
let mut pcg = Self { state, increment };
104+
// Move away from initial value:
105+
pcg.state = pcg.state.wrapping_add(pcg.increment);
106+
pcg.step();
107+
pcg
108+
}
109+
110+
#[inline(always)]
111+
fn step(&mut self) {
112+
// prepare the LCG for the next round
113+
self.state = self
114+
.state
115+
.wrapping_mul(MULTIPLIER as u128)
116+
.wrapping_add(self.increment);
117+
}
118+
}
119+
120+
// Custom Debug implementation that does not expose the internal state
121+
impl fmt::Debug for Lcg128CmDxsm64 {
122+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123+
write!(f, "Lcg128CmDxsm64 {{}}")
124+
}
125+
}
126+
127+
impl SeedableRng for Lcg128CmDxsm64 {
128+
type Seed = [u8; 32];
129+
130+
/// We use a single 255-bit seed to initialise the state and select a stream.
131+
/// One `seed` bit (lowest bit of `seed[8]`) is ignored.
132+
fn from_seed(seed: Self::Seed) -> Self {
133+
let mut seed_u64 = [0u64; 4];
134+
le::read_u64_into(&seed, &mut seed_u64);
135+
let state = u128::from(seed_u64[0]) | (u128::from(seed_u64[1]) << 64);
136+
let incr = u128::from(seed_u64[2]) | (u128::from(seed_u64[3]) << 64);
137+
138+
// The increment must be odd, hence we discard one bit:
139+
Self::from_state_incr(state, incr | 1)
140+
}
141+
}
142+
143+
impl RngCore for Lcg128CmDxsm64 {
144+
#[inline]
145+
fn next_u32(&mut self) -> u32 {
146+
self.next_u64() as u32
147+
}
148+
149+
#[inline]
150+
fn next_u64(&mut self) -> u64 {
151+
let val = output_dxsm(self.state);
152+
self.step();
153+
val
154+
}
155+
156+
#[inline]
157+
fn fill_bytes(&mut self, dest: &mut [u8]) {
158+
impls::fill_bytes_via_next(self, dest)
159+
}
160+
161+
#[inline]
162+
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
163+
self.fill_bytes(dest);
164+
Ok(())
165+
}
166+
}
167+
168+
#[inline(always)]
169+
fn output_dxsm(state: u128) -> u64 {
170+
// See https://github.com/imneme/pcg-cpp/blob/ffd522e7188bef30a00c74dc7eb9de5faff90092/include/pcg_random.hpp#L1016
171+
// for a short discussion of the construction and its original implementation.
172+
let mut hi = (state >> 64) as u64;
173+
let mut lo = state as u64;
174+
175+
lo |= 1;
176+
hi ^= hi >> 32;
177+
hi = hi.wrapping_mul(MULTIPLIER);
178+
hi ^= hi >> 48;
179+
hi = hi.wrapping_mul(lo);
180+
181+
hi
182+
}

rand_pcg/src/pcg64.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ impl Lcg64Xsh32 {
7777

7878
/// Construct an instance compatible with PCG seed and stream.
7979
///
80+
/// Note that the highest bit of the `stream` parameter is discarded
81+
/// to simplify upholding internal invariants.
82+
///
8083
/// Note that two generators with different stream parameters may be closely
8184
/// correlated.
8285
///
@@ -117,11 +120,11 @@ impl fmt::Debug for Lcg64Xsh32 {
117120
}
118121
}
119122

120-
/// We use a single 127-bit seed to initialise the state and select a stream.
121-
/// One `seed` bit (lowest bit of `seed[8]`) is ignored.
122123
impl SeedableRng for Lcg64Xsh32 {
123124
type Seed = [u8; 16];
124125

126+
/// We use a single 127-bit seed to initialise the state and select a stream.
127+
/// One `seed` bit (lowest bit of `seed[8]`) is ignored.
125128
fn from_seed(seed: Self::Seed) -> Self {
126129
let mut seed_u64 = [0u64; 2];
127130
le::read_u64_into(&seed, &mut seed_u64);

0 commit comments

Comments
 (0)