ferray_random/bitgen/seed_sequence.rs
1// ferray-random: SeedSequence — entropy mixing and child-spawn API
2//
3// Implements the NumPy `SeedSequence` interface: mix arbitrary entropy
4// (a primary u128 seed plus a spawn-key path) into a deterministic,
5// well-distributed sequence of 32-bit words suitable for seeding any
6// BitGenerator. Spawning produces children whose entropy paths differ
7// from the parent's, so independent streams from a single root seed are
8// reproducible.
9
10#![allow(clippy::unreadable_literal)]
11
12use super::BitGenerator;
13use super::splitmix64;
14
15/// SeedSequence — deterministic entropy expansion and child spawning.
16///
17/// Equivalent to `numpy.random.SeedSequence`. Construct from an
18/// `entropy: u64`; call [`generate_state`](Self::generate_state) for
19/// `n_words` 32-bit seed words, or [`spawn`](Self::spawn) to create
20/// `n` independent child sequences whose state is reproducibly derived
21/// from this one.
22#[derive(Debug, Clone)]
23pub struct SeedSequence {
24 /// User-supplied root entropy.
25 entropy: u64,
26 /// Path of indices identifying this sequence within the parent's
27 /// spawn tree. Empty for the root.
28 spawn_key: Vec<u64>,
29 /// How many children have been spawned from this sequence so far.
30 /// Used to give the next spawn a fresh key.
31 n_children_spawned: u64,
32}
33
34impl SeedSequence {
35 /// Create a SeedSequence from a single entropy value.
36 #[must_use]
37 pub fn new(entropy: u64) -> Self {
38 Self {
39 entropy,
40 spawn_key: Vec::new(),
41 n_children_spawned: 0,
42 }
43 }
44
45 /// Create a SeedSequence with an explicit spawn-key path.
46 ///
47 /// The spawn key disambiguates children of the same parent.
48 /// Normally users construct via [`new`](Self::new) and let
49 /// [`spawn`](Self::spawn) populate the key automatically.
50 #[must_use]
51 pub fn with_spawn_key(entropy: u64, spawn_key: Vec<u64>) -> Self {
52 Self {
53 entropy,
54 spawn_key,
55 n_children_spawned: 0,
56 }
57 }
58
59 /// The entropy this sequence was created with.
60 #[must_use]
61 pub fn entropy(&self) -> u64 {
62 self.entropy
63 }
64
65 /// The spawn-key path of this sequence (empty for the root).
66 #[must_use]
67 pub fn spawn_key(&self) -> &[u64] {
68 &self.spawn_key
69 }
70
71 /// Generate `n_words` 32-bit seed words from this sequence.
72 ///
73 /// The output is a deterministic function of `(entropy, spawn_key)`
74 /// only — calling `generate_state` repeatedly returns the same
75 /// words. Different sequences produce statistically independent
76 /// outputs.
77 #[must_use]
78 pub fn generate_state(&self, n_words: usize) -> Vec<u32> {
79 // Mix entropy + spawn_key into a SplitMix64 state, then emit
80 // `n_words` 32-bit halves.
81 let mut state = self.entropy;
82 for &k in &self.spawn_key {
83 state ^= k.wrapping_mul(0x9e37_79b9_7f4a_7c15);
84 // Round-trip through SplitMix64 to avalanche bits.
85 let _ = splitmix64(&mut state);
86 }
87 let mut out = Vec::with_capacity(n_words);
88 let mut produced = 0usize;
89 while produced < n_words {
90 let v = splitmix64(&mut state);
91 out.push((v & 0xFFFF_FFFF) as u32);
92 produced += 1;
93 if produced < n_words {
94 out.push((v >> 32) as u32);
95 produced += 1;
96 }
97 }
98 out.truncate(n_words);
99 out
100 }
101
102 /// Generate a single u64 derived from this sequence — convenient for
103 /// seeding a [`BitGenerator`] via [`BitGenerator::seed_from_u64`].
104 #[must_use]
105 pub fn generate_u64(&self) -> u64 {
106 let words = self.generate_state(2);
107 (u64::from(words[1]) << 32) | u64::from(words[0])
108 }
109
110 /// Seed a fresh BitGenerator deterministically from this sequence.
111 pub fn seed<B: BitGenerator>(&self) -> B {
112 B::seed_from_u64(self.generate_u64())
113 }
114
115 /// Create `n` independent child sequences.
116 ///
117 /// Each child has the same `entropy` as the parent but a different
118 /// `spawn_key`, so their `generate_state` outputs do not overlap.
119 /// Mutates `self`'s spawn counter so successive calls yield fresh
120 /// children.
121 #[must_use]
122 pub fn spawn(&mut self, n: usize) -> Vec<SeedSequence> {
123 let mut children = Vec::with_capacity(n);
124 for _ in 0..n {
125 let mut key = self.spawn_key.clone();
126 key.push(self.n_children_spawned);
127 self.n_children_spawned += 1;
128 children.push(SeedSequence {
129 entropy: self.entropy,
130 spawn_key: key,
131 n_children_spawned: 0,
132 });
133 }
134 children
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn generate_state_deterministic() {
144 let s1 = SeedSequence::new(42);
145 let s2 = SeedSequence::new(42);
146 assert_eq!(s1.generate_state(8), s2.generate_state(8));
147 }
148
149 #[test]
150 fn different_entropy_differs() {
151 let s1 = SeedSequence::new(42).generate_state(8);
152 let s2 = SeedSequence::new(43).generate_state(8);
153 assert_ne!(s1, s2);
154 }
155
156 #[test]
157 fn spawn_produces_independent_children() {
158 let mut root = SeedSequence::new(1234);
159 let children = root.spawn(4);
160 assert_eq!(children.len(), 4);
161 let mut all_distinct = true;
162 for i in 0..children.len() {
163 for j in (i + 1)..children.len() {
164 if children[i].generate_state(8) == children[j].generate_state(8) {
165 all_distinct = false;
166 }
167 }
168 }
169 assert!(all_distinct);
170 // Children also differ from root.
171 for c in &children {
172 assert_ne!(c.generate_state(8), root.generate_state(8));
173 }
174 }
175
176 #[test]
177 fn spawn_advances_counter() {
178 let mut a = SeedSequence::new(1);
179 let mut b = SeedSequence::new(1);
180 let _first = a.spawn(2); // a.n_children_spawned = 2
181 let second = a.spawn(1)[0].clone(); // spawn_key uses index 2
182 let zeroth = b.spawn(1)[0].clone(); // spawn_key uses index 0
183 assert_ne!(second.generate_state(4), zeroth.generate_state(4));
184 }
185
186 #[test]
187 fn seed_bitgenerator_roundtrips() {
188 use crate::bitgen::Pcg64;
189 let s = SeedSequence::new(7);
190 let mut a: Pcg64 = s.seed();
191 let mut b: Pcg64 = s.seed();
192 for _ in 0..32 {
193 assert_eq!(a.next_u64(), b.next_u64());
194 }
195 }
196}