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}