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