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.elevator_ids_buf,
153        );
154        for group in &self.groups {
155            self.hooks
156                .run_after_group(Phase::Doors, group.id(), &mut self.world);
157        }
158        self.hooks.run_after(Phase::Doors, &mut self.world);
159    }
160
161    /// Run only the loading phase (with hooks).
162    pub fn run_loading(&mut self) {
163        self.hooks.run_before(Phase::Loading, &mut self.world);
164        for group in &self.groups {
165            self.hooks
166                .run_before_group(Phase::Loading, group.id(), &mut self.world);
167        }
168        let ctx = self.phase_context();
169        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
170        crate::systems::loading::run(
171            &mut self.world,
172            &mut self.events,
173            &ctx,
174            &self.elevator_ids_buf,
175            &mut self.rider_index,
176        );
177        for group in &self.groups {
178            self.hooks
179                .run_after_group(Phase::Loading, group.id(), &mut self.world);
180        }
181        self.hooks.run_after(Phase::Loading, &mut self.world);
182    }
183
184    /// Run only the advance-queue phase (with hooks).
185    ///
186    /// Reconciles each elevator's phase/target with the front of its
187    /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
188    /// between Reposition and Movement.
189    pub fn run_advance_queue(&mut self) {
190        self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
191        for group in &self.groups {
192            self.hooks
193                .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
194        }
195        let ctx = self.phase_context();
196        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
197        crate::systems::advance_queue::run(
198            &mut self.world,
199            &mut self.events,
200            &ctx,
201            &self.elevator_ids_buf,
202        );
203        for group in &self.groups {
204            self.hooks
205                .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
206        }
207        self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
208    }
209
210    /// Run only the reposition phase (with hooks).
211    ///
212    /// Global before/after hooks always fire even when no
213    /// [`RepositionStrategy`](crate::dispatch::RepositionStrategy) is
214    /// configured. Per-group hooks only fire for groups that have a
215    /// repositioner — this differs from other phases where per-group hooks
216    /// fire unconditionally.
217    pub fn run_reposition(&mut self) {
218        self.sync_world_tick();
219        self.hooks.run_before(Phase::Reposition, &mut self.world);
220        if !self.repositioners.is_empty() {
221            // Only run per-group hooks for groups that have a repositioner.
222            for group in &self.groups {
223                if self.repositioners.contains_key(&group.id()) {
224                    self.hooks
225                        .run_before_group(Phase::Reposition, group.id(), &mut self.world);
226                }
227            }
228            let ctx = self.phase_context();
229            crate::systems::reposition::run(
230                &mut self.world,
231                &mut self.events,
232                &ctx,
233                &self.groups,
234                &mut self.repositioners,
235                &mut self.reposition_buf,
236            );
237            for group in &self.groups {
238                if self.repositioners.contains_key(&group.id()) {
239                    self.hooks
240                        .run_after_group(Phase::Reposition, group.id(), &mut self.world);
241                }
242            }
243        }
244        self.hooks.run_after(Phase::Reposition, &mut self.world);
245    }
246
247    /// Run the energy system (no hooks — inline phase).
248    #[cfg(feature = "energy")]
249    fn run_energy(&mut self) {
250        let ctx = self.phase_context();
251        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
252        crate::systems::energy::run(
253            &mut self.world,
254            &mut self.events,
255            &ctx,
256            &self.elevator_ids_buf,
257        );
258    }
259
260    /// Run only the metrics phase (with hooks).
261    pub fn run_metrics(&mut self) {
262        self.hooks.run_before(Phase::Metrics, &mut self.world);
263        for group in &self.groups {
264            self.hooks
265                .run_before_group(Phase::Metrics, group.id(), &mut self.world);
266        }
267        let ctx = self.phase_context();
268        crate::systems::metrics::run(
269            &mut self.world,
270            &self.events,
271            &mut self.metrics,
272            &ctx,
273            &self.groups,
274        );
275        for group in &self.groups {
276            self.hooks
277                .run_after_group(Phase::Metrics, group.id(), &mut self.world);
278        }
279        self.hooks.run_after(Phase::Metrics, &mut self.world);
280    }
281
282    // Phase-hook registration lives in `sim/construction.rs`.
283
284    /// Increment the tick counter and flush events to the output buffer.
285    ///
286    /// Call after running all desired phases. Events emitted during this tick
287    /// are moved to the output buffer and available via `drain_events()`.
288    pub fn advance_tick(&mut self) {
289        self.pending_output.extend(self.events.drain());
290        self.tick += 1;
291        self.tick_in_progress = false;
292        // Keep the `CurrentTick` world resource in lockstep after the tick
293        // counter advances; substep consumers driving phases manually
294        // will see the fresh value on their next call.
295        self.sync_world_tick();
296        // Drop arrival-log entries older than the configured retention.
297        // Unbounded growth would turn `arrivals_in_window` into an O(n)
298        // per-stop per-tick scan.
299        let retention = self
300            .world
301            .resource::<crate::arrival_log::ArrivalLogRetention>()
302            .copied()
303            .unwrap_or_default()
304            .0;
305        let cutoff = self.tick.saturating_sub(retention);
306        if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
307            log.prune_before(cutoff);
308        }
309        if let Some(log) = self
310            .world
311            .resource_mut::<crate::arrival_log::DestinationLog>()
312        {
313            log.prune_before(cutoff);
314        }
315    }
316
317    /// Mirror `self.tick` into the `CurrentTick` world resource so
318    /// phases that only have `&World` (reposition strategies, custom
319    /// consumers) can compute rolling-window queries without plumbing
320    /// `PhaseContext`. Called from `step()` and `advance_tick()` so
321    /// manual-phase callers stay in sync too.
322    fn sync_world_tick(&mut self) {
323        if let Some(ct) = self.world.resource_mut::<crate::arrival_log::CurrentTick>() {
324            ct.0 = self.tick;
325        }
326    }
327
328    /// Advance the simulation by one tick.
329    ///
330    /// Events from this tick are buffered internally and available via
331    /// `drain_events()`. The metrics system only processes events from
332    /// the current tick, regardless of whether the consumer drains them.
333    ///
334    /// ```
335    /// use elevator_core::prelude::*;
336    ///
337    /// let mut sim = SimulationBuilder::demo().build().unwrap();
338    /// sim.step();
339    /// assert_eq!(sim.current_tick(), 1);
340    /// ```
341    pub fn step(&mut self) {
342        self.sync_world_tick();
343        self.world.snapshot_prev_positions();
344        self.run_advance_transient();
345        self.run_dispatch();
346        self.run_reposition();
347        self.run_advance_queue();
348        self.run_movement();
349        self.run_doors();
350        self.run_loading();
351        #[cfg(feature = "energy")]
352        self.run_energy();
353        self.run_metrics();
354        self.advance_tick();
355    }
356
357    /// Step the simulation until every rider reaches a terminal phase
358    /// (`Arrived`, `Abandoned`, or `Resident`), draining events each
359    /// tick so event-driven metrics stay up to date.
360    ///
361    /// Returns the number of ticks actually stepped, or `Err(max_ticks)`
362    /// if the budget was exhausted before the sim drained. The cap is a
363    /// safety net against a stuck dispatch or an unserviceable rider
364    /// holding the tick loop open forever — right-size it for your
365    /// workload and fail fast rather than spinning silently.
366    ///
367    /// A sim with zero riders returns `Ok(0)` immediately.
368    ///
369    /// ```
370    /// use elevator_core::prelude::*;
371    /// use elevator_core::stop::StopId;
372    ///
373    /// let mut sim = SimulationBuilder::demo().build().unwrap();
374    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
375    /// let ticks = sim.run_until_quiet(2_000).expect("sim drained in time");
376    /// assert!(sim.metrics().total_delivered() >= 1);
377    /// assert!(ticks <= 2_000);
378    /// ```
379    ///
380    /// # Errors
381    /// Returns `Err(max_ticks)` when `max_ticks` elapse without every
382    /// rider reaching a terminal phase. Inspect `sim.world()`
383    /// iteration or `sim.metrics()` to diagnose stuck riders; the
384    /// sim is left in its partially-advanced state so you can
385    /// snapshot it for post-mortem.
386    pub fn run_until_quiet(&mut self, max_ticks: u64) -> Result<u64, u64> {
387        use crate::components::RiderPhase;
388
389        fn all_quiet(sim: &super::Simulation) -> bool {
390            sim.world().iter_riders().all(|(_, r)| {
391                matches!(
392                    r.phase(),
393                    RiderPhase::Arrived | RiderPhase::Abandoned | RiderPhase::Resident
394                )
395            })
396        }
397
398        if all_quiet(self) {
399            return Ok(0);
400        }
401        for tick in 1..=max_ticks {
402            self.step();
403            let _ = self.drain_events();
404            if all_quiet(self) {
405                return Ok(tick);
406            }
407        }
408        Err(max_ticks)
409    }
410}