Skip to main content

rustsim_transit/
policy.rs

1//! Transit queue, dispatch, and dwell policies.
2
3use crate::boarding::{Boarding, BoardingResult, Waiter};
4use crate::dwell::{self, DwellParams, DwellTime};
5use crate::route::Route;
6use crate::stop::{Stop, StopId};
7use crate::vehicle::TransitVehicle;
8
9/// Policy for admitting passengers to a stop queue.
10pub trait StopQueuePolicy {
11    /// Return `true` if `waiter` may join the stop queue.
12    fn can_enqueue(&self, stop: &Stop, queue: &Boarding, waiter: &Waiter) -> bool;
13}
14
15/// Stop queue policy that enforces the stop's soft capacity as a hard cap.
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
17pub struct CapacityStopQueuePolicy;
18
19impl StopQueuePolicy for CapacityStopQueuePolicy {
20    fn can_enqueue(&self, stop: &Stop, queue: &Boarding, _waiter: &Waiter) -> bool {
21        queue.len() < stop.capacity as usize
22    }
23}
24
25/// Policy for deciding which queued passengers board a vehicle.
26pub trait BoardingPolicy {
27    /// Maximum number of passengers this boarding event may board.
28    fn max_boardings(&self) -> Option<u32> {
29        None
30    }
31
32    /// Return `true` if a waiter may board this vehicle.
33    fn may_board(&self, waiter: &Waiter, vehicle: &TransitVehicle, served_stops: &[StopId])
34        -> bool;
35}
36
37/// FIFO boarding policy with an optional per-stop-event boarding cap.
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39pub struct FifoBoardingPolicy {
40    /// Optional maximum passengers boarded in one stop event.
41    pub max_boardings: Option<u32>,
42}
43
44impl FifoBoardingPolicy {
45    /// Create a FIFO policy with no event-level cap.
46    pub fn unlimited() -> Self {
47        Self {
48            max_boardings: None,
49        }
50    }
51
52    /// Create a FIFO policy capped to `max_boardings` per stop event.
53    pub fn capped(max_boardings: u32) -> Self {
54        Self {
55            max_boardings: Some(max_boardings),
56        }
57    }
58}
59
60impl BoardingPolicy for FifoBoardingPolicy {
61    fn max_boardings(&self) -> Option<u32> {
62        self.max_boardings
63    }
64
65    fn may_board(
66        &self,
67        waiter: &Waiter,
68        _vehicle: &TransitVehicle,
69        served_stops: &[StopId],
70    ) -> bool {
71        served_stops.contains(&waiter.destination)
72    }
73}
74
75/// Dispatch request context for one route at one simulation time.
76#[derive(Debug, Clone, Copy)]
77pub struct DispatchContext<'a> {
78    /// Route being considered for dispatch.
79    pub route: &'a Route,
80    /// Current simulation time in seconds.
81    pub now_s: f64,
82    /// Number of currently active vehicles assigned to this route.
83    pub active_vehicles: usize,
84    /// Optional active-vehicle cap for this route.
85    pub max_active_vehicles: Option<usize>,
86}
87
88impl<'a> DispatchContext<'a> {
89    /// Create a dispatch context for `route` at `now_s`.
90    pub fn new(route: &'a Route, now_s: f64) -> Self {
91        Self {
92            route,
93            now_s,
94            active_vehicles: 0,
95            max_active_vehicles: None,
96        }
97    }
98
99    /// Attach active-vehicle counts and an optional cap.
100    pub fn with_active_vehicles(
101        mut self,
102        active_vehicles: usize,
103        max_active_vehicles: Option<usize>,
104    ) -> Self {
105        self.active_vehicles = active_vehicles;
106        self.max_active_vehicles = max_active_vehicles;
107        self
108    }
109}
110
111/// Decision returned by a dispatch policy.
112#[derive(Debug, Clone, Copy, PartialEq)]
113pub enum DispatchDecision {
114    /// A vehicle should dispatch now for this departure time.
115    Dispatch {
116        /// Scheduled departure time being served.
117        departure_s: f64,
118    },
119    /// No dispatch should occur now.
120    Wait {
121        /// Next known departure time, if any.
122        next_departure_s: Option<f64>,
123        /// Seconds until the next departure, if known.
124        wait_s: Option<f64>,
125    },
126}
127
128/// Policy that decides whether a route should dispatch a vehicle.
129pub trait DispatchPolicy {
130    /// Return the dispatch decision for `context`.
131    fn decide(&self, context: DispatchContext<'_>) -> DispatchDecision;
132}
133
134/// Schedule-following dispatch policy with optional active-vehicle caps.
135#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
136pub struct ScheduledDispatchPolicy;
137
138impl DispatchPolicy for ScheduledDispatchPolicy {
139    fn decide(&self, context: DispatchContext<'_>) -> DispatchDecision {
140        if context
141            .max_active_vehicles
142            .is_some_and(|cap| context.active_vehicles >= cap)
143        {
144            return DispatchDecision::Wait {
145                next_departure_s: context.route.schedule.next_departure(context.now_s),
146                wait_s: None,
147            };
148        }
149
150        match context.route.schedule.next_departure(context.now_s) {
151            Some(departure_s) if departure_s <= context.now_s => {
152                DispatchDecision::Dispatch { departure_s }
153            }
154            Some(departure_s) => DispatchDecision::Wait {
155                next_departure_s: Some(departure_s),
156                wait_s: Some((departure_s - context.now_s).max(0.0)),
157            },
158            None => DispatchDecision::Wait {
159                next_departure_s: None,
160                wait_s: None,
161            },
162        }
163    }
164}
165
166/// Policy for computing stop dwell times from passenger exchange counts.
167pub trait DwellPolicy {
168    /// Compute dwell time for a stop event.
169    fn dwell_time(&self, alighters: u32, boarders: u32) -> DwellTime;
170}
171
172/// Linear dwell policy backed by [`DwellParams`].
173#[derive(Debug, Clone, Copy, Default, PartialEq)]
174pub struct LinearDwellPolicy {
175    /// Dwell parameters used by this policy.
176    pub params: DwellParams,
177}
178
179impl LinearDwellPolicy {
180    /// Create a linear dwell policy from explicit parameters.
181    pub fn new(params: DwellParams) -> Self {
182        Self { params }
183    }
184}
185
186impl DwellPolicy for LinearDwellPolicy {
187    fn dwell_time(&self, alighters: u32, boarders: u32) -> DwellTime {
188        dwell::compute(alighters, boarders, &self.params)
189    }
190}
191
192/// Apply a boarding policy to a queue and vehicle.
193pub fn board_with_policy<P: BoardingPolicy>(
194    queue: &mut Boarding,
195    vehicle: &mut TransitVehicle,
196    served_stops: &[StopId],
197    passenger_destinations: &mut Vec<StopId>,
198    policy: &P,
199) -> BoardingResult {
200    queue.board_vehicle_with_policy(vehicle, served_stops, passenger_destinations, policy)
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::{Boarding, Route, Schedule, TransitVehicle, Waiter};
207
208    #[test]
209    fn capacity_stop_queue_policy_rejects_full_stop() {
210        let stop = Stop::at_ground(1, "A", 0.0, 0.0, 1);
211        let mut queue = Boarding::new();
212        let first = Waiter {
213            passenger: 1,
214            destination: 2,
215            arrived_at: 0.0,
216        };
217        let second = Waiter {
218            passenger: 2,
219            destination: 2,
220            arrived_at: 1.0,
221        };
222        assert!(CapacityStopQueuePolicy.can_enqueue(&stop, &queue, &first));
223        queue.enqueue(first);
224        assert!(!CapacityStopQueuePolicy.can_enqueue(&stop, &queue, &second));
225    }
226
227    #[test]
228    fn capped_fifo_boarding_limits_one_stop_event() {
229        let mut queue = Boarding::new();
230        for passenger in 1..=3 {
231            queue.enqueue(Waiter {
232                passenger,
233                destination: 9,
234                arrived_at: passenger as f64,
235            });
236        }
237        let mut vehicle = TransitVehicle::idle(10, 5, 10);
238        let mut destinations = Vec::new();
239
240        let result = queue.board_vehicle_with_policy(
241            &mut vehicle,
242            &[9],
243            &mut destinations,
244            &FifoBoardingPolicy::capped(2),
245        );
246
247        assert_eq!(result.boarded, 2);
248        assert_eq!(vehicle.passengers, vec![1, 2]);
249        assert_eq!(destinations, vec![9, 9]);
250        assert_eq!(queue.len(), 1);
251    }
252
253    #[test]
254    fn scheduled_dispatch_waits_dispatches_and_respects_active_cap() {
255        let route = Route::new(
256            1,
257            "Blue",
258            vec![1, 2],
259            vec![60.0],
260            Schedule::fixed_headway(300.0),
261        );
262        let policy = ScheduledDispatchPolicy;
263
264        assert_eq!(
265            policy.decide(DispatchContext::new(&route, 100.0)),
266            DispatchDecision::Wait {
267                next_departure_s: Some(300.0),
268                wait_s: Some(200.0)
269            }
270        );
271        assert_eq!(
272            policy.decide(DispatchContext::new(&route, 300.0)),
273            DispatchDecision::Dispatch { departure_s: 300.0 }
274        );
275        assert_eq!(
276            policy.decide(DispatchContext::new(&route, 300.0).with_active_vehicles(2, Some(2))),
277            DispatchDecision::Wait {
278                next_departure_s: Some(300.0),
279                wait_s: None
280            }
281        );
282    }
283
284    #[test]
285    fn linear_dwell_policy_matches_dwell_formula() {
286        let dwell = LinearDwellPolicy::default().dwell_time(2, 3);
287        assert!((dwell.total - (8.0 + 2.0 * 1.8 + 3.0 * 2.6)).abs() < 1e-9);
288    }
289}