sim: add event type and time ordered event queue

This commit is contained in:
ApsisRS contributors 2026-09-26 22:23:13 +08:00
parent 436ebcc6f9
commit 15c1531f30
6 changed files with 659 additions and 0 deletions

3
Cargo.lock generated
View File

@ -86,6 +86,9 @@ version = "0.1.0"
[[package]] [[package]]
name = "apsis-event" name = "apsis-event"
version = "0.1.0" version = "0.1.0"
dependencies = [
"apsis-core",
]
[[package]] [[package]]
name = "apsis-frames" name = "apsis-frames"

View File

@ -11,3 +11,4 @@ publish = false
workspace = true workspace = true
[dependencies] [dependencies]
apsis-core.workspace = true

View File

@ -0,0 +1,111 @@
//! The event type and its identifier.
use core::fmt;
/// An identifier for a scheduled event.
///
/// The identifier combines the time at which the event is due with the order
/// in which it was scheduled. Two events that fall at the same instant are
/// therefore distinguishable, which is what makes the delivery order
/// repeatable.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EventId {
/// The scheduled time, in seconds from the start of the simulation.
///
/// The value is stored as its bit pattern so that the derived ordering on
/// the identifier matches the ordering on time for non-negative values,
/// which is the only range a simulation uses.
time_bits: u64,
/// The order in which the event was scheduled.
sequence: u64,
}
impl EventId {
/// Creates an identifier from a time and a sequence number.
///
/// The time must be finite and non-negative. A caller that has a time
/// outside that range cannot use this type, which is deliberate: a
/// simulation clock that goes backwards or becomes infinite has no
/// meaningful order.
pub fn new(time: f64, sequence: u64) -> Option<Self> {
if !time.is_finite() || time < 0.0 {
return None;
}
Some(Self {
time_bits: time.to_bits(),
sequence,
})
}
/// Creates an identifier from a time already validated as usable.
///
/// This is the crate internal constructor used by the queue, which has
/// already checked the time before it stored the entry.
pub(crate) fn from_valid(time: f64, sequence: u64) -> Self {
Self {
time_bits: time.to_bits(),
sequence,
}
}
/// Returns the scheduled time, in seconds.
pub fn time(self) -> f64 {
f64::from_bits(self.time_bits)
}
/// Returns the order in which the event was scheduled.
pub const fn sequence(self) -> u64 {
self.sequence
}
}
impl fmt::Display for EventId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "event {} at {} s", self.sequence, self.time())
}
}
/// One scheduled event.
///
/// The payload is the caller's: it names what should happen. The queue does
/// not interpret it, so the same queue serves any model.
#[derive(Debug, Clone, PartialEq)]
pub struct Event<T> {
/// The identifier of this event.
pub id: EventId,
/// The scheduled time, in seconds from the start of the simulation.
pub time: f64,
/// The payload describing what should happen.
pub payload: T,
}
impl<T> Event<T> {
/// Returns `true` when the event is due at or before a time.
pub fn is_due(&self, now: f64) -> bool {
self.time <= now
}
/// Returns a copy of this event with a different payload.
pub fn with_payload<U>(self, payload: U) -> Event<U> {
Event {
id: self.id,
time: self.time,
payload,
}
}
/// Returns a copy of this event scheduled at a different time.
///
/// Returns `None` when the new time is not usable, which leaves the
/// original event available for the caller to keep using.
pub fn reschedule(&self, time: f64, sequence: u64) -> Option<Event<T>>
where
T: Clone,
{
Some(Event {
id: EventId::new(time, sequence)?,
time,
payload: self.payload.clone(),
})
}
}

View File

