rustsim_traffic/
signal.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum SignalPhase {
37 Green,
39 Red,
41}
42
43#[derive(Debug, Clone, PartialEq)]
49pub struct SignalTiming {
50 pub cycle_s: f64,
52 pub offset_s: f64,
54 pub green_phases_s: Vec<f64>,
57}
58
59impl SignalTiming {
60 pub fn new(cycle_s: f64, offset_s: f64, green_phases_s: Vec<f64>) -> Self {
63 Self {
64 cycle_s,
65 offset_s,
66 green_phases_s,
67 }
68 }
69
70 pub fn phase_at(&self, sim_time_s: f64) -> (SignalPhase, f64) {
79 if self.cycle_s <= 0.0 {
80 return (SignalPhase::Green, 0.0);
81 }
82 let total_green: f64 = self.green_phases_s.iter().sum();
83 if total_green <= 0.0 {
84 return (SignalPhase::Red, self.cycle_s);
85 }
86 if total_green >= self.cycle_s {
87 return (SignalPhase::Green, 0.0);
88 }
89
90 let effective = ((sim_time_s - self.offset_s) % self.cycle_s + self.cycle_s) % self.cycle_s;
91
92 if effective < total_green {
93 let remaining_green = total_green - effective;
94 (SignalPhase::Green, remaining_green)
95 } else {
96 let remaining_red = self.cycle_s - effective;
97 (SignalPhase::Red, remaining_red)
98 }
99 }
100
101 pub fn total_green_s(&self) -> f64 {
103 self.green_phases_s.iter().sum()
104 }
105
106 pub fn total_red_s(&self) -> f64 {
108 (self.cycle_s - self.total_green_s()).max(0.0)
109 }
110
111 pub fn green_ratio(&self) -> f64 {
113 if self.cycle_s <= 0.0 {
114 0.0
115 } else {
116 (self.total_green_s() / self.cycle_s).clamp(0.0, 1.0)
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn green_at_start_of_cycle() {
127 let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
128 let (phase, remaining) = timing.phase_at(0.0);
129 assert_eq!(phase, SignalPhase::Green);
130 assert!((remaining - 30.0).abs() < 1e-6);
131 }
132
133 #[test]
134 fn red_in_second_half() {
135 let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
136 let (phase, remaining) = timing.phase_at(45.0);
137 assert_eq!(phase, SignalPhase::Red);
138 assert!((remaining - 15.0).abs() < 1e-6);
139 }
140
141 #[test]
142 fn wraps_around_cycle() {
143 let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
144 let (phase, _) = timing.phase_at(65.0);
146 assert_eq!(phase, SignalPhase::Green);
147 let (phase, _) = timing.phase_at(95.0);
149 assert_eq!(phase, SignalPhase::Red);
150 }
151
152 #[test]
153 fn offset_shifts_phase() {
154 let timing = SignalTiming::new(60.0, 10.0, vec![30.0]);
155 let (phase, _) = timing.phase_at(10.0);
157 assert_eq!(phase, SignalPhase::Green);
158 let (phase, _) = timing.phase_at(5.0);
160 assert_eq!(phase, SignalPhase::Red);
161 }
162
163 #[test]
164 fn zero_cycle_always_green() {
165 let timing = SignalTiming::new(0.0, 0.0, vec![30.0]);
166 let (phase, _) = timing.phase_at(100.0);
167 assert_eq!(phase, SignalPhase::Green);
168 }
169
170 #[test]
171 fn all_green_cycle() {
172 let timing = SignalTiming::new(60.0, 0.0, vec![60.0]);
173 for t in [0.0, 30.0, 59.9] {
174 let (phase, _) = timing.phase_at(t);
175 assert_eq!(phase, SignalPhase::Green);
176 }
177 }
178
179 #[test]
180 fn zero_green_always_red() {
181 let timing = SignalTiming::new(60.0, 0.0, vec![]);
182 let (phase, _) = timing.phase_at(0.0);
183 assert_eq!(phase, SignalPhase::Red);
184 }
185
186 #[test]
187 fn multiple_green_phases_summed() {
188 let timing = SignalTiming::new(60.0, 0.0, vec![10.0, 10.0]);
189 let (phase, _) = timing.phase_at(15.0);
191 assert_eq!(phase, SignalPhase::Green);
192 let (phase, _) = timing.phase_at(25.0);
194 assert_eq!(phase, SignalPhase::Red);
195 }
196
197 #[test]
198 fn green_ratio_computed_correctly() {
199 let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
200 assert!((timing.green_ratio() - 0.5).abs() < 1e-6);
201 assert!((timing.total_red_s() - 30.0).abs() < 1e-6);
202 }
203}