Skip to main content

mabi_scenario/
generator.rs

1//! Pattern generators for scenario simulation.
2
3use rand::prelude::*;
4use rand_distr::{Normal, Uniform};
5
6use crate::schema::PatternConfig;
7
8/// Pattern type enumeration.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PatternType {
11    Constant,
12    Sine,
13    Cosine,
14    Ramp,
15    Step,
16    Random,
17    Noise,
18    Follow,
19    Replay,
20}
21
22/// Pattern generator.
23pub struct PatternGenerator {
24    config: PatternConfig,
25    rng: StdRng,
26    start_time: std::time::Instant,
27    last_value: f64,
28}
29
30impl PatternGenerator {
31    /// Create a new pattern generator.
32    pub fn new(config: PatternConfig) -> Self {
33        Self {
34            config,
35            rng: StdRng::from_entropy(),
36            start_time: std::time::Instant::now(),
37            last_value: 0.0,
38        }
39    }
40
41    /// Reset the generator.
42    pub fn reset(&mut self) {
43        self.start_time = std::time::Instant::now();
44        self.last_value = 0.0;
45    }
46
47    /// Generate the next value.
48    pub fn generate(&mut self) -> f64 {
49        let elapsed = self.start_time.elapsed().as_secs_f64();
50        self.generate_at(elapsed)
51    }
52
53    /// Generate value at specific time.
54    pub fn generate_at(&mut self, time_secs: f64) -> f64 {
55        let value = match &self.config {
56            PatternConfig::Constant { value } => *value,
57
58            PatternConfig::Sine {
59                amplitude,
60                offset,
61                period_secs,
62                phase,
63            } => {
64                let angle = (2.0 * std::f64::consts::PI * time_secs / period_secs) + phase;
65                offset + amplitude * angle.sin()
66            }
67
68            PatternConfig::Cosine {
69                amplitude,
70                offset,
71                period_secs,
72                phase,
73            } => {
74                let angle = (2.0 * std::f64::consts::PI * time_secs / period_secs) + phase;
75                offset + amplitude * angle.cos()
76            }
77
78            PatternConfig::Ramp {
79                start,
80                end,
81                duration_secs,
82                repeat,
83            } => {
84                let t = if *repeat {
85                    (time_secs % duration_secs) / duration_secs
86                } else {
87                    (time_secs / duration_secs).min(1.0)
88                };
89                start + (end - start) * t
90            }
91
92            PatternConfig::Step {
93                levels,
94                step_duration_secs,
95            } => {
96                if levels.is_empty() {
97                    0.0
98                } else {
99                    let total_duration = step_duration_secs * levels.len() as f64;
100                    let t = time_secs % total_duration;
101                    let index = (t / step_duration_secs) as usize;
102                    levels[index.min(levels.len() - 1)]
103                }
104            }
105
106            PatternConfig::Random {
107                min,
108                max,
109                distribution,
110            } => {
111                match distribution.as_str() {
112                    "uniform" => {
113                        let dist = Uniform::new(*min, *max);
114                        dist.sample(&mut self.rng)
115                    }
116                    "normal" | "gaussian" => {
117                        let mean = (min + max) / 2.0;
118                        let std_dev = (max - min) / 6.0; // 99.7% within range
119                        let dist = Normal::new(mean, std_dev).unwrap();
120                        dist.sample(&mut self.rng).clamp(*min, *max)
121                    }
122                    _ => {
123                        let dist = Uniform::new(*min, *max);
124                        dist.sample(&mut self.rng)
125                    }
126                }
127            }
128
129            PatternConfig::Noise { mean, std_dev } => {
130                let dist = Normal::new(*mean, *std_dev).unwrap();
131                dist.sample(&mut self.rng)
132            }
133
134            PatternConfig::Follow { offset, gain, .. } => {
135                // Follow requires external source value
136                self.last_value * gain + offset
137            }
138
139            PatternConfig::Replay { .. } => {
140                // Replay requires external data
141                self.last_value
142            }
143        };
144
145        self.last_value = value;
146        value
147    }
148
149    /// Set source value for Follow pattern.
150    pub fn set_source_value(&mut self, value: f64) {
151        self.last_value = value;
152    }
153
154    /// Get pattern type.
155    pub fn pattern_type(&self) -> PatternType {
156        match &self.config {
157            PatternConfig::Constant { .. } => PatternType::Constant,
158            PatternConfig::Sine { .. } => PatternType::Sine,
159            PatternConfig::Cosine { .. } => PatternType::Cosine,
160            PatternConfig::Ramp { .. } => PatternType::Ramp,
161            PatternConfig::Step { .. } => PatternType::Step,
162            PatternConfig::Random { .. } => PatternType::Random,
163            PatternConfig::Noise { .. } => PatternType::Noise,
164            PatternConfig::Follow { .. } => PatternType::Follow,
165            PatternConfig::Replay { .. } => PatternType::Replay,
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_constant_pattern() {
176        let mut gen = PatternGenerator::new(PatternConfig::Constant { value: 42.0 });
177        assert!((gen.generate() - 42.0).abs() < 0.001);
178    }
179
180    #[test]
181    fn test_sine_pattern() {
182        let mut gen = PatternGenerator::new(PatternConfig::Sine {
183            amplitude: 10.0,
184            offset: 20.0,
185            period_secs: 4.0,
186            phase: 0.0,
187        });
188
189        // At t=0, sin(0) = 0, so value = 20
190        let v0 = gen.generate_at(0.0);
191        assert!((v0 - 20.0).abs() < 0.001);
192
193        // At t=1 (quarter period), sin(π/2) = 1, so value = 30
194        let v1 = gen.generate_at(1.0);
195        assert!((v1 - 30.0).abs() < 0.001);
196    }
197
198    #[test]
199    fn test_ramp_pattern() {
200        let mut gen = PatternGenerator::new(PatternConfig::Ramp {
201            start: 0.0,
202            end: 100.0,
203            duration_secs: 10.0,
204            repeat: false,
205        });
206
207        assert!((gen.generate_at(0.0) - 0.0).abs() < 0.001);
208        assert!((gen.generate_at(5.0) - 50.0).abs() < 0.001);
209        assert!((gen.generate_at(10.0) - 100.0).abs() < 0.001);
210        assert!((gen.generate_at(15.0) - 100.0).abs() < 0.001); // Clamped
211    }
212
213    #[test]
214    fn test_step_pattern() {
215        let mut gen = PatternGenerator::new(PatternConfig::Step {
216            levels: vec![10.0, 20.0, 30.0],
217            step_duration_secs: 1.0,
218        });
219
220        assert!((gen.generate_at(0.5) - 10.0).abs() < 0.001);
221        assert!((gen.generate_at(1.5) - 20.0).abs() < 0.001);
222        assert!((gen.generate_at(2.5) - 30.0).abs() < 0.001);
223    }
224
225    #[test]
226    fn test_random_pattern() {
227        let mut gen = PatternGenerator::new(PatternConfig::Random {
228            min: 0.0,
229            max: 100.0,
230            distribution: "uniform".to_string(),
231        });
232
233        for _ in 0..100 {
234            let value = gen.generate();
235            assert!(value >= 0.0 && value <= 100.0);
236        }
237    }
238}