use crate::engine::SimTime;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimClock {
current: SimTime,
timestep_nanos: u64,
step_count: u64,
max_time: Option<SimTime>,
}
impl SimClock {
#[must_use]
pub fn new(timestep_secs: f64) -> Self {
assert!(timestep_secs > 0.0, "Timestep must be positive");
assert!(timestep_secs.is_finite(), "Timestep must be finite");
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let timestep_nanos = (timestep_secs * 1_000_000_000.0) as u64;
Self {
current: SimTime::ZERO,
timestep_nanos,
step_count: 0,
max_time: None,
}
}
#[must_use]
pub const fn from_nanos(timestep_nanos: u64) -> Self {
Self {
current: SimTime::ZERO,
timestep_nanos,
step_count: 0,
max_time: None,
}
}
#[must_use]
pub const fn current_time(&self) -> SimTime {
self.current
}
#[must_use]
pub fn timestep_secs(&self) -> f64 {
contract_pre_iterator!();
let result = self.timestep_nanos as f64 / 1_000_000_000.0;
contract_post_configuration!(&"ok");
result
}
#[must_use]
pub fn dt(&self) -> f64 {
self.timestep_secs()
}
#[must_use]
pub const fn timestep_nanos(&self) -> u64 {
self.timestep_nanos
}
#[must_use]
pub const fn step_count(&self) -> u64 {
self.step_count
}
#[allow(clippy::missing_const_for_fn)] pub fn set_max_time(&mut self, max: SimTime) {
self.max_time = Some(max);
}
#[must_use]
pub fn at_max_time(&self) -> bool {
self.max_time.is_some_and(|max| self.current >= max)
}
#[allow(clippy::missing_const_for_fn)] pub fn tick(&mut self) -> SimTime {
self.current = self.current.add_nanos(self.timestep_nanos);
self.step_count += 1;
self.current
}
pub fn tick_n(&mut self, n: u64) -> SimTime {
for _ in 0..n {
self.tick();
}
self.current
}
#[allow(clippy::missing_const_for_fn)] pub fn set_time(&mut self, time: SimTime) {
self.current = time;
}
#[allow(clippy::missing_const_for_fn)] pub fn reset(&mut self) {
self.current = SimTime::ZERO;
self.step_count = 0;
}
#[must_use]
pub fn time_until(&self, target: SimTime) -> SimTime {
if target > self.current {
target - self.current
} else {
SimTime::ZERO
}
}
#[must_use]
pub fn steps_until(&self, target: SimTime) -> u64 {
contract_pre_iterator!();
let time_diff = self.time_until(target);
let nanos = time_diff.as_nanos();
if self.timestep_nanos == 0 {
contract_post_configuration!(&"ok");
return 0;
}
let result = nanos.div_ceil(self.timestep_nanos);
contract_post_configuration!(&"ok");
result
}
}
impl Default for SimClock {
fn default() -> Self {
Self::from_nanos(1_000_000)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clock_creation() {
let clock = SimClock::new(0.001);
assert_eq!(clock.current_time(), SimTime::ZERO);
assert!((clock.timestep_secs() - 0.001).abs() < 1e-9);
assert_eq!(clock.step_count(), 0);
}
#[test]
fn test_clock_tick() {
let mut clock = SimClock::new(0.001);
clock.tick();
assert_eq!(clock.step_count(), 1);
assert!((clock.current_time().as_secs_f64() - 0.001).abs() < 1e-9);
clock.tick();
assert_eq!(clock.step_count(), 2);
assert!((clock.current_time().as_secs_f64() - 0.002).abs() < 1e-9);
}
#[test]
fn test_clock_tick_n() {
let mut clock = SimClock::new(0.001);
clock.tick_n(100);
assert_eq!(clock.step_count(), 100);
assert!((clock.current_time().as_secs_f64() - 0.1).abs() < 1e-9);
}
#[test]
fn test_clock_max_time() {
let mut clock = SimClock::new(0.1);
clock.set_max_time(SimTime::from_secs(0.5));
assert!(!clock.at_max_time());
clock.tick_n(4);
assert!(!clock.at_max_time());
clock.tick();
assert!(clock.at_max_time());
}
#[test]
fn test_clock_reset() {
let mut clock = SimClock::new(0.001);
clock.tick_n(100);
assert!(clock.step_count() > 0);
clock.reset();
assert_eq!(clock.step_count(), 0);
assert_eq!(clock.current_time(), SimTime::ZERO);
}
#[test]
fn test_clock_time_until() {
let mut clock = SimClock::new(0.001);
clock.tick_n(10);
let until = clock.time_until(SimTime::from_secs(0.1));
assert!((until.as_secs_f64() - 0.09).abs() < 1e-9);
let until_past = clock.time_until(SimTime::from_secs(0.005));
assert_eq!(until_past, SimTime::ZERO);
}
#[test]
fn test_clock_steps_until() {
let clock = SimClock::new(0.01);
let steps = clock.steps_until(SimTime::from_secs(1.0));
assert_eq!(steps, 100);
let steps2 = clock.steps_until(SimTime::from_secs(0.015));
assert_eq!(steps2, 2); }
#[test]
fn test_clock_set_time() {
let mut clock = SimClock::new(0.001);
clock.set_time(SimTime::from_secs(5.0));
assert!((clock.current_time().as_secs_f64() - 5.0).abs() < 1e-9);
}
#[test]
fn test_clock_from_nanos() {
let clock = SimClock::from_nanos(1_000_000);
assert_eq!(clock.current_time(), SimTime::ZERO);
assert_eq!(clock.timestep_nanos(), 1_000_000);
assert!((clock.timestep_secs() - 0.001).abs() < 1e-9);
assert_eq!(clock.step_count(), 0);
}
#[test]
fn test_clock_default() {
let clock = SimClock::default();
assert_eq!(clock.timestep_nanos(), 1_000_000); assert_eq!(clock.current_time(), SimTime::ZERO);
assert_eq!(clock.step_count(), 0);
}
#[test]
fn test_clock_steps_until_zero_timestep() {
let clock = SimClock::from_nanos(0);
let steps = clock.steps_until(SimTime::from_secs(1.0));
assert_eq!(steps, 0); }
#[test]
fn test_clock_at_max_time_no_max() {
let clock = SimClock::new(0.001);
assert!(!clock.at_max_time()); }
#[test]
fn test_clock_timestep_nanos_accessor() {
let clock = SimClock::new(0.5); assert_eq!(clock.timestep_nanos(), 500_000_000);
}
#[test]
fn test_clock_tick_returns_new_time() {
let mut clock = SimClock::new(0.1);
let new_time = clock.tick();
assert!((new_time.as_secs_f64() - 0.1).abs() < 1e-9);
}
#[test]
fn test_clock_tick_n_returns_final_time() {
let mut clock = SimClock::new(0.1);
let final_time = clock.tick_n(5);
assert!((final_time.as_secs_f64() - 0.5).abs() < 1e-9);
}
#[test]
fn test_clock_clone() {
let clock = SimClock::new(0.001);
let cloned = clock.clone();
assert_eq!(cloned.timestep_nanos(), clock.timestep_nanos());
assert_eq!(cloned.current_time(), clock.current_time());
}
#[test]
fn test_clock_debug() {
let clock = SimClock::new(0.001);
let debug = format!("{:?}", clock);
assert!(debug.contains("SimClock"));
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_time_increases(timestep in 0.0001f64..1.0, ticks in 1u64..1000) {
let mut clock = SimClock::new(timestep);
let initial = clock.current_time();
clock.tick_n(ticks);
prop_assert!(clock.current_time() > initial);
}
#[test]
fn prop_step_count_accurate(timestep in 0.0001f64..1.0, ticks in 0u64..1000) {
let mut clock = SimClock::new(timestep);
clock.tick_n(ticks);
prop_assert_eq!(clock.step_count(), ticks);
}
#[test]
fn prop_time_advance_correct(timestep in 0.0001f64..0.1, ticks in 1u64..100) {
let mut clock = SimClock::new(timestep);
clock.tick_n(ticks);
let expected = timestep * ticks as f64;
let actual = clock.current_time().as_secs_f64();
let tolerance = 1e-6 * expected.max(1.0);
prop_assert!((actual - expected).abs() < tolerance,
"Expected {}, got {}, diff {}", expected, actual, (actual - expected).abs());
}
}
}