Skip to main content

simular/engine/
scheduler.rs

1//! Event scheduler with deterministic ordering.
2//!
3//! Implements a priority queue that ensures:
4//! - Events are processed in time order
5//! - Ties are broken by insertion order (sequence number)
6//! - Reproducible across runs
7
8use serde::{Deserialize, Serialize};
9use std::cmp::Reverse;
10use std::collections::BinaryHeap;
11
12use crate::engine::state::SimEvent;
13use crate::engine::SimTime;
14
15/// A scheduled event with time and sequence number.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ScheduledEvent {
18    /// Scheduled time.
19    pub time: SimTime,
20    /// Sequence number for deterministic tie-breaking.
21    pub sequence: u64,
22    /// The event to execute.
23    pub event: SimEvent,
24}
25
26impl ScheduledEvent {
27    /// Create a new scheduled event.
28    #[must_use]
29    pub const fn new(time: SimTime, sequence: u64, event: SimEvent) -> Self {
30        Self {
31            time,
32            sequence,
33            event,
34        }
35    }
36}
37
38// Custom ordering for BinaryHeap (min-heap by time, then sequence)
39impl PartialEq for ScheduledEvent {
40    fn eq(&self, other: &Self) -> bool {
41        self.time == other.time && self.sequence == other.sequence
42    }
43}
44
45impl Eq for ScheduledEvent {}
46
47impl PartialOrd for ScheduledEvent {
48    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
49        Some(self.cmp(other))
50    }
51}
52
53impl Ord for ScheduledEvent {
54    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
55        // First by time, then by sequence
56        match self.time.cmp(&other.time) {
57            std::cmp::Ordering::Equal => self.sequence.cmp(&other.sequence),
58            ord => ord,
59        }
60    }
61}
62
63/// Priority-ordered event queue.
64///
65/// Ensures deterministic processing order:
66/// 1. Events are sorted by time
67/// 2. Ties are broken by sequence number (insertion order)
68///
69/// # Example
70///
71/// ```rust
72/// use simular::engine::scheduler::EventScheduler;
73/// use simular::engine::SimTime;
74/// use simular::engine::state::{SimEvent, Vec3};
75///
76/// let mut scheduler = EventScheduler::new();
77///
78/// scheduler.schedule(
79///     SimTime::from_secs(1.0),
80///     SimEvent::AddBody {
81///         mass: 1.0,
82///         position: Vec3::zero(),
83///         velocity: Vec3::zero(),
84///     },
85/// );
86/// ```
87#[derive(Debug, Default)]
88pub struct EventScheduler {
89    /// Min-heap ordered by (time, sequence).
90    queue: BinaryHeap<Reverse<ScheduledEvent>>,
91    /// Monotonic sequence counter for tie-breaking.
92    sequence: u64,
93}
94
95impl EventScheduler {
96    /// Create a new event scheduler.
97    #[must_use]
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Schedule an event at the given time.
103    pub fn schedule(&mut self, time: SimTime, event: SimEvent) {
104        let seq = self.sequence;
105        self.sequence += 1;
106
107        self.queue
108            .push(Reverse(ScheduledEvent::new(time, seq, event)));
109    }
110
111    /// Schedule multiple events at the same time.
112    ///
113    /// Events will be processed in the order they appear in the slice.
114    pub fn schedule_all(&mut self, time: SimTime, events: &[SimEvent]) {
115        for event in events {
116            self.schedule(time, event.clone());
117        }
118    }
119
120    /// Get the next event (removes from queue).
121    #[must_use]
122    #[allow(clippy::should_implement_trait)] // Not an Iterator, different semantics
123    pub fn next(&mut self) -> Option<ScheduledEvent> {
124        self.queue.pop().map(|Reverse(e)| e)
125    }
126
127    /// Peek at the next event without removing it.
128    #[must_use]
129    pub fn peek(&self) -> Option<&ScheduledEvent> {
130        self.queue.peek().map(|Reverse(e)| e)
131    }
132
133    /// Get the next event if its time is before or at the given time.
134    #[must_use]
135    pub fn next_before(&mut self, time: SimTime) -> Option<ScheduledEvent> {
136        if let Some(Reverse(e)) = self.queue.peek() {
137            if e.time <= time {
138                return self.next();
139            }
140        }
141        None
142    }
143
144    /// Get all events up to and including the given time.
145    #[must_use]
146    pub fn drain_until(&mut self, time: SimTime) -> Vec<ScheduledEvent> {
147        let mut events = Vec::new();
148
149        while let Some(event) = self.next_before(time) {
150            events.push(event);
151        }
152
153        events
154    }
155
156    /// Check if the queue is empty.
157    #[must_use]
158    pub fn is_empty(&self) -> bool {
159        self.queue.is_empty()
160    }
161
162    /// Get the number of pending events.
163    #[must_use]
164    pub fn len(&self) -> usize {
165        self.queue.len()
166    }
167
168    /// Clear all pending events.
169    pub fn clear(&mut self) {
170        self.queue.clear();
171    }
172
173    /// Get the time of the next event, if any.
174    #[must_use]
175    pub fn next_event_time(&self) -> Option<SimTime> {
176        self.peek().map(|e| e.time)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::engine::state::Vec3;
184
185    fn make_add_body_event(mass: f64) -> SimEvent {
186        SimEvent::AddBody {
187            mass,
188            position: Vec3::zero(),
189            velocity: Vec3::zero(),
190        }
191    }
192
193    #[test]
194    fn test_scheduler_time_ordering() {
195        let mut scheduler = EventScheduler::new();
196
197        // Schedule events out of order
198        scheduler.schedule(SimTime::from_secs(3.0), make_add_body_event(3.0));
199        scheduler.schedule(SimTime::from_secs(1.0), make_add_body_event(1.0));
200        scheduler.schedule(SimTime::from_secs(2.0), make_add_body_event(2.0));
201
202        // Should come out in time order
203        let e1 = scheduler.next();
204        assert!(e1.is_some());
205        assert!(
206            (e1.as_ref().map(|e| e.time.as_secs_f64()).unwrap_or(0.0) - 1.0).abs() < f64::EPSILON
207        );
208
209        let e2 = scheduler.next();
210        assert!(
211            (e2.as_ref().map(|e| e.time.as_secs_f64()).unwrap_or(0.0) - 2.0).abs() < f64::EPSILON
212        );
213
214        let e3 = scheduler.next();
215        assert!(
216            (e3.as_ref().map(|e| e.time.as_secs_f64()).unwrap_or(0.0) - 3.0).abs() < f64::EPSILON
217        );
218
219        assert!(scheduler.is_empty());
220    }
221
222    #[test]
223    fn test_scheduler_sequence_ordering() {
224        let mut scheduler = EventScheduler::new();
225
226        // Schedule multiple events at same time
227        let time = SimTime::from_secs(1.0);
228        scheduler.schedule(time, make_add_body_event(1.0));
229        scheduler.schedule(time, make_add_body_event(2.0));
230        scheduler.schedule(time, make_add_body_event(3.0));
231
232        // Should come out in insertion order (sequence)
233        if let Some(e) = scheduler.next() {
234            if let SimEvent::AddBody { mass, .. } = e.event {
235                assert!((mass - 1.0).abs() < f64::EPSILON);
236            }
237        }
238
239        if let Some(e) = scheduler.next() {
240            if let SimEvent::AddBody { mass, .. } = e.event {
241                assert!((mass - 2.0).abs() < f64::EPSILON);
242            }
243        }
244
245        if let Some(e) = scheduler.next() {
246            if let SimEvent::AddBody { mass, .. } = e.event {
247                assert!((mass - 3.0).abs() < f64::EPSILON);
248            }
249        }
250    }
251
252    #[test]
253    fn test_scheduler_next_before() {
254        let mut scheduler = EventScheduler::new();
255
256        scheduler.schedule(SimTime::from_secs(1.0), make_add_body_event(1.0));
257        scheduler.schedule(SimTime::from_secs(2.0), make_add_body_event(2.0));
258        scheduler.schedule(SimTime::from_secs(3.0), make_add_body_event(3.0));
259
260        // Get events up to time 1.5
261        let e1 = scheduler.next_before(SimTime::from_secs(1.5));
262        assert!(e1.is_some());
263        assert!((e1.map(|e| e.time.as_secs_f64()).unwrap_or(0.0) - 1.0).abs() < f64::EPSILON);
264
265        // No more events before 1.5
266        let e2 = scheduler.next_before(SimTime::from_secs(1.5));
267        assert!(e2.is_none());
268
269        // But there are events at 2.0
270        let e3 = scheduler.next_before(SimTime::from_secs(2.0));
271        assert!(e3.is_some());
272    }
273
274    #[test]
275    fn test_scheduler_drain_until() {
276        let mut scheduler = EventScheduler::new();
277
278        for i in 1..=5 {
279            scheduler.schedule(SimTime::from_secs(i as f64), make_add_body_event(i as f64));
280        }
281
282        let events = scheduler.drain_until(SimTime::from_secs(3.0));
283        assert_eq!(events.len(), 3);
284        assert_eq!(scheduler.len(), 2);
285    }
286
287    #[test]
288    fn test_scheduler_peek() {
289        let mut scheduler = EventScheduler::new();
290
291        assert!(scheduler.peek().is_none());
292
293        scheduler.schedule(SimTime::from_secs(1.0), make_add_body_event(1.0));
294
295        // Peek doesn't remove
296        assert!(scheduler.peek().is_some());
297        assert!(scheduler.peek().is_some());
298        assert_eq!(scheduler.len(), 1);
299
300        // Next removes
301        let _ = scheduler.next();
302        assert!(scheduler.peek().is_none());
303    }
304
305    #[test]
306    fn test_scheduler_clear() {
307        let mut scheduler = EventScheduler::new();
308
309        for i in 1..=10 {
310            scheduler.schedule(SimTime::from_secs(i as f64), make_add_body_event(i as f64));
311        }
312
313        assert_eq!(scheduler.len(), 10);
314
315        scheduler.clear();
316
317        assert!(scheduler.is_empty());
318        assert_eq!(scheduler.len(), 0);
319    }
320
321    #[test]
322    fn test_scheduler_schedule_all() {
323        let mut scheduler = EventScheduler::new();
324        let events = vec![
325            make_add_body_event(1.0),
326            make_add_body_event(2.0),
327            make_add_body_event(3.0),
328        ];
329
330        scheduler.schedule_all(SimTime::from_secs(1.0), &events);
331        assert_eq!(scheduler.len(), 3);
332
333        // All events should be at the same time, but in insertion order
334        let mut masses = Vec::new();
335        while let Some(e) = scheduler.next() {
336            if let SimEvent::AddBody { mass, .. } = e.event {
337                masses.push(mass);
338            }
339        }
340        assert_eq!(masses, vec![1.0, 2.0, 3.0]);
341    }
342
343    #[test]
344    fn test_scheduler_next_event_time() {
345        let mut scheduler = EventScheduler::new();
346
347        assert!(scheduler.next_event_time().is_none());
348
349        scheduler.schedule(SimTime::from_secs(2.5), make_add_body_event(1.0));
350        scheduler.schedule(SimTime::from_secs(1.0), make_add_body_event(2.0));
351
352        // Should return earliest event time
353        let next_time = scheduler.next_event_time();
354        assert!(next_time.is_some());
355        assert!((next_time.map_or(0.0, |t| t.as_secs_f64()) - 1.0).abs() < f64::EPSILON);
356    }
357
358    #[test]
359    fn test_scheduled_event_new() {
360        let event = ScheduledEvent::new(SimTime::from_secs(1.0), 42, make_add_body_event(5.0));
361
362        assert!((event.time.as_secs_f64() - 1.0).abs() < f64::EPSILON);
363        assert_eq!(event.sequence, 42);
364    }
365
366    #[test]
367    fn test_scheduled_event_eq() {
368        let e1 = ScheduledEvent::new(SimTime::from_secs(1.0), 1, make_add_body_event(1.0));
369        let e2 = ScheduledEvent::new(SimTime::from_secs(1.0), 1, make_add_body_event(2.0));
370        let e3 = ScheduledEvent::new(SimTime::from_secs(1.0), 2, make_add_body_event(1.0));
371        let e4 = ScheduledEvent::new(SimTime::from_secs(2.0), 1, make_add_body_event(1.0));
372
373        // Same time and sequence = equal (event content ignored)
374        assert_eq!(e1, e2);
375        // Different sequence = not equal
376        assert_ne!(e1, e3);
377        // Different time = not equal
378        assert_ne!(e1, e4);
379    }
380
381    #[test]
382    fn test_scheduled_event_ord() {
383        let earlier = ScheduledEvent::new(SimTime::from_secs(1.0), 1, make_add_body_event(1.0));
384        let later = ScheduledEvent::new(SimTime::from_secs(2.0), 1, make_add_body_event(1.0));
385        let same_time_seq1 =
386            ScheduledEvent::new(SimTime::from_secs(1.0), 1, make_add_body_event(1.0));
387        let same_time_seq2 =
388            ScheduledEvent::new(SimTime::from_secs(1.0), 2, make_add_body_event(1.0));
389
390        assert!(earlier < later);
391        assert!(same_time_seq1 < same_time_seq2);
392    }
393
394    #[test]
395    fn test_scheduled_event_partial_ord() {
396        let e1 = ScheduledEvent::new(SimTime::from_secs(1.0), 1, make_add_body_event(1.0));
397        let e2 = ScheduledEvent::new(SimTime::from_secs(2.0), 1, make_add_body_event(1.0));
398
399        assert!(e1.partial_cmp(&e2).is_some());
400        assert!(e1 < e2);
401    }
402
403    #[test]
404    fn test_scheduled_event_clone() {
405        let event = ScheduledEvent::new(SimTime::from_secs(1.0), 5, make_add_body_event(3.0));
406        let cloned = event.clone();
407
408        assert_eq!(event.time, cloned.time);
409        assert_eq!(event.sequence, cloned.sequence);
410    }
411
412    #[test]
413    fn test_scheduled_event_debug() {
414        let event = ScheduledEvent::new(SimTime::from_secs(1.0), 5, make_add_body_event(3.0));
415        let debug = format!("{:?}", event);
416        assert!(debug.contains("ScheduledEvent"));
417    }
418
419    #[test]
420    fn test_scheduler_default() {
421        let scheduler: EventScheduler = Default::default();
422        assert!(scheduler.is_empty());
423        assert_eq!(scheduler.len(), 0);
424    }
425
426    #[test]
427    fn test_scheduler_debug() {
428        let scheduler = EventScheduler::new();
429        let debug = format!("{:?}", scheduler);
430        assert!(debug.contains("EventScheduler"));
431    }
432
433    #[test]
434    fn test_scheduler_next_before_empty() {
435        let mut scheduler = EventScheduler::new();
436        assert!(scheduler.next_before(SimTime::from_secs(1.0)).is_none());
437    }
438
439    #[test]
440    fn test_scheduler_drain_until_empty() {
441        let mut scheduler = EventScheduler::new();
442        let events = scheduler.drain_until(SimTime::from_secs(1.0));
443        assert!(events.is_empty());
444    }
445}
446
447#[cfg(test)]
448mod proptests {
449    use super::*;
450    use crate::engine::state::Vec3;
451    use proptest::prelude::*;
452
453    fn make_event(mass: f64) -> SimEvent {
454        SimEvent::AddBody {
455            mass,
456            position: Vec3::zero(),
457            velocity: Vec3::zero(),
458        }
459    }
460
461    proptest! {
462        /// Falsification: events always come out in time order.
463        #[test]
464        fn prop_time_ordering(times in prop::collection::vec(0.0f64..1000.0, 1..100)) {
465            let mut scheduler = EventScheduler::new();
466
467            for (i, &t) in times.iter().enumerate() {
468                scheduler.schedule(SimTime::from_secs(t), make_event(i as f64));
469            }
470
471            let mut last_time = 0.0;
472            while let Some(event) = scheduler.next() {
473                let current_time = event.time.as_secs_f64();
474                prop_assert!(current_time >= last_time, "Events not in time order");
475                last_time = current_time;
476            }
477        }
478
479        /// Falsification: drain_until gets correct count.
480        #[test]
481        fn prop_drain_count(
482            times in prop::collection::vec(0.0f64..100.0, 1..50),
483            threshold in 0.0f64..100.0,
484        ) {
485            let mut scheduler = EventScheduler::new();
486
487            for (i, &t) in times.iter().enumerate() {
488                scheduler.schedule(SimTime::from_secs(t), make_event(i as f64));
489            }
490
491            let expected_count = times.iter().filter(|&&t| t <= threshold).count();
492            let events = scheduler.drain_until(SimTime::from_secs(threshold));
493
494            prop_assert_eq!(events.len(), expected_count);
495        }
496    }
497}