Skip to main content

thruput_testkit/
rng.rs

1//! Deterministic per-agent randomness.
2//!
3//! Each agent gets its own stream derived from the scenario seed and the
4//! agent's identity, so adding or reordering agents does not perturb another
5//! agent's sequence. The generator is a small `SplitMix64`, kept in-crate so
6//! the byte-for-byte sequence stays stable across dependency updates.
7
8/// A deterministic pseudo-random stream for one agent.
9#[derive(Debug, Clone)]
10pub struct AgentRng {
11    state: u64,
12}
13
14impl AgentRng {
15    /// Derive an agent's stream from the scenario `seed` plus the agent's
16    /// `index` and `name`.
17    pub(crate) fn derive(seed: &str, index: usize, name: &str) -> Self {
18        // FNV-1a over the three inputs, then used to seed `SplitMix64`.
19        let mut hash = 0xcbf2_9ce4_8422_2325_u64;
20        let mut mix = |bytes: &[u8]| {
21            for &byte in bytes {
22                hash ^= u64::from(byte);
23                hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
24            }
25        };
26        mix(seed.as_bytes());
27        mix(&index.to_le_bytes());
28        mix(name.as_bytes());
29        Self::from_u64(hash)
30    }
31
32    /// Create a stream directly from a 64-bit seed.
33    pub fn from_u64(seed: u64) -> Self {
34        // Avoid an all-zero state, which reads like an uninitialized generator.
35        Self { state: seed | 1 }
36    }
37
38    /// Next 64-bit value in the stream.
39    pub fn next_u64(&mut self) -> u64 {
40        self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
41        let mut z = self.state;
42        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
43        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
44        z ^ (z >> 31)
45    }
46
47    /// Pick an index in `0..len`, or `None` when `len` is zero.
48    pub fn choose_index(&mut self, len: usize) -> Option<usize> {
49        if len == 0 {
50            return None;
51        }
52        let len_u64 = u64::try_from(len).unwrap_or(u64::MAX);
53        let pick = self.next_u64() % len_u64;
54        Some(usize::try_from(pick).unwrap_or(len - 1))
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::AgentRng;
61
62    #[test]
63    fn derivation_is_deterministic_and_agent_specific() {
64        let mut first = AgentRng::derive("seed-v1", 0, "alice");
65        let mut again = AgentRng::derive("seed-v1", 0, "alice");
66        let first_seq: Vec<u64> = (0..4).map(|_| first.next_u64()).collect();
67        let again_seq: Vec<u64> = (0..4).map(|_| again.next_u64()).collect();
68        assert_eq!(
69            first_seq, again_seq,
70            "same seed and identity yields the same stream"
71        );
72
73        let mut other = AgentRng::derive("seed-v1", 1, "bob");
74        let other_seq: Vec<u64> = (0..4).map(|_| other.next_u64()).collect();
75        assert_ne!(
76            first_seq, other_seq,
77            "a different agent yields a different stream"
78        );
79    }
80}