Skip to main content

ixa/triggers/
periodic_time.rs

1//! Trigger criterion for regular simulation time intervals.
2//!
3//! [`PeriodicTimeTrigger`] observes the simulation clock and emits repeatedly at a configured
4//! period and execution phase.
5//!
6//! ## Construction
7//!
8//! ```rust,ignore
9//! PeriodicTimeTrigger::every(period)
10//! PeriodicTimeTrigger::every_with_phase(period, phase)
11//! PeriodicTimeTrigger::every(period).with_phase(phase) // Equivalent to `every_with_phase`
12//! PeriodicTimeTrigger::every(period).start_with_delay(delay)
13//! PeriodicTimeTrigger::every(period).start_at(start_time)
14//! ```
15//!
16//! ## Observation
17//!
18//! The observation data passed to
19//! [`TriggerCriterion::emit_with`](super::TriggerCriterion::emit_with) is
20//! [`PeriodicTimeTriggerEvent`]. It contains the simulation time observed when the scheduled
21//! periodic plan runs, the configured period, and the phase used to schedule it:
22//!
23//! ```rust,ignore
24//! pub struct PeriodicTimeTriggerEvent {
25//!     pub time: f64,
26//!     pub period: f64,
27//!     pub phase: ExecutionPhase,
28//! }
29//! ```
30//!
31//! ## Semantics
32//!
33//! This trigger uses the same rescheduling behavior as periodic plans: when the scheduled callback
34//! runs, the next occurrence is scheduled at `current_time + period` if there are still plans in the
35//! queue. Unlike [`Context::add_periodic_plan_with_phase`](crate::Context::add_periodic_plan_with_phase),
36//! the first occurrence is seeded explicitly so it can start at the current time, after a delay, or
37//! at an absolute simulation time.
38//!
39//! By default, the first occurrence is scheduled at `context.get_current_time()` when the trigger is
40//! installed, and the execution phase is
41//! [`ExecutionPhase::Normal`](crate::ExecutionPhase::Normal).
42//!
43//! The period must be positive, finite, and not NaN. A delay must be non-negative, finite, and not
44//! NaN. An absolute start time must be finite and not NaN; the context validates at trigger
45//! installation that it is not in the past.
46//!
47//! Since time is monotonic, this criterion does not use [`Direction`](super::Direction) or
48//! [`TriggerMode`](super::TriggerMode). It emits whenever its periodic schedule executes. If several
49//! plans are scheduled for the same time, the selected [`ExecutionPhase`](crate::ExecutionPhase)
50//! controls phase ordering.
51//!
52//! ## Example
53//!
54//! ```rust
55//! use ixa::{Context, ExecutionPhase, IxaEvent};
56//! use ixa::triggers::{ContextTriggersExt, PeriodicTimeTrigger, TriggerCriterion};
57//!
58//! #[derive(IxaEvent)]
59//! struct ReportTimeReached {
60//!     time: f64,
61//!     period: f64,
62//!     phase: ExecutionPhase,
63//! }
64//!
65//! let mut context = Context::new();
66//!
67//! context.register_trigger(
68//!     PeriodicTimeTrigger::every(7.0)
69//!         .with_phase(ExecutionPhase::Last)
70//!         .start_with_delay(7.0)
71//!         .emit_with(|observation| ReportTimeReached {
72//!             time: observation.time,
73//!             period: observation.period,
74//!             phase: observation.phase,
75//!         }),
76//! );
77//!
78//! context.subscribe_to_event(|_context, _event: ReportTimeReached| {
79//!     // collect periodic reports
80//! });
81//! ```
82//!
83use super::TriggerCriterion;
84use crate::{Context, ExecutionPhase};
85
86pub struct PeriodicTimeTrigger {
87    period: f64,
88    start: PeriodicTimeTriggerStart,
89    phase: ExecutionPhase,
90}
91
92enum PeriodicTimeTriggerStart {
93    CurrentTime,
94    Delay(f64),
95    At(f64),
96}
97
98#[derive(Clone, Copy, Debug)]
99pub struct PeriodicTimeTriggerEvent {
100    pub time: f64,
101    pub period: f64,
102    pub phase: ExecutionPhase,
103}
104
105impl PeriodicTimeTrigger {
106    #[must_use]
107    pub fn every(period: f64) -> Self {
108        validate_period(period);
109        Self {
110            period,
111            start: PeriodicTimeTriggerStart::CurrentTime,
112            phase: ExecutionPhase::Normal,
113        }
114    }
115
116    #[must_use]
117    pub fn every_with_phase(period: f64, phase: ExecutionPhase) -> Self {
118        validate_period(period);
119        Self {
120            period,
121            start: PeriodicTimeTriggerStart::CurrentTime,
122            phase,
123        }
124    }
125
126    #[must_use]
127    pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
128        self.phase = phase;
129        self
130    }
131
132    #[must_use]
133    pub fn start_with_delay(mut self, delay: f64) -> Self {
134        assert!(
135            delay >= 0.0 && !delay.is_nan() && !delay.is_infinite(),
136            "delay must be greater than or equal to 0"
137        );
138        self.start = PeriodicTimeTriggerStart::Delay(delay);
139        self
140    }
141
142    #[must_use]
143    pub fn start_at(mut self, start_time: f64) -> Self {
144        assert!(
145            !start_time.is_nan(),
146            "start_time {start_time} is invalid: cannot be NaN"
147        );
148        assert!(
149            !start_time.is_infinite(),
150            "start_time {start_time} is invalid: cannot be infinite"
151        );
152        self.start = PeriodicTimeTriggerStart::At(start_time);
153        self
154    }
155}
156
157impl TriggerCriterion for PeriodicTimeTrigger {
158    type Observation = PeriodicTimeTriggerEvent;
159
160    fn install<F>(self, context: &mut Context, on_match: F)
161    where
162        F: Fn(&mut Context, Self::Observation) + 'static,
163    {
164        let start_time = match self.start {
165            PeriodicTimeTriggerStart::CurrentTime => context.get_current_time(),
166            PeriodicTimeTriggerStart::Delay(delay) => context.get_current_time() + delay,
167            PeriodicTimeTriggerStart::At(start_time) => start_time,
168        };
169        let period = self.period;
170        let phase = self.phase;
171
172        context.add_plan_with_phase(
173            start_time,
174            move |context| {
175                context.evaluate_periodic_and_schedule_next(
176                    period,
177                    move |context| {
178                        on_match(
179                            context,
180                            PeriodicTimeTriggerEvent {
181                                time: context.get_current_time(),
182                                period,
183                                phase,
184                            },
185                        );
186                    },
187                    phase,
188                );
189            },
190            phase,
191        );
192    }
193}
194
195fn validate_period(period: f64) {
196    assert!(
197        period > 0.0 && !period.is_nan() && !period.is_infinite(),
198        "period must be greater than 0"
199    );
200}