1use 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
9pub trait StopQueuePolicy {
11 fn can_enqueue(&self, stop: &Stop, queue: &Boarding, waiter: &Waiter) -> bool;
13}
14
15#[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
25pub trait BoardingPolicy {
27 fn max_boardings(&self) -> Option<u32> {
29 None
30 }
31
32 fn may_board(&self, waiter: &Waiter, vehicle: &TransitVehicle, served_stops: &[StopId])
34 -> bool;
35}
36
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39pub struct FifoBoardingPolicy {
40 pub max_boardings: Option<u32>,
42}
43
44impl FifoBoardingPolicy {
45 pub fn unlimited() -> Self {
47 Self {
48 max_boardings: None,
49 }
50 }
51
52 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#[derive(Debug, Clone, Copy)]
77pub struct DispatchContext<'a> {
78 pub route: &'a Route,
80 pub now_s: f64,
82 pub active_vehicles: usize,
84 pub max_active_vehicles: Option<usize>,
86}
87
88impl<'a> DispatchContext<'a> {
89 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 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#[derive(Debug, Clone, Copy, PartialEq)]
113pub enum DispatchDecision {
114 Dispatch {
116 departure_s: f64,
118 },
119 Wait {
121 next_departure_s: Option<f64>,
123 wait_s: Option<f64>,
125 },
126}
127
128pub trait DispatchPolicy {
130 fn decide(&self, context: DispatchContext<'_>) -> DispatchDecision;
132}
133
134#[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
166pub trait DwellPolicy {
168 fn dwell_time(&self, alighters: u32, boarders: u32) -> DwellTime;
170}
171
172#[derive(Debug, Clone, Copy, Default, PartialEq)]
174pub struct LinearDwellPolicy {
175 pub params: DwellParams,
177}
178
179impl LinearDwellPolicy {
180 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
192pub 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}