Skip to main content

des_sim/modeling/sampler/instance/
rotate.rs

1//! The `rotate` module provides the `RotateSampler`, a `DurationSampler`
2//! that cycles through a predefined list of durations.
3//!
4//! This sampler is useful for scenarios requiring a fixed, repeating sequence
5//! of delays or intervals, such as periodic maintenance schedules or
6//! round-robin processing. It maintains internal state to track the current
7//! position in the sequence.
8
9use crate::modeling::sampler::{DurationSampler, PendingDuration};
10use crate::primitive::time::SimTime;
11use rand::Rng;
12
13/// A sampler that returns durations from a fixed list in a cyclic (rotating) order.
14///
15/// # Note
16/// This sampler maintains internal state (`next_index`). If you re-instantiate
17/// this sampler frequently, the sequence will always start from the beginning.
18#[derive(Debug, Clone)]
19pub struct RotateSampler {
20    next_index: usize,
21    list: Vec<PendingDuration>,
22    item_count: usize,
23}
24
25impl DurationSampler for RotateSampler {
26    fn sample(&mut self, _rng: &mut dyn Rng, _current_tick: SimTime) -> PendingDuration {
27        let result = self.list[self.next_index];
28        self.next_index = (self.next_index + 1) % self.item_count;
29        result
30    }
31}
32
33impl RotateSampler {
34    /// Creates a new `RotateSampler` starting from the first element.
35    pub fn new(duration_list: impl IntoIterator<Item = PendingDuration>) -> RotateSampler {
36        let list: Vec<_> = duration_list.into_iter().collect();
37        assert!(
38            !list.is_empty(),
39            "RotateSampler requires at least one duration"
40        );
41
42        let item_count = list.len();
43        RotateSampler {
44            next_index: 0,
45            list,
46            item_count,
47        }
48    }
49
50    /// Creates a new `RotateSampler` starting from a specific index.
51    pub fn new_with_index(
52        duration_list: impl IntoIterator<Item = PendingDuration>,
53        next_index: usize,
54    ) -> RotateSampler {
55        let list: Vec<_> = duration_list.into_iter().collect();
56        assert!(
57            !list.is_empty(),
58            "RotateSampler requires at least one duration"
59        );
60        assert!(next_index < list.len(), "Initial index is out of bounds");
61
62        let item_count = list.len();
63        RotateSampler {
64            next_index,
65            list,
66            item_count,
67        }
68    }
69
70    /// Returns the index of the next duration to be sampled.
71    pub fn peek_next_index(&self) -> usize {
72        self.next_index
73    }
74
75    /// Returns the total number of items in the rotation list.
76    pub fn item_count(&self) -> usize {
77        self.item_count
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::primitive::time::Duration;
85    use rand::SeedableRng;
86    use rand::rngs::SmallRng;
87
88    #[test]
89    fn test_rotate_sampler() {
90        let mut rng = SmallRng::seed_from_u64(2);
91        let durations = vec![
92            Duration::ticks(1).into(),
93            Duration::ticks(2).into(),
94            Duration::ticks(3).into(),
95        ];
96        let mut sampler = RotateSampler::new(durations.clone());
97
98        assert_eq!(sampler.peek_next_index(), 0);
99        assert_eq!(sampler.item_count(), 3);
100
101        assert_eq!(
102            sampler.sample(&mut rng, SimTime::from_ticks(0)).raw_value(),
103            1.0
104        );
105        assert_eq!(sampler.peek_next_index(), 1);
106
107        assert_eq!(
108            sampler.sample(&mut rng, SimTime::from_ticks(0)).raw_value(),
109            2.0
110        );
111        assert_eq!(sampler.peek_next_index(), 2);
112
113        assert_eq!(
114            sampler.sample(&mut rng, SimTime::from_ticks(0)).raw_value(),
115            3.0
116        );
117        assert_eq!(sampler.peek_next_index(), 0); // Wraps around
118
119        assert_eq!(
120            sampler.sample(&mut rng, SimTime::from_ticks(0)).raw_value(),
121            1.0
122        );
123        assert_eq!(sampler.peek_next_index(), 1);
124    }
125
126    #[test]
127    fn test_rotate_sampler_with_initial_index() {
128        let mut rng = SmallRng::seed_from_u64(2);
129        let durations = vec![
130            Duration::ticks(1).into(),
131            Duration::ticks(2).into(),
132            Duration::ticks(3).into(),
133        ];
134        let mut sampler = RotateSampler::new_with_index(durations, 1);
135
136        assert_eq!(sampler.peek_next_index(), 1);
137
138        assert_eq!(
139            sampler.sample(&mut rng, SimTime::from_ticks(0)).raw_value(),
140            2.0
141        );
142        assert_eq!(sampler.peek_next_index(), 2);
143    }
144
145    #[test]
146    #[should_panic(expected = "RotateSampler requires at least one duration")]
147    fn test_rotate_sampler_empty_list1() {
148        let _ = RotateSampler::new(vec![]);
149    }
150
151    #[test]
152    #[should_panic(expected = "RotateSampler requires at least one duration")]
153    fn test_rotate_sampler_empty_list2() {
154        let _ = RotateSampler::new_with_index(vec![], 0);
155    }
156
157    #[test]
158    #[should_panic(expected = "Initial index is out of bounds")]
159    fn test_rotate_sampler_invalid_initial_index() {
160        let durations = vec![Duration::ticks(1).into()];
161        let _ = RotateSampler::new_with_index(durations, 1);
162    }
163}