//! The engine itself. use crate::outcome::{EngineOutcome, EngineState, StopReason}; use apsis_event::{Event, EventId, EventQueue}; /// A discrete-event simulation engine. /// /// The engine owns the event queue and the clock. A run repeatedly takes the /// earliest event and passes it to a handler, which may schedule more /// events. The engine stops when the queue empties or when one of the limits /// is reached. #[derive(Debug, Clone)] pub struct Engine { queue: EventQueue, now: f64, executed: u64, stop_requested: bool, time_limit: Option, event_limit: Option, } impl Default for Engine { fn default() -> Self { Self::new() } } impl Engine { /// Creates an engine with the clock at zero and no limits. /// /// An engine with no limits runs until the queue empties, which is the /// right default for a model that terminates. A model that schedules /// without bound needs a limit, which is set with /// [`Engine::with_time_limit`] or [`Engine::with_event_limit`]. pub const fn new() -> Self { Self { queue: EventQueue::new(), now: 0.0, executed: 0, stop_requested: false, time_limit: None, event_limit: None, } } /// Sets the latest simulation time the engine will process. /// /// An event scheduled beyond the limit is left in the queue rather than /// executed, so the caller can examine what remained. A limit that is /// negative or not finite disables the limit. pub const fn with_time_limit(mut self, limit: f64) -> Self { self.time_limit = if limit.is_finite() && limit >= 0.0 { Some(limit) } else { None }; self } /// Sets the greatest number of events the engine will execute. /// /// A limit of zero disables the rule, because a run that executes no /// events is never useful. pub const fn with_event_limit(mut self, limit: u64) -> Self { self.event_limit = if limit == 0 { None } else { Some(limit) }; self } /// Returns the current simulation time, in seconds. pub const fn now(&self) -> f64 { self.now } /// Sets the current simulation time. /// /// This is used to start a run at an offset, for example at a scenario /// epoch rather than at zero. It does not reschedule pending events, so a /// caller that moves the clock forward past a pending event will execute /// that event late. The engine has no way to detect the mistake, so the /// caller is responsible for keeping the clock consistent with the /// queue. pub fn set_now(&mut self, time: f64) { if time.is_finite() && time >= 0.0 { self.now = time; } } /// Returns the number of events executed so far. pub const fn executed(&self) -> u64 { self.executed } /// Returns the number of pending events. pub fn pending(&self) -> usize { self.queue.len() } /// Returns `true` when no events are pending. pub fn is_idle(&self) -> bool { self.queue.is_empty() } /// Returns the time of the next pending event, if there is one. pub fn next_time(&self) -> Option { self.queue.next_time() } /// Returns what the engine currently holds. pub fn state(&self) -> EngineState { EngineState { now: self.now, pending: self.queue.len(), executed: self.executed, next_time: self.queue.next_time(), } } /// Schedules an event at a time. /// /// Returns the identifier of the new event, or `None` when the time is /// not usable. pub fn schedule(&mut self, time: f64, payload: T) -> Option { self.queue.schedule(time, payload) } /// Schedules an event at a delay from the current simulation time. /// /// Returns the identifier of the new event, or `None` when the delay is /// not usable. pub fn schedule_after(&mut self, delay: f64, payload: T) -> Option { self.queue.schedule_after(self.now, delay, payload) } /// Asks the engine to stop after the current event. /// /// The handler calls this when its own model says the run is over, for /// example when a stopping condition that the queue cannot express is /// met. The pending events are left in the queue. pub fn request_stop(&mut self) { self.stop_requested = true; } /// Clears the stop request, so that a subsequent run continues. pub fn clear_stop(&mut self) { self.stop_requested = false; } /// Removes every pending event and returns how many were discarded. pub fn clear(&mut self) -> usize { self.queue.clear() } /// Returns the queue, for a caller that needs to inspect or manipulate it /// directly. pub const fn queue(&self) -> &EventQueue { &self.queue } /// Returns the queue for modification. pub const fn queue_mut(&mut self) -> &mut EventQueue { &mut self.queue } /// Runs the engine until it stops. /// /// Each iteration takes the earliest event, advances the clock to its /// time, and passes it to the handler. The handler may schedule further /// events, including at the same instant, which are delivered after the /// events already scheduled for that instant. /// /// Returns the outcome of the run. The clock is left at the time of the /// last event executed, not at a limit, so a caller that records the /// clock sees the last instant the model actually reached. pub fn run(&mut self, mut handler: F) -> EngineOutcome where F: FnMut(&mut Engine, Event), { let started_at = self.now; let mut executed_this_run = 0_u64; self.stop_requested = false; let reason = loop { // The event limit is checked before taking an event, so a run // that reaches the limit leaves the queue untouched for the next // call. if let Some(limit) = self.event_limit { if self.executed >= limit { break StopReason::EventLimit; } } let Some(event) = self.queue.pop() else { break StopReason::QueueEmpty; }; // The time limit leaves the event in the queue rather than // discarding it, so the caller can inspect what remained. The // event was already popped, so it is put back. if let Some(limit) = self.time_limit { if event.time > limit { self.queue.schedule(event.time, event.payload); break StopReason::TimeLimit; } } self.now = event.time; self.executed = self.executed.wrapping_add(1); executed_this_run += 1; handler(self, event); if self.stop_requested { break StopReason::Requested; } }; EngineOutcome { reason, executed: executed_this_run, started_at, ended_at: self.now, remaining: self.queue.len(), } } }