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