proxima_primitives/pipe/
when.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
19pub struct When {
20 pub prob: f64,
22 pub seed: u64,
24}
25
26impl When {
27 #[must_use]
29 pub fn prob(probability: f64) -> Self {
30 Self {
31 prob: probability.clamp(0.0, 1.0),
32 seed: 0,
33 }
34 }
35
36 #[must_use]
38 pub fn seed(mut self, seed: u64) -> Self {
39 self.seed = seed;
40 self
41 }
42
43 #[must_use]
47 pub fn fires(&self, call_index: u64) -> bool {
48 if self.prob <= 0.0 {
49 return false;
50 }
51 if self.prob >= 1.0 {
52 return true;
53 }
54 let mut rng = fastrand::Rng::with_seed(self.seed.wrapping_add(call_index));
55 rng.f64() < self.prob
56 }
57}
58
59#[cfg(test)]
60#[allow(clippy::unwrap_used, clippy::expect_used)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn fire_sequence_is_deterministic_across_runs() {
66 let gate = When::prob(0.5).seed(0xABCD_1234);
67 let first: Vec<bool> = (0..512).map(|index| gate.fires(index)).collect();
68 let second: Vec<bool> = (0..512).map(|index| gate.fires(index)).collect();
69 assert_eq!(
70 first, second,
71 "same (seed, prob) yields an identical fire sequence"
72 );
73 }
74
75 #[test]
76 fn prob_zero_never_fires_and_prob_one_always_fires() {
77 let never = When::prob(0.0).seed(7);
78 let always = When::prob(1.0).seed(7);
79 for index in 0..1_000 {
80 assert!(!never.fires(index), "prob 0.0 must never fire");
81 assert!(always.fires(index), "prob 1.0 must always fire");
82 }
83 }
84
85 #[test]
86 fn prob_is_clamped_into_unit_interval() {
87 assert_eq!(
88 When::prob(-3.0).prob,
89 0.0,
90 "negative probability clamps to 0"
91 );
92 assert_eq!(
93 When::prob(42.0).prob,
94 1.0,
95 "above-one probability clamps to 1"
96 );
97 }
98
99 #[test]
100 fn distinct_seeds_diverge() {
101 let left = When::prob(0.5).seed(1);
102 let right = When::prob(0.5).seed(2);
103 let diverged = (0..512).any(|index| left.fires(index) != right.fires(index));
104 assert!(diverged, "different seeds must produce different sequences");
105 }
106
107 #[test]
108 fn empirical_fire_rate_tracks_probability() {
109 let gate = When::prob(0.25).seed(99);
110 let fired = (0..10_000_u64).filter(|&index| gate.fires(index)).count();
111 let rate = fired as f64 / 10_000.0;
112 assert!(
113 (rate - 0.25).abs() < 0.05,
114 "observed rate {rate} should track prob 0.25"
115 );
116 }
117}