Skip to main content

elevator_core/dispatch/
look.rs

1//! LOOK dispatch algorithm — reverses at the last request, not the shaft end.
2//!
3//! Introduced in Merten, A. G. (1970), "Some Quantitative Techniques for
4//! File Organization" (Univ. Wisconsin tech report) as an improvement on
5//! SCAN that avoids unnecessary travel past the furthest pending request.
6//!
7//! Within this library SCAN and LOOK share identical dispatch semantics:
8//! both prefer demanded stops in the current sweep direction and reverse
9//! only when nothing remains ahead. The historical distinction — whether
10//! the car drives to the physical shaft end between sweeps — applies to
11//! the motion layer, not dispatch.
12
13use std::collections::{BTreeMap, HashMap};
14
15use crate::entity::EntityId;
16use crate::world::World;
17
18use super::sweep::{self, SweepDirection, SweepMode};
19use super::{DispatchManifest, DispatchStrategy, ElevatorGroup, RankContext, pair_is_useful};
20
21/// Elevator dispatch using the LOOK algorithm. See module docs.
22#[derive(serde::Serialize, serde::Deserialize)]
23pub struct LookDispatch {
24    /// Per-elevator sweep direction. Persisted across dispatch passes
25    /// (reversed once a sweep exhausts demand ahead) and round-tripped
26    /// through [`DispatchStrategy::snapshot_config`] so a restored sim
27    /// continues the current sweep instead of defaulting to `Up` for
28    /// every car. `BTreeMap` so RON serialization in `snapshot_config`
29    /// is byte-identical across processes (#254 follow-up).
30    direction: BTreeMap<EntityId, SweepDirection>,
31    /// Per-elevator accept mode for the current dispatch pass.
32    /// Overwritten in full by `prepare_car` every pass, so no round-
33    /// trip is needed.
34    #[serde(skip)]
35    mode: HashMap<EntityId, SweepMode>,
36}
37
38impl LookDispatch {
39    /// Create a new `LookDispatch` with no initial direction state.
40    #[must_use]
41    pub fn new() -> Self {
42        Self {
43            direction: BTreeMap::new(),
44            mode: HashMap::new(),
45        }
46    }
47
48    /// Sweep direction for `car`, defaulting to `Up` for first-time callers.
49    fn direction_for(&self, car: EntityId) -> SweepDirection {
50        self.direction
51            .get(&car)
52            .copied()
53            .unwrap_or(SweepDirection::Up)
54    }
55
56    /// Accept mode for `car` in the current pass, defaulting to `Strict`.
57    fn mode_for(&self, car: EntityId) -> SweepMode {
58        self.mode.get(&car).copied().unwrap_or(SweepMode::Strict)
59    }
60}
61
62impl Default for LookDispatch {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl DispatchStrategy for LookDispatch {
69    fn prepare_car(
70        &mut self,
71        car: EntityId,
72        car_position: f64,
73        group: &ElevatorGroup,
74        manifest: &DispatchManifest,
75        world: &World,
76    ) {
77        let current = self.direction_for(car);
78        if sweep::strict_demand_ahead(current, car_position, group, manifest, world) {
79            self.mode.insert(car, SweepMode::Strict);
80        } else {
81            self.direction.insert(car, current.reversed());
82            self.mode.insert(car, SweepMode::Lenient);
83        }
84    }
85
86    fn rank(&mut self, ctx: &RankContext<'_>) -> Option<f64> {
87        // Same guard as SCAN: deny un-servable pairs so an over-capacity
88        // waiting rider at the car's own stop can't pull the car into a
89        // cost-0 self-assignment during the Lenient reversal tick.
90        if !pair_is_useful(ctx, false) {
91            return None;
92        }
93        sweep::rank(
94            self.mode_for(ctx.car),
95            self.direction_for(ctx.car),
96            ctx.car_position,
97            ctx.stop_position,
98        )
99    }
100
101    fn notify_removed(&mut self, elevator: EntityId) {
102        self.direction.remove(&elevator);
103        self.mode.remove(&elevator);
104    }
105
106    fn builtin_id(&self) -> Option<super::BuiltinStrategy> {
107        Some(super::BuiltinStrategy::Look)
108    }
109
110    fn snapshot_config(&self) -> Option<String> {
111        ron::to_string(self).ok()
112    }
113
114    fn restore_config(&mut self, serialized: &str) -> Result<(), String> {
115        let restored: Self = ron::from_str(serialized).map_err(|e| e.to_string())?;
116        *self = restored;
117        Ok(())
118    }
119}