Skip to main content

elevator_core/
arrival_log.rs

1//! Rolling per-stop arrival log.
2//!
3//! Carries a `CurrentTick` sibling resource that mirrors
4//! [`Simulation::current_tick`](crate::sim::Simulation::current_tick)
5//! so strategies reading the log from phases without direct access to
6//! `PhaseContext` (e.g. [`RepositionStrategy`](crate::dispatch::RepositionStrategy))
7//! can still compute windowed queries.
8//!
9//!
10//! Commercial group controllers sample per-stop arrival rates to pick
11//! traffic modes (up-peak, down-peak) and to pre-position idle cars
12//! ahead of expected demand (Otis Compass Infinity's *predictive
13//! parking*, KONE Polaris's pattern-driven mode switch). This log is
14//! the signal source: dispatch strategies read it via
15//! [`DispatchManifest::arrivals_at`](crate::dispatch::DispatchManifest::arrivals_at),
16//! reposition strategies read it directly from `World` resources.
17//!
18//! The log is append-only during a tick and pruned at the start of each
19//! tick to keep memory bounded under long runs. Stored entries are
20//! `(tick, stop)` pairs; queries are by stop and time window only.
21
22use crate::entity::EntityId;
23use serde::{Deserialize, Serialize};
24
25/// World resource mirroring the current simulation tick.
26///
27/// Kept in sync by [`Simulation::step`](crate::sim::Simulation::step).
28/// Lets phases that don't receive a `PhaseContext` (reposition
29/// strategies, custom `World` consumers) compute rolling-window queries
30/// against [`ArrivalLog`] without plumbing tick through their
31/// signatures.
32#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
33pub struct CurrentTick(
34    /// Tick value at the start of the last `step()` entry.
35    pub u64,
36);
37
38/// Default rolling window (in ticks).
39///
40/// Used by [`DispatchManifest::arrivals_at`](crate::dispatch::DispatchManifest::arrivals_at)
41/// when the sim doesn't override it. Five minutes of real time at the
42/// default 60 Hz tick rate, matching the window commercial controllers
43/// use to detect up-peak / down-peak transitions.
44pub const DEFAULT_ARRIVAL_WINDOW_TICKS: u64 = 18_000;
45
46/// World resource controlling how far back the [`ArrivalLog`] retains
47/// entries before `Simulation::advance_tick` prunes them.
48///
49/// Defaults to [`DEFAULT_ARRIVAL_WINDOW_TICKS`]. Strategies that query
50/// a longer window (e.g.
51/// [`PredictiveParking::with_window_ticks`](crate::dispatch::reposition::PredictiveParking::with_window_ticks)
52/// with a value greater than the default) will silently see only the
53/// last `DEFAULT_ARRIVAL_WINDOW_TICKS` unless this retention is
54/// widened via [`Simulation::set_arrival_log_retention_ticks`](crate::sim::Simulation::set_arrival_log_retention_ticks).
55#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
56pub struct ArrivalLogRetention(pub u64);
57
58impl Default for ArrivalLogRetention {
59    fn default() -> Self {
60        Self(DEFAULT_ARRIVAL_WINDOW_TICKS)
61    }
62}
63
64/// Append-only log of per-stop arrival events used to compute rolling
65/// arrival-rate signals.
66///
67/// Stored as a `Vec<(tick, stop)>` sorted by tick (records are appended
68/// in tick order during normal sim execution). Queries are `O(n)` worst
69/// case but typically `O(window_size)`; `prune_before` keeps the tail
70/// short enough that this is a non-issue for practical window sizes.
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72pub struct ArrivalLog {
73    /// `(tick, stop)` pairs. Entries are appended in tick order.
74    entries: Vec<(u64, EntityId)>,
75}
76
77impl ArrivalLog {
78    /// Record an arrival at `stop` on `tick`.
79    pub fn record(&mut self, tick: u64, stop: EntityId) {
80        self.entries.push((tick, stop));
81    }
82
83    /// Count arrivals at `stop` within the window `[now - window, now]`
84    /// inclusive. `window_ticks = 0` always returns 0.
85    #[must_use]
86    pub fn arrivals_in_window(&self, stop: EntityId, now: u64, window_ticks: u64) -> u64 {
87        if window_ticks == 0 {
88            return 0;
89        }
90        let lower = now.saturating_sub(window_ticks);
91        self.entries
92            .iter()
93            .filter(|(t, s)| *s == stop && *t >= lower && *t <= now)
94            .count() as u64
95    }
96
97    /// Drop every entry with tick strictly before `cutoff`. Called each
98    /// tick by the sim with `cutoff = current_tick - max_window` so the
99    /// log can't grow without bound.
100    pub fn prune_before(&mut self, cutoff: u64) {
101        self.entries.retain(|(t, _)| *t >= cutoff);
102    }
103
104    /// Number of recorded events currently in the log. Intended for
105    /// snapshot inspection and tests.
106    #[must_use]
107    pub const fn len(&self) -> usize {
108        self.entries.len()
109    }
110
111    /// Whether the log has no recorded events.
112    #[must_use]
113    pub const fn is_empty(&self) -> bool {
114        self.entries.is_empty()
115    }
116
117    /// Rewrite every entry's stop `EntityId` through `id_remap`, dropping
118    /// entries whose stop isn't present in the map. Used by snapshot
119    /// restore to translate pre-restore stop IDs to the new allocations.
120    pub fn remap_entity_ids(&mut self, id_remap: &std::collections::HashMap<EntityId, EntityId>) {
121        self.entries
122            .retain_mut(|(_, stop)| match id_remap.get(stop) {
123                Some(&new) => {
124                    *stop = new;
125                    true
126                }
127                None => false,
128            });
129    }
130}