rlevo_core/util/seed.rs
1//! Deterministic seed derivation for benchmark trials.
2//!
3//! A single `base_seed` fans out to per-trial, per-env, and per-agent seeds
4//! via splitmix64 mixing. Outputs are independent of thread scheduling,
5//! ensuring that two runs with the same base seed produce identical seed
6//! sequences regardless of how work is distributed across threads.
7//!
8//! # Reproducibility contract
9//!
10//! All `rlevo` algorithms must draw randomness by calling into a
11//! [`SeedStream`] rather than reaching for the backend's process-wide RNG
12//! (e.g. `B::seed` + `Tensor::random`). The process-wide RNG is a global
13//! mutex; parallel test workers racing on it produce non-deterministic
14//! initialisation order even when the top-level seed is fixed.
15//!
16//! The recommended pattern:
17//!
18//! ```rust
19//! use rlevo_core::util::seed::SeedStream;
20//!
21//! let stream = SeedStream::new(42);
22//! let t = stream.trial_seed(/*env_idx=*/0, /*trial_idx=*/0);
23//! let env_rng_seed = stream.env_seed(t);
24//! let agent_rng_seed = stream.agent_seed(t);
25//! // Pass env_rng_seed / agent_rng_seed to the respective constructors.
26//! ```
27
28/// A deterministic, fan-out seed generator for benchmark and training trials.
29///
30/// `SeedStream` derives independent 64-bit seeds for each (environment index,
31/// trial index) pair, and further splits each trial seed into separate env and
32/// agent seeds. Every derivation is a pure function of `base` and the index
33/// arguments, so the same `SeedStream` always produces the same sequence.
34///
35/// # Design
36///
37/// Derivation uses splitmix64 — a bijective mixing function — XOR'd with
38/// index-dependent multipliers. The env and agent branches are separated by
39/// distinct domain constants so `env_seed(t) != agent_seed(t)` for all `t`.
40///
41/// # Thread safety
42///
43/// `SeedStream` holds no mutable state and is `Copy`. All methods are
44/// `const fn`. It is safe to share across threads without synchronisation.
45#[derive(Debug, Clone, Copy)]
46pub struct SeedStream {
47 base: u64,
48}
49
50impl SeedStream {
51 /// Creates a new `SeedStream` from the given base seed.
52 ///
53 /// All seeds derived from this stream are fully determined by `base`.
54 /// Two streams constructed with the same `base` produce identical output.
55 ///
56 /// # Examples
57 ///
58 /// ```rust
59 /// use rlevo_core::util::seed::SeedStream;
60 ///
61 /// let stream = SeedStream::new(0xDEAD_BEEF);
62 /// assert_eq!(stream.base(), 0xDEAD_BEEF);
63 /// ```
64 #[must_use]
65 pub const fn new(base: u64) -> Self {
66 Self { base }
67 }
68
69 /// Returns the base seed this stream was constructed with.
70 ///
71 /// Useful for serialising or logging the root of a reproducible run.
72 ///
73 /// # Examples
74 ///
75 /// ```rust
76 /// use rlevo_core::util::seed::SeedStream;
77 ///
78 /// let stream = SeedStream::new(42);
79 /// assert_eq!(stream.base(), 42);
80 /// ```
81 #[must_use]
82 pub const fn base(&self) -> u64 {
83 self.base
84 }
85
86 /// Derives a deterministic seed for a specific (env index, trial index) pair.
87 ///
88 /// The returned value is the root from which [`env_seed`](Self::env_seed)
89 /// and [`agent_seed`](Self::agent_seed) are derived for that trial.
90 /// Different `(env_idx, trial_idx)` pairs always produce different seeds.
91 ///
92 /// # Examples
93 ///
94 /// ```rust
95 /// use rlevo_core::util::seed::SeedStream;
96 ///
97 /// let stream = SeedStream::new(42);
98 ///
99 /// // Repeated calls with identical arguments are stable.
100 /// assert_eq!(stream.trial_seed(0, 0), stream.trial_seed(0, 0));
101 ///
102 /// // Different index pairs yield different seeds.
103 /// assert_ne!(stream.trial_seed(0, 0), stream.trial_seed(0, 1));
104 /// assert_ne!(stream.trial_seed(0, 0), stream.trial_seed(1, 0));
105 /// ```
106 #[must_use]
107 pub const fn trial_seed(&self, env_idx: usize, trial_idx: usize) -> u64 {
108 let mixed = splitmix64(self.base ^ (env_idx as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
109 splitmix64(mixed ^ (trial_idx as u64).wrapping_mul(0xBF58_476D_1CE4_E5B9))
110 }
111
112 /// Derives the environment seed for a given trial seed.
113 ///
114 /// Pass the result of [`trial_seed`](Self::trial_seed) as `trial_seed`.
115 /// The returned value is guaranteed to differ from [`agent_seed`](Self::agent_seed)
116 /// for the same input, preventing env and agent RNGs from correlating.
117 ///
118 /// # Examples
119 ///
120 /// ```rust
121 /// use rlevo_core::util::seed::SeedStream;
122 ///
123 /// let stream = SeedStream::new(42);
124 /// let t = stream.trial_seed(0, 0);
125 /// let env_seed = stream.env_seed(t);
126 /// let agent_seed = stream.agent_seed(t);
127 /// assert_ne!(env_seed, agent_seed);
128 /// ```
129 #[must_use]
130 pub const fn env_seed(&self, trial_seed: u64) -> u64 {
131 splitmix64(trial_seed ^ 0xD1B5_4A32_D192_ED03)
132 }
133
134 /// Derives the agent seed for a given trial seed.
135 ///
136 /// Pass the result of [`trial_seed`](Self::trial_seed) as `trial_seed`.
137 /// The returned value is guaranteed to differ from [`env_seed`](Self::env_seed)
138 /// for the same input, preventing agent and env RNGs from correlating.
139 ///
140 /// # Examples
141 ///
142 /// ```rust
143 /// use rlevo_core::util::seed::SeedStream;
144 ///
145 /// let stream = SeedStream::new(42);
146 /// let t = stream.trial_seed(0, 0);
147 ///
148 /// // agent_seed and env_seed are derived with different domain constants,
149 /// // so they are always distinct for the same trial seed.
150 /// assert_ne!(stream.agent_seed(t), stream.env_seed(t));
151 ///
152 /// // Repeated calls are stable.
153 /// assert_eq!(stream.agent_seed(t), stream.agent_seed(t));
154 /// ```
155 #[must_use]
156 pub const fn agent_seed(&self, trial_seed: u64) -> u64 {
157 splitmix64(trial_seed ^ 0x94D0_49BB_1331_11EB)
158 }
159}
160
161const fn splitmix64(mut x: u64) -> u64 {
162 x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
163 x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
164 x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
165 x ^ (x >> 31)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn trial_seeds_are_deterministic() {
174 let s = SeedStream::new(42);
175 assert_eq!(s.trial_seed(0, 0), s.trial_seed(0, 0));
176 assert_eq!(s.trial_seed(3, 7), s.trial_seed(3, 7));
177 }
178
179 #[test]
180 fn trial_seeds_are_distinct() {
181 let s = SeedStream::new(42);
182 let a = s.trial_seed(0, 0);
183 let b = s.trial_seed(0, 1);
184 let c = s.trial_seed(1, 0);
185 assert_ne!(a, b);
186 assert_ne!(a, c);
187 assert_ne!(b, c);
188 }
189
190 #[test]
191 fn env_and_agent_seeds_differ() {
192 let s = SeedStream::new(42);
193 let t = s.trial_seed(0, 0);
194 assert_ne!(s.env_seed(t), s.agent_seed(t));
195 }
196
197 #[test]
198 fn base_seed_changes_output() {
199 let a = SeedStream::new(1).trial_seed(0, 0);
200 let b = SeedStream::new(2).trial_seed(0, 0);
201 assert_ne!(a, b);
202 }
203}