@ -1 +1,40 @@
//! Simulation event types and event queue. //! Simulation event types and event queue.
//!
//! A discrete-event simulation advances by jumping from one event to the
//! next rather than by stepping through every instant. This crate provides
//! the queue that makes that possible: a priority queue ordered by time,
//! with a well defined order among events that fall at the same instant.
//!
//! The tie-break matters more than it first appears. Two events scheduled
//! for the same instant must be delivered in a repeatable order, or the
//! simulation produces a different result on each run and a cross-check
//! against another implementation is impossible. Each event therefore
//! carries a sequence number assigned when it is scheduled, and events at
//! equal times are ordered by that number, so the order is the order in
//! which they were scheduled. That is the convention used by every
//! discrete-event simulator.
//!
//! Times are in seconds from the start of the simulation, so the queue holds
//! no knowledge of calendars or time scales.
//!
//! ```
//! use apsis_event::EventQueue;
//!
//! let mut queue = EventQueue::new();
//! queue.schedule(10.0, "second");
//! queue.schedule(5.0, "first");
//!
//! // The earliest event comes out first.
//! let event = queue.pop().expect("the queue is not empty");
//! assert_eq!(event.time, 5.0);
//! assert_eq!(event.payload, "first");
//! ```
#![forbid(unsafe_code)]
#![deny(missing_docs)]
mod event;
mod queue;
pub use event::{Event, EventId};
pub use queue::{EventQueue, QueueState};

View File

@ -0,0 +1,226 @@
//! The event queue.
use crate::event::{Event, EventId};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
/// A heap entry ordered so that the earliest event is the greatest.
///
/// The standard library heap is a maximum heap, so the comparison is
/// reversed on time. Ties on time are broken by the sequence number, and
/// that comparison is also reversed so that the earlier sequence is the
/// greater element.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Scheduled {
time_bits: u64,
sequence: u64,
}
impl Scheduled {
fn time(self) -> f64 {
f64::from_bits(self.time_bits)
}
}
impl Ord for Scheduled {
fn cmp(&self, other: &Self) -> Ordering {
// Reverse the time so that the smallest time is the greatest heap
// element. For equal times, the smaller sequence is the greater
// element, so an earlier scheduled event is delivered first.
other
.time_bits
.cmp(&self.time_bits)
.then_with(|| other.sequence.cmp(&self.sequence))
}
}
impl PartialOrd for Scheduled {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// What the queue currently holds.
///
/// The state is exposed so that a simulation loop can decide whether to
/// advance the clock or to stop without popping an event first.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct QueueState {
/// The number of pending events.
pub pending: usize,
/// The time of the earliest pending event, if there is one.
pub next_time: Option<f64>,
/// The number of events delivered so far.
pub delivered: u64,
}
impl QueueState {
/// Returns `true` when no events are pending.
pub const fn is_idle(&self) -> bool {
self.pending == 0
}
}
/// A priority queue of events ordered by time and then by schedule order.
///
/// The queue assigns the sequence numbers, so a caller cannot accidentally
/// create two events with the same identifier. Sequence numbers start at
/// zero and increase by one with every event scheduled, including events
/// scheduled from inside the handling of another event.
#[derive(Debug, Clone)]
pub struct EventQueue<T> {
pending: BinaryHeap<Scheduled>,
payloads: Vec<Option<T>>,
next_sequence: u64,
delivered: u64,
}
impl<T> Default for EventQueue<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> EventQueue<T> {
/// Creates an empty queue.
pub const fn new() -> Self {
Self {
pending: BinaryHeap::new(),
payloads: Vec::new(),
next_sequence: 0,
delivered: 0,
}
}
/// Returns the number of pending events.
pub fn len(&self) -> usize {
self.pending.len()
}
/// Returns `true` when no events are pending.
pub fn is_empty(&self) -> bool {
self.pending.is_empty()
}
/// Returns the number of events delivered so far.
pub const fn delivered(&self) -> u64 {
self.delivered
}
/// Returns what the queue currently holds.
pub fn state(&self) -> QueueState {
QueueState {
pending: self.pending.len(),
next_time: self.pending.peek().map(|entry| entry.time()),
delivered: self.delivered,
}
}
/// Returns the time of the earliest pending event.
///
/// Returns `None` when the queue is empty.
pub fn next_time(&self) -> Option<f64> {
self.pending.peek().map(|entry| entry.time())
}
/// Schedules an event at a time.
///
/// The sequence number is assigned by the queue, so the delivery order
/// among events at the same instant is the order they were scheduled.
///
/// Returns the identifier of the new event, or `None` when the time is
/// not usable: a simulation clock runs forward from zero, so a negative
/// or non-finite time is refused rather than stored.
pub fn schedule(&mut self, time: f64, payload: T) -> Option<EventId> {
let sequence = self.next_sequence;
let id = EventId::new(time, sequence)?;
self.next_sequence = self.next_sequence.wrapping_add(1);
self.pending.push(Scheduled {
time_bits: time.to_bits(),
sequence,
});
// The payload slots are indexed by sequence, so gaps are filled with
// `None` when a caller has popped an event without taking its
// payload.
let index = sequence as usize;
if index < self.payloads.len() {
self.payloads[index] = Some(payload);
} else {
while self.payloads.len() < index {
self.payloads.push(None);
}
self.payloads.push(Some(payload));
}
Some(id)
}
/// Schedules an event at a time relative to a base instant.
///
/// This is the form a handler uses when it schedules a follow-up: the
/// delay is relative to the current simulation time.
///
/// Returns `None` when the resulting time is not usable.
pub fn schedule_after(&mut self, now: f64, delay: f64, payload: T) -> Option<EventId> {
if !delay.is_finite() || delay < 0.0 {
return None;
}
self.schedule(now + delay, payload)
}
/// Removes and returns the earliest event.
///
/// Returns `None` when the queue is empty. The payload is taken from the
/// queue, so the slot is left empty; scheduling a later event reuses the
/// slot.
pub fn pop(&mut self) -> Option<Event<T>> {
let entry = self.pending.pop()?;
let index = entry.sequence as usize;
let payload = self
.payloads
.get_mut(index)
.and_then(Option::take)
.expect("a pending entry always has its payload");
self.delivered = self.delivered.wrapping_add(1);
Some(Event {
id: EventId::from_valid(entry.time(), entry.sequence),
time: entry.time(),
payload,
})
}
/// Returns a reference to the payload of the earliest event without
/// removing it.
pub fn peek(&self) -> Option<&T> {
let entry = self.pending.peek()?;
self.payloads
.get(entry.sequence as usize)
.and_then(Option::as_ref)
}
/// Removes every pending event and returns how many were discarded.
///
/// The delivered count is left alone, because it records history rather
/// than queue contents.
pub fn clear(&mut self) -> usize {
let count = self.pending.len();
self.pending.clear();
for slot in &mut self.payloads {
*slot = None;
}
count
}
/// Returns every pending event in delivery order, leaving the queue
/// empty.
///
/// The order is the order the events would have been delivered in, which
/// is what a caller wants when it records the remaining schedule at the
/// end of a run.
pub fn drain(&mut self) -> Vec<Event<T>> {
let mut events = Vec::with_capacity(self.pending.len());
while let Some(event) = self.pop() {
events.push(event);
}
events
}
}

