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 /// Advance the simulation by `n` ticks.
361 ///
362 /// Equivalent to calling [`step`](Self::step) `n` times. Hosts driving
363 /// the sim across an FFI / wasm boundary should prefer this over a
364 /// per-tick loop on their side: keeping the loop in Rust avoids
365 /// per-tick boundary crossings that add up at scale.
366 ///
367 /// Events from each tick accumulate in the internal queue; consumers
368 /// call [`drain_events`](Self::drain_events) once after the batch to
369 /// read the cumulative stream.
370 ///
371 /// `n == 0` is a no-op.
372 ///
373 /// ```
374 /// use elevator_core::prelude::*;
375 ///
376 /// let mut sim = SimulationBuilder::demo().build().unwrap();
377 /// sim.step_many(60);
378 /// assert_eq!(sim.current_tick(), 60);
379 /// ```
380 pub fn step_many(&mut self, n: u32) {
381 for _ in 0..n {
382 self.step();
383 }
384 }
385
386 /// Step the simulation until every rider reaches a terminal phase
387 /// (`Arrived`, `Abandoned`, or `Resident`), draining events each
388 /// tick so event-driven metrics stay up to date.
389 ///
390 /// Returns the number of ticks actually stepped, or `Err(max_ticks)`
391 /// if the budget was exhausted before the sim drained. The cap is a
392 /// safety net against a stuck dispatch or an unserviceable rider
393 /// holding the tick loop open forever — right-size it for your
394 /// workload and fail fast rather than spinning silently.
395 ///
396 /// A sim with zero riders returns `Ok(0)` immediately.
397 ///
398 /// ```
399 /// use elevator_core::prelude::*;
400 /// use elevator_core::stop::StopId;
401 ///
402 /// let mut sim = SimulationBuilder::demo().build().unwrap();
403 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
404 /// let ticks = sim.run_until_quiet(2_000).expect("sim drained in time");
405 /// assert!(sim.metrics().total_delivered() >= 1);
406 /// assert!(ticks <= 2_000);
407 /// ```
408 ///
409 /// # Errors
410 /// Returns `Err(max_ticks)` when `max_ticks` elapse without every
411 /// rider reaching a terminal phase. Inspect `sim.world()`
412 /// iteration or `sim.metrics()` to diagnose stuck riders; the
413 /// sim is left in its partially-advanced state so you can
414 /// snapshot it for post-mortem.
415 pub fn run_until_quiet(&mut self, max_ticks: u64) -> Result<u64, u64> {
416 use crate::components::RiderPhase;
417
418 fn all_quiet(sim: &super::Simulation) -> bool {
419 sim.world().iter_riders().all(|(_, r)| {
420 matches!(
421 r.phase(),
422 RiderPhase::Arrived | RiderPhase::Abandoned | RiderPhase::Resident
423 )
424 })
425 }
426
427 if all_quiet(self) {
428 return Ok(0);
429 }
430 for tick in 1..=max_ticks {
431 self.step();
432 let _ = self.drain_events();
433 if all_quiet(self) {
434 return Ok(tick);
435 }
436 }
437 Err(max_ticks)
438 }
439}