Skip to main content

des_sim/modeling/sampler/instance/
mode.rs

1//! The `mode` module provides the `ModeSampler`, a `DurationSampler` that
2//! dynamically switches between different sub-samplers based on a `TimeTrigger`.
3//!
4//! This allows for modeling systems whose behavior changes over time, such as
5//! different operating modes or time-of-day effects. The `TimeTrigger` determines
6//! which sampler is active at any given simulation time.
7
8use crate::modeling::sampler::{DurationSampler, PendingDuration};
9use crate::primitive::time::SimTime;
10use rand::Rng;
11
12/// A trait to determine which sampler should be active based on the current simulation time.
13pub trait TimeTrigger {
14    /// Returns the index of the sampler that should be active at the given simulation time.
15    fn get_active_index(&self, now: SimTime) -> usize;
16
17    /// Returns a hint about the maximum possible index to allow for pre-check optimizations.
18    fn max_possible_index_hint(&self) -> usize;
19}
20
21/// A sampler that switches between different sub-samplers based on a [TimeTrigger].
22pub struct ModeSampler<T: TimeTrigger> {
23    trigger: T,
24    samplers: Vec<Box<dyn DurationSampler>>,
25}
26
27impl<T: TimeTrigger> DurationSampler for ModeSampler<T> {
28    fn sample(&mut self, rng: &mut dyn Rng, current_tick: SimTime) -> PendingDuration {
29        let index = self.trigger.get_active_index(current_tick);
30
31        // Boundary check: if out of range, fall back to the first sampler (index 0).
32        if let Some(sampler) = self.samplers.get_mut(index) {
33            sampler.sample(rng, current_tick)
34        } else {
35            log::warn!(
36                "ModeSampler index {} out of bounds, falling back to 0",
37                index
38            );
39            self.samplers[0].sample(rng, current_tick)
40        }
41    }
42}
43
44impl<T: TimeTrigger> ModeSampler<T> {
45    /// Creates a new `ModeSampler`.
46    ///
47    /// # Panics
48    /// Panics if the provided samplers collection is empty.
49    pub fn new(trigger: T, samplers: impl IntoIterator<Item = Box<dyn DurationSampler>>) -> Self {
50        let samplers: Vec<_> = samplers.into_iter().collect();
51
52        assert!(
53            !samplers.is_empty(),
54            "ModeSampler requires at least one sampler"
55        );
56        debug_assert!(
57            trigger.max_possible_index_hint() < samplers.len(),
58            "ModeSampler trigger hint exceeds sampler collection bounds"
59        );
60
61        Self { trigger, samplers }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::modeling::sampler::CombinatorExt;
69    use crate::modeling::sampler::instance::ConstantSampler;
70    use rand::SeedableRng;
71    use rand::rngs::SmallRng;
72
73    // Mock TimeTrigger for testing ModeSampler
74    struct MockTimeTrigger {
75        active_index: usize,
76        max_index_hint: usize,
77    }
78
79    impl TimeTrigger for MockTimeTrigger {
80        fn get_active_index(&self, _now: SimTime) -> usize {
81            self.active_index
82        }
83
84        fn max_possible_index_hint(&self) -> usize {
85            self.max_index_hint
86        }
87    }
88
89    #[test]
90    fn test_mode_sampler_valid_index() {
91        let mut rng = SmallRng::seed_from_u64(2);
92        let s1 = ConstantSampler::new(10.0);
93        let s2 = ConstantSampler::new(20.0);
94        let s3 = ConstantSampler::new(30.0);
95
96        let trigger = MockTimeTrigger {
97            active_index: 1,
98            max_index_hint: 2,
99        };
100        let mut sampler = ModeSampler::new(trigger, vec![s1.boxed(), s2.boxed(), s3.boxed()]);
101
102        let sample = sampler.sample(&mut rng, SimTime::from_ticks(0));
103        assert_eq!(sample.raw_value(), 20.0); // Should sample from s2
104    }
105
106    #[test]
107    fn test_mode_sampler_out_of_bounds_fallback() {
108        let mut rng = SmallRng::seed_from_u64(2);
109        let s1 = ConstantSampler::new(10.0);
110        let s2 = ConstantSampler::new(20.0);
111
112        let trigger = MockTimeTrigger {
113            active_index: 5, // Out of bounds
114            max_index_hint: 1,
115        };
116        let mut sampler = ModeSampler::new(trigger, vec![s1.boxed(), s2.boxed()]);
117
118        let sample = sampler.sample(&mut rng, SimTime::from_ticks(0));
119        assert_eq!(sample.raw_value(), 10.0); // Should fall back to s1
120    }
121
122    #[test]
123    #[should_panic(expected = "ModeSampler requires at least one sampler")]
124    fn test_mode_sampler_empty_samplers() {
125        let trigger = MockTimeTrigger {
126            active_index: 0,
127            max_index_hint: 0,
128        };
129        let _sampler = ModeSampler::new(trigger, vec![]);
130    }
131}