Skip to main content

mscore/simulation/
noise_rng.rs

1//! Deterministic RNG streams for the simulation's noise, keyed by identity rather than by
2//! scheduling order.
3//!
4//! The m/z jitter and the reference-noise overlay used `rand::thread_rng`, so which thread picked
5//! up a spectrum decided its noise. That makes a run irreproducible, and it is not a cosmetic
6//! problem: a jittered m/z can cross a TOF bin boundary, which changes the `(scan, tof)` dedup and
7//! so the peaks written to the file.
8//!
9//! Deriving the stream from a master seed plus the stable identifiers already in scope — frame id,
10//! peptide id, ion index, scan — makes the draw independent of thread count and scheduling.
11
12use rand::rngs::StdRng;
13use rand::SeedableRng;
14
15/// Golden-ratio odd constant, the usual choice for cheap integer mixing.
16const MIX: u64 = 0x9E37_79B9_7F4A_7C15;
17
18/// Mix a master seed and a list of identifiers into one 64-bit stream key.
19///
20/// Order matters: `[frame, peptide]` and `[peptide, frame]` give different streams, which is what
21/// you want, since the components are not interchangeable.
22#[inline]
23pub fn stream_key(seed: u64, keys: &[u64]) -> u64 {
24    let mut h = seed ^ MIX;
25    for (i, &k) in keys.iter().enumerate() {
26        // SplitMix64-style finaliser per component, position-dependent so permutations differ.
27        let mut x = k.wrapping_add(MIX.wrapping_mul(i as u64 + 1));
28        x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
29        x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
30        x ^= x >> 31;
31        h = (h ^ x).wrapping_mul(MIX);
32    }
33    h
34}
35
36/// A reproducible RNG for one unit of noise, keyed by `seed` and `keys`.
37#[inline]
38pub fn noise_rng(seed: u64, keys: &[u64]) -> StdRng {
39    StdRng::seed_from_u64(stream_key(seed, keys))
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use rand::Rng;
46
47    fn draw(seed: u64, keys: &[u64]) -> u64 {
48        noise_rng(seed, keys).gen()
49    }
50
51    #[test]
52    fn same_inputs_give_the_same_stream() {
53        assert_eq!(draw(41, &[7, 3, 1]), draw(41, &[7, 3, 1]));
54    }
55
56    #[test]
57    fn the_seed_matters() {
58        assert_ne!(draw(41, &[7, 3, 1]), draw(42, &[7, 3, 1]));
59    }
60
61    #[test]
62    fn each_key_component_matters() {
63        let base = draw(41, &[7, 3, 1]);
64        assert_ne!(base, draw(41, &[8, 3, 1]));
65        assert_ne!(base, draw(41, &[7, 4, 1]));
66        assert_ne!(base, draw(41, &[7, 3, 2]));
67    }
68
69    #[test]
70    fn key_order_matters() {
71        assert_ne!(draw(41, &[1, 2]), draw(41, &[2, 1]));
72    }
73
74    #[test]
75    fn neighbouring_keys_do_not_collide() {
76        // Frame/scan ids are dense and small; adjacent ones must not share a stream.
77        let mut seen = std::collections::HashSet::new();
78        for frame in 0..200u64 {
79            for scan in 0..50u64 {
80                assert!(seen.insert(stream_key(41, &[frame, scan])), "collision at {frame}/{scan}");
81            }
82        }
83    }
84}