Skip to main content

elevator_core/sim/
substep.rs

1//! Sub-tick stepping, phase hooks, event drainage.
2//!
3//! Part of the [`super::Simulation`] API surface; extracted from the
4//! monolithic `sim.rs` for readability. See the parent module for the
5//! overarching essential-API summary.
6
7use crate::dispatch::DispatchStrategy;
8use crate::events::EventBus;
9use crate::hooks::Phase;
10use crate::ids::GroupId;
11use crate::metrics::Metrics;
12use crate::systems::PhaseContext;
13use std::collections::BTreeMap;
14
15impl super::Simulation {
16    // ── Sub-stepping ────────────────────────────────────────────────
17
18    /// Get the dispatch strategies map (for advanced sub-stepping).
19    #[must_use]
20    pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
21        &self.dispatchers
22    }
23
24    /// Get the dispatch strategies map mutably (for advanced sub-stepping).
25    pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
26        &mut self.dispatchers
27    }
28
29    /// Get a mutable reference to the event bus.
30    pub const fn events_mut(&mut self) -> &mut EventBus {
31        &mut self.events
32    }
33
34    /// Get a mutable reference to the metrics.
35    pub const fn metrics_mut(&mut self) -> &mut Metrics {
36        &mut self.metrics
37    }
38
39    /// Build the `PhaseContext` for the current tick.
40    #[must_use]
41    pub const fn phase_context(&self) -> PhaseContext {
42        PhaseContext {
43            tick: self.tick,
44            dt: self.dt,
45        }
46    }
47
48    /// Run only the `advance_transient` phase (with hooks).
49    ///
50    /// # Phase ordering
51    ///
52    /// When calling individual phase methods instead of [`step()`](Self::step),
53    /// phases **must** be called in this order each tick:
54    ///
55    /// 1. `run_advance_transient`
56    /// 2. `run_dispatch`
57    /// 3. `run_reposition`
58    /// 4. `run_advance_queue`
59    /// 5. `run_movement`
60    /// 6. `run_doors`
61    /// 7. `run_loading`
62    /// 8. `run_metrics`
63    ///
64    /// Out-of-order execution may cause riders to board with closed doors,
65    /// elevators to move before dispatch, or transient states to persist
66    /// across tick boundaries.
67    pub fn run_advance_transient(&mut self) {
68        self.tick_in_progress = true;
69        self.sync_world_tick();
70        self.hooks
71            .run_before(Phase::AdvanceTransient, &mut self.world);
72        for group in &self.groups {
73            self.hooks
74                .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
75        }
76        let ctx = self.phase_context();
77        crate::systems::advance_transient::run(
78            &mut self.world,
79            &mut self.events,
80            &ctx,
81            &mut self.rider_index,
82        );
83        for group in &self.groups {
84            self.hooks
85                .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
86        }
87        self.hooks
88            .run_after(Phase::AdvanceTransient, &mut self.world);
89    }
90
91    /// Run only the dispatch phase (with hooks).
92    pub fn run_dispatch(&mut self) {
93        self.sync_world_tick();
94        self.hooks.run_before(Phase::Dispatch, &mut self.world);
95        for group in &self.groups {
96            self.hooks
97                .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
98        }
99        let ctx = self.phase_context();
100        crate::systems::dispatch::run(
101            &mut self.world,
102            &mut self.events,
103            &ctx,
104            &self.groups,
105            &mut self.dispatchers,
106            &self.rider_index,
107            &mut self.dispatch_scratch,
108        );
109        for group in &self.groups {
110            self.hooks
111                .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
112        }
113        self.hooks.run_after(Phase::Dispatch, &mut self.world);
114    }
115
116    /// Run only the movement phase (with hooks).
117    pub fn run_movement(&mut self) {
118        self.hooks.run_before(Phase::Movement, &mut self.world);
119        for group in &self.groups {
120            self.hooks
121                .run_before_group(Phase::Movement, group.id(), &mut self.world);
122        }
123        let ctx = self.phase_context();
124        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
125        crate::systems::movement::run(
126            &mut self.world,
127            &mut self.events,
128            &ctx,
129            &self.elevator_ids_buf,
130            &mut self.metrics,
131        );
132        for group in &self.groups {
133            self.hooks
134                .run_after_group(Phase::Movement, group.id(), &mut self.world);
135        }
136        self.hooks.run_after(Phase::Movement, &mut self.world);
137    }
138
139    /// Run only the doors phase (with hooks).
140    pub fn run_doors(&mut self) {
141        self.hooks.run_before(Phase::Doors, &mut self.world);
142        for group in &self.groups {
143            self.hooks
144                .run_before_group(Phase::Doors, group.id(), &mut self.world);
145        }
146        let ctx = self.phase_context();
147        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
148        crate::systems::doors::run(
149            &mut self.world,
150            &mut self.events,
151            &ctx,
152            &self.groups,
153            &self.elevator_ids_buf,
154        );
155        for group in &self.groups {
156            self.hooks
157                .run_after_group(Phase::Doors, group.id(), &mut self.world);
158        }
159        self.hooks.run_after(Phase::Doors, &mut self.world);
160    }
161
162    /// Run only the loading phase (with hooks).
163    pub fn run_loading(&mut self) {
164        self.hooks.run_before(Phase::Loading, &mut self.world);
165        for group in &self.groups {
166            self.hooks
167                .run_before_group(Phase::Loading, group.id(), &mut self.world);
168        }
169        let ctx = self.phase_context();
170        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
171        crate::systems::loading::run(
172            &mut self.world,
173            &mut self.events,
174            &ctx,
175            &self.groups,
176            &self.elevator_ids_buf,
177            &mut self.rider_index,
178        );
179        for group in &self.groups {
180            self.hooks
181                .run_after_group(Phase::Loading, group.id(), &mut self.world);
182        }
183        self.hooks.run_after(Phase::Loading, &mut self.world);
184    }
185
186    /// Run only the advance-queue phase (with hooks).
187    ///
188    /// Reconciles each elevator's phase/target with the front of its
189    /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
190    /// between Reposition and Movement.
191    pub fn run_advance_queue(&mut self) {
192        self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
193        for group in &self.groups {
194            self.hooks
195                .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
196        }
197        let ctx = self.phase_context();
198        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
199        crate::systems::advance_queue::run(
200            &mut self.world,
201            &mut self.events,
202            &ctx,
203            &self.groups,
204            &self.elevator_ids_buf,
205        );
206        for group in &self.groups {
207            self.hooks
208                .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
209        }
210        self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
211    }
212
213    /// Run only the reposition phase (with hooks).
214    ///
215    /// Global before/after hooks always fire even when no
216    /// [`RepositionStrategy`](crate::dispatch::RepositionStrategy) is
217    /// configured. Per-group hooks only fire for groups that have a
218    /// repositioner — this differs from other phases where per-group hooks
219    /// fire unconditionally.
220    pub fn run_reposition(&mut self) {
221        self.sync_world_tick();
222        self.hooks.run_before(Phase::Reposition, &mut self.world);
223        if !self.repositioners.is_empty() {
224            // Only run per-group hooks for groups that have a repositioner.
225            for group in &self.groups {
226                if self.repositioners.contains_key(&group.id()) {
227                    self.hooks
228                        .run_before_group(Phase::Reposition, group.id(), &mut self.world);
229                }
230            }
231            let ctx = self.phase_context();
232            crate::systems::reposition::run(
233                &mut self.world,
234                &mut self.events,
235                &ctx,
236                &self.groups,
237                &mut self.repositioners,
238                &mut self.reposition_buf,
239            );
240            for group in &self.groups {
241                if self.repositioners.contains_key(&group.id()) {
242                    self.hooks
243                        .run_after_group(Phase::Reposition, group.id(), &mut self.world);
244                }
245            }
246        }
247        self.hooks.run_after(Phase::Reposition, &mut self.world);
248    }
249
250    /// Run the energy system (no hooks — inline phase).
251    #[cfg(feature = "energy")]
252    fn run_energy(&mut self) {
253        let ctx = self.phase_context();
254        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
255        crate::systems::energy::run(
256            &mut self.world,
257            &mut self.events,
258            &ctx,
259            &self.elevator_ids_buf,
260        );
261    }
262
263    /// Run only the metrics phase (with hooks).
264    pub fn run_metrics(&mut self) {
265        self.hooks.run_before(Phase::Metrics, &mut self.world);
266        for group in &self.groups {
267            self.hooks
268                .run_before_group(Phase::Metrics, group.id(), &mut self.world);
269        }
270        let ctx = self.phase_context();
271        crate::systems::metrics::run(
272            &mut self.world,
273            &self.events,
274            &mut self.metrics,
275            &ctx,
276            &self.groups,
277        );
278        for group in &self.groups {
279            self.hooks
280                .run_after_group(Phase::Metrics, group.id(), &mut self.world);
281        }
282        self.hooks.run_after(Phase::Metrics, &mut self.world);
283    }
284
285    // Phase-hook registration lives in `sim/construction.rs`.
286
287    /// Increment the tick counter and flush events to the output buffer.
288    ///
289    /// Call after running all desired phases. Events emitted during this tick
290    /// are moved to the output buffer and available via `drain_events()`.
291    pub fn advance_tick(&mut self) {
292        self.pending_output.extend(self.events.drain());
293        self.tick += 1;
294        self.tick_in_progress = false;
295        // Keep the `CurrentTick` world resource in lockstep after the tick
296        // counter advances; substep consumers driving phases manually
297        // will see the fresh value on their next call.
298        self.sync_world_tick();
299        // Drop arrival-log entries older than the configured retention.
300        // Unbounded growth would turn `arrivals_in_window` into an O(n)
301        // per-stop per-tick scan.
302        let retention = self
303            .world
304            .resource::<crate::arrival_log::ArrivalLogRetention>()
305            .copied()
306            .unwrap_or_default()
307            .0;
308        let cutoff = self.tick.saturating_sub(retention);
309        if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
310            log.prune_before(cutoff);
311        }
312        if let Some(log) = self
313            .world
314            .resource_mut::<crate::arrival_log::DestinationLog>()
315        {
316            log.prune_before(cutoff);
317        }
318    }
319
320    /// Mirror `self.tick` into the `CurrentTick` world resource so
321    /// phases that only have `&World` (reposition strategies, custom
322    /// consumers) can compute rolling-window queries without plumbing
323    /// `PhaseContext`. Called from `step()` and `advance_tick()` so
324    /// manual-phase callers stay in sync too.
325    fn sync_world_tick(&mut self) {
326        if let Some(ct) = self.world.resource_mut::<crate::arrival_log::CurrentTick>() {
327            ct.0 = self.tick;
328        }
329    }
330
331    /// Advance the simulation by one tick.
332    ///
333    /// Events from this tick are buffered internally and available via
334    /// `drain_events()`. The metrics system only processes events from
335    /// the current tick, regardless of whether the consumer drains them.
336    ///
337    /// ```
338    /// use elevator_core::prelude::*;
339    ///
340    /// let mut sim = SimulationBuilder::demo().build().unwrap();
341    /// sim.step();
342    /// assert_eq!(sim.current_tick(), 1);
343    /// ```
344    pub fn step(&mut self) {
345        self.sync_world_tick();
346        self.world.snapshot_prev_positions();
347        self.run_advance_transient();
348        self.run_dispatch();
349        self.run_reposition();
350        self.run_advance_queue();
351        self.run_movement();
352        self.run_doors();
353        self.run_loading();
354        #[cfg(feature = "energy")]
355        self.run_energy();
356        self.run_metrics();
357        self.advance_tick();
358    }
359
360    /// Step the simulation until every rider reaches a terminal phase
361    /// (`Arrived`, `Abandoned`, or `Resident`), draining events each
362    /// tick so event-driven metrics stay up to date.
363    ///
364    /// Returns the number of ticks actually stepped, or `Err(max_ticks)`
365    /// if the budget was exhausted before the sim drained. The cap is a
366    /// safety net against a stuck dispatch or an unserviceable rider
367    /// holding the tick loop open forever — right-size it for your
368    /// workload and fail fast rather than spinning silently.
369    ///
370    /// A sim with zero riders returns `Ok(0)` immediately.
371    ///
372    /// ```
373    /// use elevator_core::prelude::*;
374    /// use elevator_core::stop::StopId;
375    ///
376    /// let mut sim = SimulationBuilder::demo().build().unwrap();
377    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
378    /// let ticks = sim.run_until_quiet(2_000).expect("sim drained in time");
379    /// assert!(sim.metrics().total_delivered() >= 1);
380    /// assert!(ticks <= 2_000);
381    /// ```
382    ///
383    /// # Errors
384    /// Returns `Err(max_ticks)` when `max_ticks` elapse without every
385    /// rider reaching a terminal phase. Inspect `sim.world()`
386    /// iteration or `sim.metrics()` to diagnose stuck riders; the
387    /// sim is left in its partially-advanced state so you can
388    /// snapshot it for post-mortem.
389    pub fn run_until_quiet(&mut self, max_ticks: u64) -> Result<u64, u64> {
390        use crate::components::RiderPhase;
391
392        fn all_quiet(sim: &super::Simulation) -> bool {
393            sim.world().iter_riders().all(|(_, r)| {
394                matches!(
395                    r.phase(),
396                    RiderPhase::Arrived | RiderPhase::Abandoned | RiderPhase::Resident
397                )
398            })
399        }
400
401        if all_quiet(self) {
402            return Ok(0);
403        }
404        for tick in 1..=max_ticks {
405            self.step();
406            let _ = self.drain_events();
407            if all_quiet(self) {
408                return Ok(tick);
409            }
410        }
411        Err(max_ticks)
412    }
413}