Skip to main content

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};
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.
24pub struct ScanDispatch {
25    /// Per-elevator sweep direction.
26    direction: HashMap<EntityId, SweepDirection>,
27    /// Per-elevator accept mode for the current dispatch pass.
28    mode: HashMap<EntityId, SweepMode>,
29}
30
31impl ScanDispatch {
32    /// Create a new `ScanDispatch` with no initial direction state.
33    #[must_use]
34    pub fn new() -> Self {
35        Self {
36            direction: HashMap::new(),
37            mode: HashMap::new(),
38        }
39    }
40
41    /// Sweep direction for `car`, defaulting to `Up` for first-time callers.
42    fn direction_for(&self, car: EntityId) -> SweepDirection {
43        self.direction
44            .get(&car)
45            .copied()
46            .unwrap_or(SweepDirection::Up)
47    }
48
49    /// Accept mode for `car` in the current pass, defaulting to `Strict`.
50    fn mode_for(&self, car: EntityId) -> SweepMode {
51        self.mode.get(&car).copied().unwrap_or(SweepMode::Strict)
52    }
53}
54
55impl Default for ScanDispatch {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl DispatchStrategy for ScanDispatch {
62    fn prepare_car(
63        &mut self,
64        car: EntityId,
65        car_position: f64,
66        group: &ElevatorGroup,
67        manifest: &DispatchManifest,
68        world: &World,
69    ) {
70        let current = self.direction_for(car);
71        if sweep::strict_demand_ahead(current, car_position, group, manifest, world) {
72            self.mode.insert(car, SweepMode::Strict);
73        } else {
74            self.direction.insert(car, current.reversed());
75            self.mode.insert(car, SweepMode::Lenient);
76        }
77    }
78
79    fn rank(
80        &mut self,
81        car: EntityId,
82        car_position: f64,
83        _stop: EntityId,
84        stop_position: f64,
85        _group: &ElevatorGroup,
86        _manifest: &DispatchManifest,
87        _world: &World,
88    ) -> Option<f64> {
89        sweep::rank(
90            self.mode_for(car),
91            self.direction_for(car),
92            car_position,
93            stop_position,
94        )
95    }
96
97    fn notify_removed(&mut self, elevator: EntityId) {
98        self.direction.remove(&elevator);
99        self.mode.remove(&elevator);
100    }
101}