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}