Skip to main content

simular/engine/
clock.rs

1//! Simulation clock management.
2//!
3//! Handles time progression with support for:
4//! - Fixed timestep mode
5//! - Adaptive timestep mode (future)
6//! - Time bounds and limits
7
8use crate::engine::SimTime;
9use serde::{Deserialize, Serialize};
10
11/// Simulation clock.
12///
13/// Manages time progression through the simulation.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SimClock {
16    /// Current simulation time.
17    current: SimTime,
18    /// Timestep duration in nanoseconds.
19    timestep_nanos: u64,
20    /// Number of steps taken.
21    step_count: u64,
22    /// Maximum simulation time (optional limit).
23    max_time: Option<SimTime>,
24}
25
26impl SimClock {
27    /// Create a new clock with the given timestep in seconds.
28    ///
29    /// # Panics
30    ///
31    /// Panics if timestep is not positive or not finite.
32    #[must_use]
33    pub fn new(timestep_secs: f64) -> Self {
34        assert!(timestep_secs > 0.0, "Timestep must be positive");
35        assert!(timestep_secs.is_finite(), "Timestep must be finite");
36
37        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
38        let timestep_nanos = (timestep_secs * 1_000_000_000.0) as u64;
39
40        Self {
41            current: SimTime::ZERO,
42            timestep_nanos,
43            step_count: 0,
44            max_time: None,
45        }
46    }
47
48    /// Create a new clock with timestep in nanoseconds.
49    #[must_use]
50    pub const fn from_nanos(timestep_nanos: u64) -> Self {
51        Self {
52            current: SimTime::ZERO,
53            timestep_nanos,
54            step_count: 0,
55            max_time: None,
56        }
57    }
58
59    /// Get current simulation time.
60    #[must_use]
61    pub const fn current_time(&self) -> SimTime {
62        self.current
63    }
64
65    /// Get timestep duration as seconds.
66    #[must_use]
67    pub fn timestep_secs(&self) -> f64 {
68        contract_pre_iterator!();
69        let result = self.timestep_nanos as f64 / 1_000_000_000.0;
70        contract_post_configuration!(&"ok");
71        result
72    }
73
74    /// Alias for `timestep_secs`.
75    #[must_use]
76    pub fn dt(&self) -> f64 {
77        self.timestep_secs()
78    }
79
80    /// Get timestep duration in nanoseconds.
81    #[must_use]
82    pub const fn timestep_nanos(&self) -> u64 {
83        self.timestep_nanos
84    }
85
86    /// Get number of steps taken.
87    #[must_use]
88    pub const fn step_count(&self) -> u64 {
89        self.step_count
90    }
91
92    /// Set maximum simulation time.
93    #[allow(clippy::missing_const_for_fn)] // Mutable const not stable
94    pub fn set_max_time(&mut self, max: SimTime) {
95        self.max_time = Some(max);
96    }
97
98    /// Check if simulation has reached max time.
99    #[must_use]
100    pub fn at_max_time(&self) -> bool {
101        self.max_time.is_some_and(|max| self.current >= max)
102    }
103
104    /// Advance clock by one timestep.
105    ///
106    /// Returns the new time.
107    #[allow(clippy::missing_const_for_fn)] // Mutable const not stable
108    pub fn tick(&mut self) -> SimTime {
109        self.current = self.current.add_nanos(self.timestep_nanos);
110        self.step_count += 1;
111        self.current
112    }
113
114    /// Advance clock by multiple timesteps.
115    ///
116    /// Returns the new time.
117    pub fn tick_n(&mut self, n: u64) -> SimTime {
118        for _ in 0..n {
119            self.tick();
120        }
121        self.current
122    }
123
124    /// Set current time (for replay/restore).
125    #[allow(clippy::missing_const_for_fn)] // Mutable const not stable
126    pub fn set_time(&mut self, time: SimTime) {
127        self.current = time;
128    }
129
130    /// Reset clock to initial state.
131    #[allow(clippy::missing_const_for_fn)] // Mutable const not stable
132    pub fn reset(&mut self) {
133        self.current = SimTime::ZERO;
134        self.step_count = 0;
135    }
136
137    /// Calculate time until a target time.
138    #[must_use]
139    pub fn time_until(&self, target: SimTime) -> SimTime {
140        if target > self.current {
141            target - self.current
142        } else {
143            SimTime::ZERO
144        }
145    }
146
147    /// Calculate number of steps to reach target time.
148    #[must_use]
149    pub fn steps_until(&self, target: SimTime) -> u64 {
150        contract_pre_iterator!();
151        let time_diff = self.time_until(target);
152        let nanos = time_diff.as_nanos();
153
154        if self.timestep_nanos == 0 {
155            contract_post_configuration!(&"ok");
156            return 0;
157        }
158
159        let result = nanos.div_ceil(self.timestep_nanos);
160        contract_post_configuration!(&"ok");
161        result
162    }
163}
164
165impl Default for SimClock {
166    fn default() -> Self {
167        // Default 1ms timestep
168        Self::from_nanos(1_000_000)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_clock_creation() {
178        let clock = SimClock::new(0.001); // 1ms
179
180        assert_eq!(clock.current_time(), SimTime::ZERO);
181        assert!((clock.timestep_secs() - 0.001).abs() < 1e-9);
182        assert_eq!(clock.step_count(), 0);
183    }
184
185    #[test]
186    fn test_clock_tick() {
187        let mut clock = SimClock::new(0.001);
188
189        clock.tick();
190        assert_eq!(clock.step_count(), 1);
191        assert!((clock.current_time().as_secs_f64() - 0.001).abs() < 1e-9);
192
193        clock.tick();
194        assert_eq!(clock.step_count(), 2);
195        assert!((clock.current_time().as_secs_f64() - 0.002).abs() < 1e-9);
196    }
197
198    #[test]
199    fn test_clock_tick_n() {
200        let mut clock = SimClock::new(0.001);
201
202        clock.tick_n(100);
203        assert_eq!(clock.step_count(), 100);
204        assert!((clock.current_time().as_secs_f64() - 0.1).abs() < 1e-9);
205    }
206
207    #[test]
208    fn test_clock_max_time() {
209        let mut clock = SimClock::new(0.1);
210        clock.set_max_time(SimTime::from_secs(0.5));
211
212        assert!(!clock.at_max_time());
213
214        clock.tick_n(4);
215        assert!(!clock.at_max_time());
216
217        clock.tick();
218        assert!(clock.at_max_time());
219    }
220
221    #[test]
222    fn test_clock_reset() {
223        let mut clock = SimClock::new(0.001);
224
225        clock.tick_n(100);
226        assert!(clock.step_count() > 0);
227
228        clock.reset();
229        assert_eq!(clock.step_count(), 0);
230        assert_eq!(clock.current_time(), SimTime::ZERO);
231    }
232
233    #[test]
234    fn test_clock_time_until() {
235        let mut clock = SimClock::new(0.001);
236        clock.tick_n(10); // Now at 0.01s
237
238        let until = clock.time_until(SimTime::from_secs(0.1));
239        assert!((until.as_secs_f64() - 0.09).abs() < 1e-9);
240
241        // Past time returns zero
242        let until_past = clock.time_until(SimTime::from_secs(0.005));
243        assert_eq!(until_past, SimTime::ZERO);
244    }
245
246    #[test]
247    fn test_clock_steps_until() {
248        let clock = SimClock::new(0.01); // 10ms steps
249
250        // 1 second = 100 steps
251        let steps = clock.steps_until(SimTime::from_secs(1.0));
252        assert_eq!(steps, 100);
253
254        // Partial step rounds up
255        let steps2 = clock.steps_until(SimTime::from_secs(0.015));
256        assert_eq!(steps2, 2); // 15ms needs 2 steps of 10ms
257    }
258
259    #[test]
260    fn test_clock_set_time() {
261        let mut clock = SimClock::new(0.001);
262
263        clock.set_time(SimTime::from_secs(5.0));
264        assert!((clock.current_time().as_secs_f64() - 5.0).abs() < 1e-9);
265    }
266
267    #[test]
268    fn test_clock_from_nanos() {
269        let clock = SimClock::from_nanos(1_000_000); // 1ms
270
271        assert_eq!(clock.current_time(), SimTime::ZERO);
272        assert_eq!(clock.timestep_nanos(), 1_000_000);
273        assert!((clock.timestep_secs() - 0.001).abs() < 1e-9);
274        assert_eq!(clock.step_count(), 0);
275    }
276
277    #[test]
278    fn test_clock_default() {
279        let clock = SimClock::default();
280
281        assert_eq!(clock.timestep_nanos(), 1_000_000); // Default 1ms
282        assert_eq!(clock.current_time(), SimTime::ZERO);
283        assert_eq!(clock.step_count(), 0);
284    }
285
286    #[test]
287    fn test_clock_steps_until_zero_timestep() {
288        // Create clock with zero timestep (edge case)
289        let clock = SimClock::from_nanos(0);
290        let steps = clock.steps_until(SimTime::from_secs(1.0));
291        assert_eq!(steps, 0); // Should handle gracefully
292    }
293
294    #[test]
295    fn test_clock_at_max_time_no_max() {
296        let clock = SimClock::new(0.001);
297        assert!(!clock.at_max_time()); // No max set, should be false
298    }
299
300    #[test]
301    fn test_clock_timestep_nanos_accessor() {
302        let clock = SimClock::new(0.5); // 500ms
303        assert_eq!(clock.timestep_nanos(), 500_000_000);
304    }
305
306    #[test]
307    fn test_clock_tick_returns_new_time() {
308        let mut clock = SimClock::new(0.1);
309        let new_time = clock.tick();
310        assert!((new_time.as_secs_f64() - 0.1).abs() < 1e-9);
311    }
312
313    #[test]
314    fn test_clock_tick_n_returns_final_time() {
315        let mut clock = SimClock::new(0.1);
316        let final_time = clock.tick_n(5);
317        assert!((final_time.as_secs_f64() - 0.5).abs() < 1e-9);
318    }
319
320    #[test]
321    fn test_clock_clone() {
322        let clock = SimClock::new(0.001);
323        let cloned = clock.clone();
324        assert_eq!(cloned.timestep_nanos(), clock.timestep_nanos());
325        assert_eq!(cloned.current_time(), clock.current_time());
326    }
327
328    #[test]
329    fn test_clock_debug() {
330        let clock = SimClock::new(0.001);
331        let debug = format!("{:?}", clock);
332        assert!(debug.contains("SimClock"));
333    }
334}
335
336#[cfg(test)]
337mod proptests {
338    use super::*;
339    use proptest::prelude::*;
340
341    proptest! {
342        /// Falsification: time always increases after tick.
343        #[test]
344        fn prop_time_increases(timestep in 0.0001f64..1.0, ticks in 1u64..1000) {
345            let mut clock = SimClock::new(timestep);
346            let initial = clock.current_time();
347
348            clock.tick_n(ticks);
349
350            prop_assert!(clock.current_time() > initial);
351        }
352
353        /// Falsification: step count equals number of ticks.
354        #[test]
355        fn prop_step_count_accurate(timestep in 0.0001f64..1.0, ticks in 0u64..1000) {
356            let mut clock = SimClock::new(timestep);
357
358            clock.tick_n(ticks);
359
360            prop_assert_eq!(clock.step_count(), ticks);
361        }
362
363        /// Falsification: time advances by correct amount.
364        #[test]
365        fn prop_time_advance_correct(timestep in 0.0001f64..0.1, ticks in 1u64..100) {
366            let mut clock = SimClock::new(timestep);
367
368            clock.tick_n(ticks);
369
370            let expected = timestep * ticks as f64;
371            let actual = clock.current_time().as_secs_f64();
372
373            // Allow floating point error due to nanosecond quantization
374            // The error comes from converting f64 timestep to u64 nanos and back
375            let tolerance = 1e-6 * expected.max(1.0);
376            prop_assert!((actual - expected).abs() < tolerance,
377                "Expected {}, got {}, diff {}", expected, actual, (actual - expected).abs());
378        }
379    }
380}