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}