elevator_core/dispatch/scan.rs
1//! SCAN (elevator) dispatch algorithm — sweeps end-to-end before reversing.
2//!
3//! Originally described for disk-arm scheduling in Denning, P. J. (1967),
4//! "Effects of Scheduling on File Memory Operations", *Proc. AFIPS Spring
5//! Joint Computer Conference*, 9–21. The same sweep discipline is the
6//! textbook "elevator" algorithm.
7
8use std::collections::HashMap;
9
10use crate::entity::EntityId;
11use crate::world::World;
12
13use super::sweep::{self, SweepDirection, SweepMode};
14use super::{DispatchManifest, DispatchStrategy, ElevatorGroup, RankContext, pair_can_do_work};
15
16/// Elevator dispatch using the SCAN (elevator) algorithm.
17///
18/// Each car tracks a sweep direction; stops demanded in that direction
19/// receive a positional cost while stops behind are excluded. When
20/// nothing remains ahead, the sweep reverses and the car considers the
21/// reverse side. Direction and mode are resolved once per pass in
22/// [`DispatchStrategy::prepare_car`] so ranking is independent of the
23/// iteration order over stops.
24#[derive(serde::Serialize, serde::Deserialize)]
25pub struct ScanDispatch {
26 /// Per-elevator sweep direction. Persisted across dispatch passes
27 /// (reversed once a sweep exhausts demand ahead) and round-tripped
28 /// through [`DispatchStrategy::snapshot_config`] so a restored sim
29 /// continues the current sweep instead of defaulting to `Up` for
30 /// every car.
31 direction: HashMap<EntityId, SweepDirection>,
32 /// Per-elevator accept mode for the current dispatch pass.
33 /// Overwritten in full by `prepare_car` every pass, so no round-
34 /// trip is needed; `#[serde(skip)]` keeps snapshot bytes compact
35 /// and deterministic across process runs.
36 #[serde(skip)]
37 mode: HashMap<EntityId, SweepMode>,
38}
39
40impl ScanDispatch {
41 /// Create a new `ScanDispatch` with no initial direction state.
42 #[must_use]
43 pub fn new() -> Self {
44 Self {
45 direction: HashMap::new(),
46 mode: HashMap::new(),
47 }
48 }
49
50 /// Sweep direction for `car`, defaulting to `Up` for first-time callers.
51 fn direction_for(&self, car: EntityId) -> SweepDirection {
52 self.direction
53 .get(&car)
54 .copied()
55 .unwrap_or(SweepDirection::Up)
56 }
57
58 /// Accept mode for `car` in the current pass, defaulting to `Strict`.
59 fn mode_for(&self, car: EntityId) -> SweepMode {
60 self.mode.get(&car).copied().unwrap_or(SweepMode::Strict)
61 }
62}
63
64impl Default for ScanDispatch {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl DispatchStrategy for ScanDispatch {
71 fn prepare_car(
72 &mut self,
73 car: EntityId,
74 car_position: f64,
75 group: &ElevatorGroup,
76 manifest: &DispatchManifest,
77 world: &World,
78 ) {
79 let current = self.direction_for(car);
80 if sweep::strict_demand_ahead(current, car_position, group, manifest, world) {
81 self.mode.insert(car, SweepMode::Strict);
82 } else {
83 self.direction.insert(car, current.reversed());
84 self.mode.insert(car, SweepMode::Lenient);
85 }
86 }
87
88 fn rank(&mut self, ctx: &RankContext<'_>) -> Option<f64> {
89 // Reject un-servable pairs so a full car with only
90 // over-capacity waiting demand at its own stop can't open/close
91 // doors indefinitely. SCAN's direction reversal normally lifts
92 // it out of the self-stop within one tick (strict-ahead excludes
93 // the current position), but the Lenient transition tick would
94 // still rank the self-pair at cost 0 without this guard.
95 if !pair_can_do_work(ctx) {
96 return None;
97 }
98 sweep::rank(
99 self.mode_for(ctx.car),
100 self.direction_for(ctx.car),
101 ctx.car_position,
102 ctx.stop_position,
103 )
104 }
105
106 fn notify_removed(&mut self, elevator: EntityId) {
107 self.direction.remove(&elevator);
108 self.mode.remove(&elevator);
109 }
110
111 fn builtin_id(&self) -> Option<super::BuiltinStrategy> {
112 Some(super::BuiltinStrategy::Scan)
113 }
114
115 fn snapshot_config(&self) -> Option<String> {
116 ron::to_string(self).ok()
117 }
118
119 fn restore_config(&mut self, serialized: &str) -> Result<(), String> {
120 let restored: Self = ron::from_str(serialized).map_err(|e| e.to_string())?;
121 *self = restored;
122 Ok(())
123 }
124}