hoomd_rand/counter.rs
1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Helpers that enable consistent use of random numbers throughout hoomd-rs.
5
6use rand::Rng;
7use serde::{Deserialize, Serialize};
8
9use crate::SFC64;
10
11/// Conveniently construct counter based random number generators.
12///
13/// Counter based random number generators produce a stream of random numbers
14/// that is a reproducible function of a counter value. They are efficient to
15/// use, even when only one or a few random numbers are needed. [`Counter`]
16/// allows callers to conveniently construct RNGS that are independent, as well
17/// as ones that produce identical values.
18///
19/// There are 3 required elements of each counter.
20/// * `step` (8 bytes) is the current simulation step and ensures that random
21/// number streams are not correlated from one simulation step to the next.
22/// * `substep` (4 bytes) similarly ensures that different parts of the
23/// algorithm that advance the simulation are not correlated within a single
24/// step.
25/// * `seed` (4 bytes) is a value that allows users to execute replicate
26/// simulations that are identical except for the random numbers applied.
27///
28/// There is an additional 8-byte index. Generally, many simulation algorithms
29/// will set this to particle indices so that RNG streams are independent from
30/// one particle (or pair of particles) to the next. The [`indices`] method
31/// treats the index as two 4-byte indices. To generate the same random numbers
32/// (e.g. for use in a DPD thermostat) in independent threads, set the first
33/// index to `min(i,j)` and the second to `max(i,j)`.
34///
35/// [`indices`]: Self::indices
36///
37/// # Performance
38///
39/// The current implementation uses [`SFC64`], which generates
40/// one 64-bit word at at time. Benchmarks show that executing
41/// `Counter.new(...).make_rng()` and sampling values that fall in the first
42/// batch runs at approximately 100 million operations per second (run `cargo
43/// bench` to see the measured performance on your architecture).
44///
45/// [`SFC64`]: crate::SFC64
46///
47/// # Example
48///
49/// ```
50/// use hoomd_rand::Counter;
51/// use rand::{Rng, RngExt};
52///
53/// # let step = 100_000;
54/// # let substep = 10;
55/// # let seed = 100;
56/// let mut rng = Counter::new(step, substep, seed).make_rng();
57///
58/// let r: f64 = rng.random();
59/// ```
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61pub struct Counter {
62 /// The current simulation step.
63 step: u64,
64 /// The current substep.
65 substep: u32,
66 /// User-chosen random seed.
67 seed: u32,
68 /// The index.
69 index: u64,
70}
71
72impl Counter {
73 /// Construct a new counter.
74 ///
75 /// On constructions, the index defaults to 0.
76 ///
77 /// # Example
78 ///
79 /// ```
80 /// use hoomd_rand::Counter;
81 ///
82 /// let step = 100_000;
83 /// let substep = 10;
84 /// let seed = 100;
85 ///
86 /// let counter = Counter::new(step, substep, seed);
87 /// ```
88 #[must_use]
89 #[inline]
90 pub fn new(step: u64, substep: u32, seed: u32) -> Self {
91 Counter {
92 step,
93 substep,
94 seed,
95 index: 0,
96 }
97 }
98
99 /// Set the index with two 4-byte values.
100 ///
101 /// # Example
102 ///
103 /// ```
104 /// use hoomd_rand::Counter;
105 ///
106 /// let step = 100_000;
107 /// let substep = 10;
108 /// let seed = 100;
109 /// let i = 12;
110 /// let j = 152;
111 ///
112 /// let counter = Counter::new(step, substep, seed).indices(i.max(j), i.min(j));
113 /// ```
114 #[must_use]
115 #[inline]
116 pub fn indices(mut self, a: u32, b: u32) -> Self {
117 self.index = u64::from(a) << 32 | u64::from(b);
118 self
119 }
120
121 /// Set the index.
122 ///
123 /// # Example
124 ///
125 /// ```
126 /// use hoomd_rand::Counter;
127 ///
128 /// let step = 100_000;
129 /// let substep = 10;
130 /// let seed = 100;
131 /// let index = 1_000_000_000_000_u64;
132 ///
133 /// let counter = Counter::new(step, substep, seed).index(index);
134 /// ```
135 #[must_use]
136 #[inline]
137 pub fn index(mut self, index: u64) -> Self {
138 self.index = index;
139 self
140 }
141
142 /// Seed a [`Rng`] with the counter.
143 ///
144 /// # Example
145 ///
146 /// ```
147 /// use hoomd_rand::Counter;
148 /// use rand::{Rng, RngExt};
149 ///
150 /// let step = 100_000;
151 /// let substep = 10;
152 /// let seed = 100;
153 ///
154 /// let mut rng = Counter::new(step, substep, seed).make_rng();
155 ///
156 /// let r: f64 = rng.random();
157 /// ```
158 #[must_use]
159 #[inline]
160 pub fn make_rng(self) -> impl Rng + use<> {
161 SFC64::initialize(
162 self.step,
163 u64::from(self.substep) << 32 | u64::from(self.seed),
164 self.index,
165 0,
166 )
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use assert2::check;
174 use rand::RngExt;
175
176 /// Number of stream elements to sample.
177 const N: usize = 256;
178
179 #[test]
180 fn independent() {
181 // This test is not exhaustive, but serves as a quick check that the
182 // different elements in Counter indeed produce different random
183 // number streams.
184
185 let counters = [
186 Counter::new(0, 0, 0),
187 Counter::new(1, 0, 0),
188 Counter::new(0, 1, 0),
189 Counter::new(0, 0, 1),
190 Counter::new(0, 0, 0).indices(1, 0),
191 Counter::new(0, 0, 0).indices(0, 1),
192 Counter::new(0, 0, 0).index(2),
193 ];
194
195 for (i, counter_i) in counters.iter().enumerate() {
196 for (j, counter_j) in counters.iter().enumerate() {
197 let mut rng_i = counter_i.clone().make_rng();
198 let values_i = core::array::from_fn::<_, N, _>(|_| rng_i.random::<f64>());
199
200 let mut rng_j = counter_j.clone().make_rng();
201 let values_j = core::array::from_fn::<_, N, _>(|_| rng_j.random::<f64>());
202
203 if i == j {
204 check!(values_i == values_j);
205 } else {
206 check!(values_i != values_j);
207 }
208 }
209 }
210 }
211}