Skip to main content

holodeck_lib/
seed.rs

1//! Deterministic seed computation for reproducible simulations.
2//!
3//! When no explicit `--seed` is provided, a seed is derived from the
4//! simulation parameters so that identical inputs always produce identical
5//! output across runs and processes.
6
7/// Compute a deterministic seed by hashing a string description of the
8/// simulation parameters.
9///
10/// Uses FNV-1a hashing which is deterministic across runs (unlike `ahash`
11/// which uses per-process random keys). The same input always produces the
12/// same seed.
13#[must_use]
14pub fn compute_seed(description: &str) -> u64 {
15    // FNV-1a hash — deterministic, fast, no external dependencies.
16    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
17    for byte in description.bytes() {
18        hash ^= u64::from(byte);
19        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
20    }
21    hash
22}
23
24/// Resolve the effective seed: use the explicit seed if provided, otherwise
25/// derive one from the given parameter description string.
26#[must_use]
27pub fn resolve_seed(explicit: Option<u64>, description: &str) -> u64 {
28    explicit.unwrap_or_else(|| compute_seed(description))
29}
30
31/// Derive a deterministic sub-seed scoped to a named namespace (e.g. a
32/// contig name) from a parent seed. Used to produce independent, reproducible
33/// RNG streams for per-contig work (reference normalization, etc.) without
34/// disturbing the main simulation RNG.
35#[must_use]
36pub fn derive_seed(parent: u64, namespace: &str) -> u64 {
37    compute_seed(&format!("{parent:016x}:{namespace}"))
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn test_compute_seed_deterministic() {
46        let a = compute_seed("hello:42:3.14");
47        let b = compute_seed("hello:42:3.14");
48        assert_eq!(a, b);
49    }
50
51    #[test]
52    fn test_compute_seed_known_value() {
53        // Pin a known value so we detect if the hash implementation changes.
54        let seed = compute_seed("test");
55        assert_eq!(seed, 0xf9e6_e6ef_197c_2b25);
56    }
57
58    #[test]
59    fn test_compute_seed_different_inputs() {
60        let a = compute_seed("hello");
61        let b = compute_seed("world");
62        assert_ne!(a, b);
63    }
64
65    #[test]
66    fn test_resolve_seed_explicit() {
67        let seed = resolve_seed(Some(42), "ignored");
68        assert_eq!(seed, 42);
69    }
70
71    #[test]
72    fn test_resolve_seed_derived() {
73        let seed = resolve_seed(None, "hello");
74        let expected = compute_seed("hello");
75        assert_eq!(seed, expected);
76    }
77
78    #[test]
79    fn test_derive_seed_deterministic() {
80        let a = derive_seed(42, "chr1");
81        let b = derive_seed(42, "chr1");
82        assert_eq!(a, b);
83    }
84
85    #[test]
86    fn test_derive_seed_varies_with_namespace() {
87        let a = derive_seed(42, "chr1");
88        let b = derive_seed(42, "chr2");
89        assert_ne!(a, b);
90    }
91
92    #[test]
93    fn test_derive_seed_varies_with_parent() {
94        let a = derive_seed(1, "chr1");
95        let b = derive_seed(2, "chr1");
96        assert_ne!(a, b);
97    }
98}