moonpool_sim/sim/
rng.rs

1//! Thread-local random number generation for simulation.
2//!
3//! This module provides deterministic randomness through thread-local storage,
4//! enabling clean API design without passing RNG through the simulation state.
5//! Each thread maintains its own RNG state, ensuring deterministic behavior
6//! within each simulation run while supporting parallel test execution.
7
8use rand::SeedableRng;
9use rand::{
10    Rng,
11    distr::{Distribution, StandardUniform, uniform::SampleUniform},
12};
13use rand_chacha::ChaCha8Rng;
14use std::cell::RefCell;
15
16thread_local! {
17    /// Thread-local random number generator for simulation.
18    ///
19    /// Uses ChaCha8Rng for deterministic, reproducible randomness.
20    /// Each thread maintains independent state for parallel test execution.
21    static SIM_RNG: RefCell<ChaCha8Rng> = RefCell::new(ChaCha8Rng::seed_from_u64(0));
22
23    /// Thread-local storage for the current simulation seed.
24    ///
25    /// This stores the last seed set via [`set_sim_seed`] to enable
26    /// error reporting with seed information.
27    static CURRENT_SEED: RefCell<u64> = const { RefCell::new(0) };
28}
29
30/// Generate a random value using the thread-local simulation RNG.
31///
32/// This function provides deterministic randomness based on the seed set
33/// via [`set_sim_seed`]. The same seed will always produce the same sequence
34/// of random values within a single thread.
35///
36/// # Type Parameters
37///
38/// * `T` - The type to generate. Must implement the Standard distribution.
39///
40/// Generate a random value using the thread-local simulation RNG.
41pub fn sim_random<T>() -> T
42where
43    StandardUniform: Distribution<T>,
44{
45    SIM_RNG.with(|rng| rng.borrow_mut().sample(StandardUniform))
46}
47
48/// Generate a random value within a specified range using the thread-local simulation RNG.
49///
50/// This function provides deterministic randomness for values within a range.
51/// The same seed will always produce the same sequence of values.
52///
53/// # Type Parameters
54///
55/// * `T` - The type to generate. Must implement SampleUniform.
56///
57/// # Parameters
58///
59/// * `range` - The range to sample from (exclusive upper bound).
60///
61/// Generate a random value within a specified range.
62pub fn sim_random_range<T>(range: std::ops::Range<T>) -> T
63where
64    T: SampleUniform + PartialOrd,
65{
66    SIM_RNG.with(|rng| rng.borrow_mut().random_range(range))
67}
68
69/// Generate a random value within the given range, returning the start value if the range is empty.
70///
71/// This is a safe version of [`sim_random_range`] that handles empty ranges gracefully
72/// by returning the start value when start == end.
73///
74/// # Parameters
75///
76/// * `range` - The range to sample from (start..end)
77///
78/// # Returns
79///
80/// A random value within the range, or the start value if the range is empty.
81///
82/// Generate a random value in range or return start value if range is empty.
83pub fn sim_random_range_or_default<T>(range: std::ops::Range<T>) -> T
84where
85    T: SampleUniform + PartialOrd + Clone,
86{
87    if range.start >= range.end {
88        range.start
89    } else {
90        sim_random_range(range)
91    }
92}
93
94/// Set the seed for the thread-local simulation RNG.
95///
96/// This function initializes the thread-local RNG with a specific seed,
97/// ensuring deterministic behavior. The same seed will always produce
98/// the same sequence of random values.
99///
100/// # Parameters
101///
102/// * `seed` - The seed value to use for deterministic randomness.
103///
104/// Set the seed for the thread-local simulation RNG.
105pub fn set_sim_seed(seed: u64) {
106    SIM_RNG.with(|rng| {
107        *rng.borrow_mut() = ChaCha8Rng::seed_from_u64(seed);
108    });
109    CURRENT_SEED.with(|current| {
110        *current.borrow_mut() = seed;
111    });
112}
113
114/// Generate a random f64 in the range [0.0, 1.0) using the simulation RNG.
115///
116/// This is a convenience function matching FDB's `deterministicRandom()->random01()`.
117///
118/// # Returns
119///
120/// A random f64 value in [0.0, 1.0).
121pub fn sim_random_f64() -> f64 {
122    sim_random::<f64>()
123}
124
125/// Get the current simulation seed.
126///
127/// Returns the seed that was last set via [`set_sim_seed`].
128/// This is useful for error reporting to help reproduce failing test cases.
129///
130/// # Returns
131///
132/// The current simulation seed, or 0 if no seed has been set.
133///
134/// Get the current simulation seed.
135pub fn get_current_sim_seed() -> u64 {
136    CURRENT_SEED.with(|current| *current.borrow())
137}
138
139/// Reset the thread-local simulation RNG to a fresh state.
140///
141/// This function clears any existing RNG state and initializes with entropy.
142/// It should be called before setting a new seed to ensure clean state
143/// between consecutive simulation runs on the same thread.
144///
145/// Reset the thread-local simulation RNG to a fresh state.
146pub fn reset_sim_rng() {
147    SIM_RNG.with(|rng| {
148        *rng.borrow_mut() = ChaCha8Rng::seed_from_u64(0);
149    });
150    CURRENT_SEED.with(|current| {
151        *current.borrow_mut() = 0;
152    });
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_deterministic_randomness() {
161        // Set seed and generate some values
162        set_sim_seed(42);
163        let value1: f64 = sim_random();
164        let value2: u32 = sim_random();
165        let value3: bool = sim_random();
166
167        // Reset to same seed and verify same sequence
168        set_sim_seed(42);
169        assert_eq!(value1, sim_random::<f64>());
170        assert_eq!(value2, sim_random::<u32>());
171        assert_eq!(value3, sim_random::<bool>());
172    }
173
174    #[test]
175    fn test_different_seeds_produce_different_values() {
176        // Generate values with first seed
177        set_sim_seed(1);
178        let value1_seed1: f64 = sim_random();
179        let value2_seed1: f64 = sim_random();
180
181        // Generate values with different seed
182        set_sim_seed(2);
183        let value1_seed2: f64 = sim_random();
184        let value2_seed2: f64 = sim_random();
185
186        // Values should be different
187        assert_ne!(value1_seed1, value1_seed2);
188        assert_ne!(value2_seed1, value2_seed2);
189    }
190
191    #[test]
192    fn test_sim_random_range() {
193        set_sim_seed(42);
194
195        // Test integer range
196        for _ in 0..100 {
197            let value = sim_random_range(10..20);
198            assert!(value >= 10);
199            assert!(value < 20);
200        }
201
202        // Test f64 range
203        for _ in 0..100 {
204            let value = sim_random_range(0.0..1.0);
205            assert!(value >= 0.0);
206            assert!(value < 1.0);
207        }
208    }
209
210    #[test]
211    fn test_range_determinism() {
212        set_sim_seed(123);
213        let value1 = sim_random_range(100..1000);
214        let value2 = sim_random_range(0.0..10.0);
215
216        set_sim_seed(123);
217        assert_eq!(value1, sim_random_range(100..1000));
218        assert_eq!(value2, sim_random_range(0.0..10.0));
219    }
220
221    #[test]
222    fn test_reset_clears_state() {
223        // Set seed and advance RNG
224        set_sim_seed(42);
225        let _advance1: f64 = sim_random();
226        let _advance2: f64 = sim_random();
227        let after_advance: f64 = sim_random();
228
229        // Reset and set same seed - should get first value, not third
230        reset_sim_rng();
231        set_sim_seed(42);
232        let first_value: f64 = sim_random();
233
234        // Should be different because reset cleared the advanced state
235        assert_ne!(after_advance, first_value);
236    }
237
238    #[test]
239    fn test_sequence_persistence_within_thread() {
240        set_sim_seed(42);
241        let value1: f64 = sim_random();
242        let value2: f64 = sim_random();
243        let value3: f64 = sim_random();
244
245        // Values should form a deterministic sequence
246        set_sim_seed(42);
247        assert_eq!(value1, sim_random::<f64>());
248        assert_eq!(value2, sim_random::<f64>());
249        assert_eq!(value3, sim_random::<f64>());
250    }
251
252    #[test]
253    fn test_multiple_resets_and_seeds() {
254        // Test multiple reset/seed cycles
255        for seed in [1, 42, 12345] {
256            reset_sim_rng();
257            set_sim_seed(seed);
258            let first: f64 = sim_random();
259
260            reset_sim_rng();
261            set_sim_seed(seed);
262            assert_eq!(first, sim_random::<f64>());
263        }
264    }
265
266    #[test]
267    fn test_get_current_sim_seed() {
268        // Test getting current seed after setting
269        set_sim_seed(12345);
270        assert_eq!(get_current_sim_seed(), 12345);
271
272        set_sim_seed(98765);
273        assert_eq!(get_current_sim_seed(), 98765);
274
275        // Test that reset clears the seed
276        reset_sim_rng();
277        assert_eq!(get_current_sim_seed(), 0);
278    }
279}