1use rustsim_core::types::AgentId;
8use rustsim_spaces::link::{LinkId, LinkSpace, LinkSpaceError};
9
10use crate::{LinkProperties, SignalPhase, SignalTiming, TrafficControlType, TransportLinkOps};
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum SpeedConstraint {
15 Unconstrained,
17 Leader {
19 leader: AgentId,
21 gap_m: f64,
23 },
24 BlockedExit {
26 remaining_m: f64,
28 },
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct SpeedDecision {
34 pub speed: f64,
36 pub constraint: SpeedConstraint,
38}
39
40pub trait QueuePolicy {
42 fn speed_for(
44 &self,
45 space: &LinkSpace<LinkProperties>,
46 agent: AgentId,
47 ) -> Result<SpeedDecision, LinkSpaceError>;
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct FifoGapPolicy {
53 pub min_gap_m: f64,
55 pub exit_clearance_m: f64,
57}
58
59impl Default for FifoGapPolicy {
60 fn default() -> Self {
61 Self {
62 min_gap_m: 5.0,
63 exit_clearance_m: 0.5,
64 }
65 }
66}
67
68impl FifoGapPolicy {
69 pub fn new(min_gap_m: f64, exit_clearance_m: f64) -> Self {
71 Self {
72 min_gap_m,
73 exit_clearance_m,
74 }
75 }
76}
77
78impl QueuePolicy for FifoGapPolicy {
79 fn speed_for(
80 &self,
81 space: &LinkSpace<LinkProperties>,
82 agent: AgentId,
83 ) -> Result<SpeedDecision, LinkSpaceError> {
84 let (link_id, position) = space
85 .agent_position(agent)
86 .ok_or(LinkSpaceError::AgentNotFound(agent))?;
87 let link_speed = space.link_speed(link_id);
88 let ids = space.agent_ids_on_link(link_id);
89 let my_idx = ids.iter().position(|&id| id == agent).expect(
90 "invariant: agent_position returned link_id whose agent list must contain the agent",
91 );
92 let min_gap_m = self.min_gap_m.max(0.0);
93 let exit_clearance_m = self.exit_clearance_m.max(0.0);
94
95 if my_idx + 1 < ids.len() {
96 let leader = ids[my_idx + 1];
97 let leader_pos = space
98 .agent_position(leader)
99 .expect("invariant: leader came from the same link's agent list and must resolve")
100 .1;
101 let gap_m = leader_pos - position;
102 let safe_speed = (gap_m - min_gap_m).max(0.0);
103 return Ok(SpeedDecision {
104 speed: link_speed.min(safe_speed),
105 constraint: SpeedConstraint::Leader { leader, gap_m },
106 });
107 }
108
109 if space.link_exit_blocked(link_id) {
110 let remaining_m = space.link_length(link_id).unwrap_or(0.0) - position;
111 let safe_speed = (remaining_m - exit_clearance_m).max(0.0);
112 return Ok(SpeedDecision {
113 speed: link_speed.min(safe_speed),
114 constraint: SpeedConstraint::BlockedExit { remaining_m },
115 });
116 }
117
118 Ok(SpeedDecision {
119 speed: link_speed,
120 constraint: SpeedConstraint::Unconstrained,
121 })
122 }
123}
124
125#[derive(Debug, Clone, Copy)]
127pub struct ControlContext<'a> {
128 pub sim_time_s: f64,
130 pub traffic_control: TrafficControlType,
132 pub signal_timing: Option<&'a SignalTiming>,
134 pub approach_link: Option<LinkId>,
136}
137
138impl<'a> ControlContext<'a> {
139 pub fn new(sim_time_s: f64, traffic_control: TrafficControlType) -> Self {
141 Self {
142 sim_time_s,
143 traffic_control,
144 signal_timing: None,
145 approach_link: None,
146 }
147 }
148
149 pub fn with_signal_timing(mut self, signal_timing: &'a SignalTiming) -> Self {
151 self.signal_timing = Some(signal_timing);
152 self
153 }
154
155 pub fn with_approach_link(mut self, approach_link: LinkId) -> Self {
157 self.approach_link = Some(approach_link);
158 self
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq)]
164pub enum ControlDecision {
165 Proceed,
167 Hold {
169 wait_s: f64,
171 },
172}
173
174impl ControlDecision {
175 pub fn can_proceed(self) -> bool {
177 matches!(self, Self::Proceed)
178 }
179
180 pub fn wait_s(self) -> f64 {
182 match self {
183 Self::Proceed => 0.0,
184 Self::Hold { wait_s } => wait_s,
185 }
186 }
187}
188
189pub trait ControlPolicy {
191 fn decide(&self, context: ControlContext<'_>) -> ControlDecision;
193}
194
195#[derive(Debug, Clone, Copy, PartialEq)]
197pub struct FixedControlPolicy {
198 pub stop_delay_s: f64,
200 pub yield_delay_s: f64,
202}
203
204impl Default for FixedControlPolicy {
205 fn default() -> Self {
206 Self {
207 stop_delay_s: 0.0,
208 yield_delay_s: 0.0,
209 }
210 }
211}
212
213impl FixedControlPolicy {
214 pub fn new(stop_delay_s: f64, yield_delay_s: f64) -> Self {
216 Self {
217 stop_delay_s,
218 yield_delay_s,
219 }
220 }
221}
222
223impl ControlPolicy for FixedControlPolicy {
224 fn decide(&self, context: ControlContext<'_>) -> ControlDecision {
225 match context.traffic_control {
226 TrafficControlType::Uncontrolled => ControlDecision::Proceed,
227 TrafficControlType::Yield => hold_or_proceed(self.yield_delay_s),
228 TrafficControlType::Stop => hold_or_proceed(self.stop_delay_s),
229 TrafficControlType::Signal => match context.signal_timing {
230 Some(timing) => {
231 let (phase, remaining_s) = timing.phase_at(context.sim_time_s);
232 match phase {
233 SignalPhase::Green => ControlDecision::Proceed,
234 SignalPhase::Red => ControlDecision::Hold {
235 wait_s: remaining_s.max(0.0),
236 },
237 }
238 }
239 None => ControlDecision::Proceed,
240 },
241 }
242 }
243}
244
245fn hold_or_proceed(delay_s: f64) -> ControlDecision {
246 if delay_s > 0.0 {
247 ControlDecision::Hold { wait_s: delay_s }
248 } else {
249 ControlDecision::Proceed
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::LinkProperties;
257
258 fn one_link_space() -> (LinkSpace<LinkProperties>, LinkId) {
259 let mut space = LinkSpace::new();
260 let a = space.add_node();
261 let b = space.add_node();
262 let (geom, props) = LinkProperties::urban(100.0, 36.0, 1).unwrap();
263 let link = space.add_link(a, b, geom, props).unwrap();
264 (space, link)
265 }
266
267 #[test]
268 fn fifo_gap_policy_limits_trailing_agent_by_leader_gap() {
269 let (mut space, link) = one_link_space();
270 space.add_agent_to_link(1, link, 10.0).unwrap();
271 space.add_agent_to_link(2, link, 18.0).unwrap();
272
273 let decision = FifoGapPolicy::default().speed_for(&space, 1).unwrap();
274
275 assert_eq!(decision.speed, 3.0);
276 assert_eq!(
277 decision.constraint,
278 SpeedConstraint::Leader {
279 leader: 2,
280 gap_m: 8.0
281 }
282 );
283 }
284
285 #[test]
286 fn default_transport_agent_speed_uses_fifo_gap_policy() {
287 let (mut space, link) = one_link_space();
288 space.add_agent_to_link(1, link, 10.0).unwrap();
289 space.add_agent_to_link(2, link, 18.0).unwrap();
290
291 assert_eq!(space.agent_speed(1).unwrap(), 3.0);
292 assert_eq!(
293 space
294 .agent_speed_decision(1, &FifoGapPolicy::default())
295 .unwrap()
296 .speed,
297 3.0
298 );
299 }
300
301 #[test]
302 fn blocked_exit_policy_limits_last_agent() {
303 let (mut space, link) = one_link_space();
304 space.add_agent_to_link(1, link, 98.0).unwrap();
305 space.set_link_exit_blocked(link, true);
306
307 let decision = FifoGapPolicy::default().speed_for(&space, 1).unwrap();
308
309 assert_eq!(decision.speed, 1.5);
310 assert_eq!(
311 decision.constraint,
312 SpeedConstraint::BlockedExit { remaining_m: 2.0 }
313 );
314 }
315
316 #[test]
317 fn fixed_control_policy_blocks_red_signal_and_releases_green() {
318 let policy = FixedControlPolicy::default();
319 let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
320
321 let red = policy.decide(
322 ControlContext::new(45.0, TrafficControlType::Signal).with_signal_timing(&timing),
323 );
324 assert_eq!(red, ControlDecision::Hold { wait_s: 15.0 });
325
326 let green = policy.decide(
327 ControlContext::new(5.0, TrafficControlType::Signal).with_signal_timing(&timing),
328 );
329 assert_eq!(green, ControlDecision::Proceed);
330 }
331}