View File

@ -0,0 +1,279 @@
//! Tests for the event queue.
use apsis_event::{Event, EventId, EventQueue};
#[test]
fn an_empty_queue_reports_itself_idle() {
let queue: EventQueue<u32> = EventQueue::new();
assert!(queue.is_empty());
assert_eq!(queue.len(), 0);
assert_eq!(queue.next_time(), None);
assert_eq!(queue.peek(), None);
assert_eq!(queue.delivered(), 0);
assert!(queue.state().is_idle());
assert_eq!(queue.state().next_time, None);
}
#[test]
fn events_are_delivered_in_time_order() {
let mut queue = EventQueue::new();
queue.schedule(30.0, "third");
queue.schedule(10.0, "first");
queue.schedule(20.0, "second");
assert_eq!(queue.len(), 3);
assert_eq!(queue.next_time(), Some(10.0));
assert_eq!(queue.peek(), Some(&"first"));
let mut order = Vec::new();
while let Some(event) = queue.pop() {
order.push(event.payload);
}
assert_eq!(order, vec!["first", "second", "third"]);
assert!(queue.is_empty());
assert_eq!(queue.delivered(), 3);
}
#[test]
fn events_at_the_same_time_are_delivered_in_schedule_order() {
// The tie-break is what makes a run reproducible.
let mut queue = EventQueue::new();
queue.schedule(10.0, 'a');
queue.schedule(10.0, 'b');
queue.schedule(10.0, 'c');
let order: Vec<char> = queue
.drain()
.into_iter()
.map(|event| event.payload)
.collect();
assert_eq!(order, vec!['a', 'b', 'c']);
}
#[test]
fn the_tie_break_holds_across_a_heap_rebuild() {
// Scheduling in a different order must still deliver in schedule order,
// because the sequence number, not the insertion position, decides.
let mut queue = EventQueue::new();
let mut ids = Vec::new();
for index in 0..20 {
ids.push(queue.schedule(5.0, index).expect("the time is usable"));
}
let delivered: Vec<u64> = queue
.drain()
.into_iter()
.map(|event| event.payload)
.collect();
assert_eq!(delivered, (0..20).collect::<Vec<u64>>());
// The identifiers are also in schedule order.
for (position, id) in ids.iter().enumerate() {
assert_eq!(id.sequence(), position as u64);
}
}
#[test]
fn an_event_carries_its_identifier_and_time() {
let mut queue = EventQueue::new();
let id = queue.schedule(12.5, "payload").expect("the time is usable");
assert_eq!(id.time(), 12.5);
assert_eq!(id.sequence(), 0);
let event = queue.pop().expect("the queue is not empty");
assert_eq!(event.id, id);
assert_eq!(event.time, 12.5);
assert_eq!(event.payload, "payload");
}
#[test]
fn scheduling_a_negative_or_non_finite_time_is_refused() {
let mut queue = EventQueue::new();
for time in [-1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(queue.schedule(time, "x"), None, "time {time}");
}
// The queue is unchanged and the sequence number was not consumed.
assert!(queue.is_empty());
let id = queue.schedule(0.0, "zero").expect("zero is usable");
assert_eq!(id.sequence(), 0);
}
#[test]
fn a_follow_up_event_is_scheduled_relative_to_now() {
let mut queue = EventQueue::new();
queue
.schedule_after(100.0, 30.0, "follow up")
.expect("the delay is usable");
let event = queue.pop().expect("the queue is not empty");
assert_eq!(event.time, 130.0);
}
#[test]
fn a_follow_up_with_a_bad_delay_is_refused() {
let mut queue = EventQueue::new();
assert_eq!(queue.schedule_after(0.0, -1.0, "x"), None);
assert_eq!(queue.schedule_after(0.0, f64::NAN, "x"), None);
assert_eq!(queue.schedule_after(0.0, f64::INFINITY, "x"), None);
assert!(queue.is_empty());
// A zero delay schedules at the current instant.
let id = queue.schedule_after(5.0, 0.0, "now").expect("usable");
assert_eq!(id.time(), 5.0);
}
#[test]
fn events_can_be_scheduled_from_a_handler() {
// A handler that schedules its successor, which is the pattern a
// discrete-event simulation uses for a periodic process.
let mut queue: EventQueue<u32> = EventQueue::new();
queue.schedule(0.0, 0).expect("usable");
let mut ticks = 0;
while let Some(event) = queue.pop() {
ticks += 1;
if event.payload < 3 {
queue
.schedule_after(event.time, 10.0, event.payload + 1)
.expect("the delay is usable");
}
}
assert_eq!(ticks, 4);
assert_eq!(queue.delivered(), 4);
}
#[test]
fn clearing_discards_the_pending_events() {
let mut queue = EventQueue::new();
for index in 0..5 {
queue.schedule(f64::from(index), index).expect("usable");
}
queue.pop().expect("the queue is not empty");
assert_eq!(queue.len(), 4);
assert_eq!(queue.delivered(), 1);
assert_eq!(queue.clear(), 4);
assert!(queue.is_empty());
// The delivered count records history and is not reset.
assert_eq!(queue.delivered(), 1);
assert_eq!(queue.pop(), None);
}
#[test]
fn draining_returns_the_events_in_delivery_order() {
let mut queue = EventQueue::new();
queue.schedule(3.0, 'c');
queue.schedule(1.0, 'a');
queue.schedule(2.0, 'b');
let events = queue.drain();
let payloads: Vec<char> = events.iter().map(|event| event.payload).collect();
assert_eq!(payloads, vec!['a', 'b', 'c']);
assert!(queue.is_empty());
assert_eq!(queue.delivered(), 3);
}
#[test]
fn the_queue_state_reports_what_is_pending() {
let mut queue = EventQueue::new();
queue.schedule(10.0, "a").expect("usable");
queue.schedule(20.0, "b").expect("usable");
let state = queue.state();
assert_eq!(state.pending, 2);
assert_eq!(state.next_time, Some(10.0));
assert_eq!(state.delivered, 0);
assert!(!state.is_idle());
queue.pop();
let state = queue.state();
assert_eq!(state.pending, 1);
assert_eq!(state.next_time, Some(20.0));
assert_eq!(state.delivered, 1);
}
#[test]
fn a_payload_slot_is_reused_after_a_pop() {
// The queue indexes payloads by sequence, so a popped slot must be
// reusable without disturbing a later event.
let mut queue = EventQueue::new();
queue.schedule(1.0, "first").expect("usable");
queue.schedule(2.0, "second").expect("usable");
let first = queue.pop().expect("non-empty");
assert_eq!(first.payload, "first");
let second = queue.pop().expect("non-empty");
assert_eq!(second.payload, "second");
assert_eq!(queue.pop(), None);
}
#[test]
fn the_identifier_orders_by_time_then_sequence() {
// The derived ordering on the identifier is the delivery order, which is
// what lets a caller sort identifiers to reproduce a run.
let earlier = EventId::new(1.0, 5).expect("usable");
let later_time = EventId::new(2.0, 0).expect("usable");
let later_sequence = EventId::new(1.0, 6).expect("usable");
assert!(earlier < later_time);
assert!(earlier < later_sequence);
assert!(EventId::new(1.0, 0).expect("usable") < earlier);
}
#[test]
fn an_identifier_reports_itself() {
let id = EventId::new(3.5, 7).expect("usable");
assert_eq!(id.time(), 3.5);
assert_eq!(id.sequence(), 7);
assert!(id.to_string().contains("7"));
assert!(id.to_string().contains("3.5"));
}
#[test]
fn an_event_reports_whether_it_is_due() {
let event = Event {
id: EventId::new(5.0, 0).expect("usable"),
time: 5.0,
payload: "x",
};
assert!(!event.is_due(4.999));
assert!(event.is_due(5.0));
assert!(event.is_due(6.0));
}
#[test]
fn an_event_can_be_rescheduled() {
let event = Event {
id: EventId::new(5.0, 0).expect("usable"),
time: 5.0,
payload: "x",
};
let moved = event.reschedule(9.0, 1).expect("the new time is usable");
assert_eq!(moved.time, 9.0);
assert_eq!(moved.id.sequence(), 1);
assert_eq!(moved.payload, "x");
// The original is unchanged.
assert_eq!(event.time, 5.0);
assert!(event.reschedule(-1.0, 2).is_none());
assert!(event.reschedule(f64::NAN, 3).is_none());
}
#[test]
fn an_event_can_change_its_payload() {
let event = Event {
id: EventId::new(5.0, 0).expect("usable"),
time: 5.0,
payload: 1_u32,
};
let changed = event.with_payload("text");
assert_eq!(changed.payload, "text");
assert_eq!(changed.time, 5.0);
}
#[test]
fn a_zero_time_event_is_the_earliest_possible() {
let mut queue = EventQueue::new();
queue.schedule(1.0, "later").expect("usable");
queue.schedule(0.0, "first").expect("usable");
assert_eq!(queue.pop().expect("non-empty").payload, "first");
}
#[test]
fn the_queue_default_is_empty() {
let queue: EventQueue<u8> = EventQueue::default();
assert!(queue.is_empty());
}