apsis-rs/crates/sim/apsis-sim-engine/src/outcome.rs

120 lines
3.6 KiB
Rust

//! What a run produced and what the engine currently holds.
use core::fmt;
/// Why a run stopped.
///
/// The reason is reported rather than inferred from the event count, because
/// the same count can arise from an empty queue or from a limit, and a
/// caller that records the outcome of a run needs to distinguish them.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StopReason {
/// No events were left when the engine looked for the next one.
QueueEmpty,
/// The time of the next event is beyond the simulation time limit.
TimeLimit,
/// The handler set a flag asking the engine to stop.
Requested,
/// The event count reached the configured limit.
EventLimit,
}
impl StopReason {
/// Returns `true` when the run ended because the model had no more work
/// to do rather than because a limit intervened.
pub const fn is_natural(self) -> bool {
matches!(self, Self::QueueEmpty)
}
/// Returns a short name for the reason.
pub const fn name(self) -> &'static str {
match self {
Self::QueueEmpty => "queue empty",
Self::TimeLimit => "time limit",
Self::Requested => "requested",
Self::EventLimit => "event limit",
}
}
}
impl fmt::Display for StopReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
/// What a run produced.
///
/// The outcome records the counts and the clock, so a caller can record the
/// result of a run without reaching back into the engine.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EngineOutcome {
/// Why the run stopped.
pub reason: StopReason,
/// The number of events the run executed.
pub executed: u64,
/// The simulation time the run started from, in seconds.
pub started_at: f64,
/// The simulation time the run reached, in seconds.
pub ended_at: f64,
/// The number of events still pending when the run stopped.
pub remaining: usize,
}
impl EngineOutcome {
/// Returns the time the run covered, in seconds.
pub fn duration(&self) -> f64 {
self.ended_at - self.started_at
}
/// Returns the mean number of events per second of simulation time.
///
/// This measures how busy the model is rather than how long the run took
/// on the wall clock. Returns `None` when the run covered no time.
pub fn event_rate(&self) -> Option<f64> {
let duration = self.duration();
if duration > 0.0 {
Some(self.executed as f64 / duration)
} else {
None
}
}
/// Returns `true` when the run finished because the model had no more
/// work rather than because a limit stopped it.
pub fn is_natural(&self) -> bool {
self.reason.is_natural()
}
}
impl fmt::Display for EngineOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} event(s) from {} s to {} s, stopped by {}",
self.executed, self.started_at, self.ended_at, self.reason
)
}
}
/// What the engine currently holds.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EngineState {
/// The current simulation time, in seconds.
pub now: f64,
/// The number of pending events.
pub pending: usize,
/// The number of events executed so far.
pub executed: u64,
/// The time of the next pending event, if there is one.
pub next_time: Option<f64>,
}
impl EngineState {
/// Returns `true` when no events are pending.
pub const fn is_idle(&self) -> bool {
self.pending == 0
}
}