Skip to main content

rustsim_traffic/
policy.rs

1//! Explicit transport queue and control policies.
2//!
3//! These policies lift the small built-in link behavior into named,
4//! configurable contracts. The default queue policy preserves the historical
5//! FIFO/gap-limited behavior used by [`TransportLinkOps`](crate::TransportLinkOps).
6
7use rustsim_core::types::AgentId;
8use rustsim_spaces::link::{LinkId, LinkSpace, LinkSpaceError};
9
10use crate::{LinkProperties, SignalPhase, SignalTiming, TrafficControlType, TransportLinkOps};
11
12/// Why a policy constrained an agent's speed.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum SpeedConstraint {
15    /// No downstream or leader constraint applied.
16    Unconstrained,
17    /// A leading agent capped this agent's speed.
18    Leader {
19        /// Leading agent identifier.
20        leader: AgentId,
21        /// Current bumper-to-bumper gap used by the policy, in meters.
22        gap_m: f64,
23    },
24    /// The downstream end of the link is blocked.
25    BlockedExit {
26        /// Remaining distance to the link end, in meters.
27        remaining_m: f64,
28    },
29}
30
31/// Speed chosen by a queue policy.
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct SpeedDecision {
34    /// Selected speed in meters per second.
35    pub speed: f64,
36    /// Constraint that determined the selected speed.
37    pub constraint: SpeedConstraint,
38}
39
40/// Policy for choosing an agent's link speed from ordered occupancy.
41pub trait QueuePolicy {
42    /// Compute the speed decision for `agent` in `space`.
43    fn speed_for(
44        &self,
45        space: &LinkSpace<LinkProperties>,
46        agent: AgentId,
47    ) -> Result<SpeedDecision, LinkSpaceError>;
48}
49
50/// FIFO policy with a fixed gap to the leader and fixed exit clearance.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct FifoGapPolicy {
53    /// Minimum gap maintained behind a leading agent, in meters.
54    pub min_gap_m: f64,
55    /// Clearance maintained before a blocked downstream exit, in meters.
56    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    /// Create a FIFO/gap policy with explicit distances in meters.
70    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/// Signal/control context for an approach or movement.
126#[derive(Debug, Clone, Copy)]
127pub struct ControlContext<'a> {
128    /// Simulation time in seconds.
129    pub sim_time_s: f64,
130    /// Coarse traffic-control type for the movement.
131    pub traffic_control: TrafficControlType,
132    /// Optional fixed-time signal timing when `traffic_control` is signalized.
133    pub signal_timing: Option<&'a SignalTiming>,
134    /// Optional approach link identifier for downstream telemetry.
135    pub approach_link: Option<LinkId>,
136}
137
138impl<'a> ControlContext<'a> {
139    /// Create a control context at `sim_time_s`.
140    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    /// Attach fixed-time signal timing.
150    pub fn with_signal_timing(mut self, signal_timing: &'a SignalTiming) -> Self {
151        self.signal_timing = Some(signal_timing);
152        self
153    }
154
155    /// Attach an approach link identifier.
156    pub fn with_approach_link(mut self, approach_link: LinkId) -> Self {
157        self.approach_link = Some(approach_link);
158        self
159    }
160}
161
162/// Control decision for a movement.
163#[derive(Debug, Clone, Copy, PartialEq)]
164pub enum ControlDecision {
165    /// The movement may proceed now.
166    Proceed,
167    /// The movement must wait for the supplied duration.
168    Hold {
169        /// Time remaining before the movement may be reconsidered, in seconds.
170        wait_s: f64,
171    },
172}
173
174impl ControlDecision {
175    /// True if the decision allows immediate movement.
176    pub fn can_proceed(self) -> bool {
177        matches!(self, Self::Proceed)
178    }
179
180    /// Waiting time implied by this decision, in seconds.
181    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
189/// Policy that decides whether a movement may proceed through a control point.
190pub trait ControlPolicy {
191    /// Evaluate a movement under the supplied context.
192    fn decide(&self, context: ControlContext<'_>) -> ControlDecision;
193}
194
195/// Fixed-rule control policy for uncontrolled, yield, stop, and signal controls.
196#[derive(Debug, Clone, Copy, PartialEq)]
197pub struct FixedControlPolicy {
198    /// Delay applied to stop-controlled movements before they may proceed.
199    pub stop_delay_s: f64,
200    /// Delay applied to yield-controlled movements before they may proceed.
201    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    /// Create a fixed-rule control policy.
215    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}