Skip to main content

use_seed/
lib.rs

1#![forbid(unsafe_code)]
2//! Repeatable seed helpers for primitive simulations.
3//!
4//! The crate provides a tiny deterministic mixing function so sibling crates
5//! can derive stable pseudo-random samples without pulling in a full RNG
6//! dependency.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use use_seed::SimulationSeed;
12//!
13//! let seed = SimulationSeed::new(7);
14//! let branches = seed.split(3).unwrap();
15//!
16//! assert_eq!(branches.len(), 3);
17//! assert!(seed.to_unit_f64() >= 0.0);
18//! assert!(seed.to_unit_f64() <= 1.0);
19//! ```
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct SimulationSeed {
23    value: usize,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SeedError {
28    ZeroCount,
29}
30
31impl SimulationSeed {
32    pub const fn new(value: usize) -> Self {
33        Self { value }
34    }
35
36    pub const fn value(self) -> usize {
37        self.value
38    }
39
40    pub fn mix(self, salt: usize) -> Self {
41        let salted = self
42            .value
43            .wrapping_add(salt.wrapping_mul(0x9E37_79B1usize))
44            .wrapping_add(0x85EB_CA6Busize);
45
46        Self {
47            value: mix_value(salted),
48        }
49    }
50
51    pub fn split(self, count: usize) -> Result<Vec<Self>, SeedError> {
52        if count == 0 {
53            return Err(SeedError::ZeroCount);
54        }
55
56        Ok((0..count).map(|index| self.mix(index + 1)).collect())
57    }
58
59    pub fn to_unit_f64(self) -> f64 {
60        mix_value(self.value) as f64 / usize::MAX as f64
61    }
62}
63
64pub fn mix_value(mut value: usize) -> usize {
65    value ^= value >> (usize::BITS / 3);
66    value = value.wrapping_mul(0x85EB_CA6Busize);
67    value ^= value >> (usize::BITS / 4);
68    value = value.wrapping_mul(0xC2B2_AE35usize);
69    value ^= value >> (usize::BITS / 5);
70    value
71}
72
73pub fn unit_f64_from_seed(seed: usize) -> f64 {
74    SimulationSeed::new(seed).to_unit_f64()
75}
76
77#[cfg(test)]
78mod tests {
79    use super::{SeedError, SimulationSeed, mix_value, unit_f64_from_seed};
80
81    #[test]
82    fn mixes_repeatably() {
83        let seed = SimulationSeed::new(42);
84
85        assert_eq!(seed.mix(3), seed.mix(3));
86        assert_ne!(seed.mix(1), seed.mix(2));
87        assert_eq!(mix_value(5), mix_value(5));
88    }
89
90    #[test]
91    fn splits_into_repeatable_branches() {
92        let seed = SimulationSeed::new(9);
93        let branches = seed.split(3).unwrap();
94
95        assert_eq!(branches.len(), 3);
96        assert_eq!(branches[0], seed.mix(1));
97        assert_eq!(branches[2], seed.mix(3));
98    }
99
100    #[test]
101    fn converts_to_unit_interval() {
102        let value = unit_f64_from_seed(7);
103
104        assert!((0.0..=1.0).contains(&value));
105        assert_eq!(value, SimulationSeed::new(7).to_unit_f64());
106    }
107
108    #[test]
109    fn rejects_zero_split_count() {
110        assert_eq!(SimulationSeed::new(1).split(0), Err(SeedError::ZeroCount));
111    }
112}