Skip to content

Commit ece8e48

Browse files
committed
Add Lcg128CmDxsm64 generator compatible with NumPy's PCG64DXSM.
1 parent b0d7833 commit ece8e48

File tree

4 files changed

+264
-0
lines changed

4 files changed

+264
-0
lines changed

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/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/tests/lcg128cmdxsm64.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use rand_core::{RngCore, SeedableRng};
2+
use rand_pcg::{Lcg128CmDxsm64, Pcg64Dxsm};
3+
4+
#[test]
5+
fn test_lcg128cmdxsm64_advancing() {
6+
for seed in 0..20 {
7+
let mut rng1 = Lcg128CmDxsm64::seed_from_u64(seed);
8+
let mut rng2 = rng1.clone();
9+
for _ in 0..20 {
10+
rng1.next_u64();
11+
}
12+
rng2.advance(20);
13+
assert_eq!(rng1, rng2);
14+
}
15+
}
16+
17+
#[test]
18+
fn test_lcg128cmdxsm64_construction() {
19+
// Test that various construction techniques produce a working RNG.
20+
#[rustfmt::skip]
21+
let seed = [1,2,3,4, 5,6,7,8, 9,10,11,12, 13,14,15,16,
22+
17,18,19,20, 21,22,23,24, 25,26,27,28, 29,30,31,32];
23+
let mut rng1 = Lcg128CmDxsm64::from_seed(seed);
24+
assert_eq!(rng1.next_u64(), 12201417210360370199);
25+
26+
let mut rng2 = Lcg128CmDxsm64::from_rng(&mut rng1).unwrap();
27+
assert_eq!(rng2.next_u64(), 11487972556150888383);
28+
29+
let mut rng3 = Lcg128CmDxsm64::seed_from_u64(0);
30+
assert_eq!(rng3.next_u64(), 4111470453933123814);
31+
32+
// This is the same as Lcg128CmDxsm64, so we only have a single test:
33+
let mut rng4 = Pcg64Dxsm::seed_from_u64(0);
34+
assert_eq!(rng4.next_u64(), 4111470453933123814);
35+
}
36+
37+
#[test]
38+
fn test_lcg128cmdxsm64_reference() {
39+
// Numbers determined using `pcg_engines::cm_setseq_dxsm_128_64` from pcg-cpp.
40+
let mut rng = Lcg128CmDxsm64::new(42, 54);
41+
42+
let mut results = [0u64; 6];
43+
for i in results.iter_mut() {
44+
*i = rng.next_u64();
45+
}
46+
let expected: [u64; 6] = [
47+
17331114245835578256,
48+
10267467544499227306,
49+
9726600296081716989,
50+
10165951391103677450,
51+
12131334649314727261,
52+
10134094537930450875,
53+
];
54+
assert_eq!(results, expected);
55+
}
56+
57+
#[cfg(feature = "serde1")]
58+
#[test]
59+
fn test_lcg128cmdxsm64_serde() {
60+
use bincode;
61+
use std::io::{BufReader, BufWriter};
62+
63+
let mut rng = Lcg128CmDxsm64::seed_from_u64(0);
64+
65+
let buf: Vec<u8> = Vec::new();
66+
let mut buf = BufWriter::new(buf);
67+
bincode::serialize_into(&mut buf, &rng).expect("Could not serialize");
68+
69+
let buf = buf.into_inner().unwrap();
70+
let mut read = BufReader::new(&buf[..]);
71+
let mut deserialized: Lcg128CmDxsm64 =
72+
bincode::deserialize_from(&mut read).expect("Could not deserialize");
73+
74+
for _ in 0..16 {
75+
assert_eq!(rng.next_u64(), deserialized.next_u64());
76+
}
77+
}

0 commit comments

Comments
 (0)