Source code for simcraft.core.clock

"""
Simulation clock for time management.

The Clock class provides centralized time tracking for the simulation,
supporting various time units and warmup period handling.
"""

from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional


[docs] class TimeUnit(Enum): """Enumeration of supported time units.""" SECONDS = "seconds" MINUTES = "minutes" HOURS = "hours" DAYS = "days"
[docs] def to_hours(self, value: float) -> float: """Convert value in this unit to hours.""" multipliers = { TimeUnit.SECONDS: 1 / 3600, TimeUnit.MINUTES: 1 / 60, TimeUnit.HOURS: 1.0, TimeUnit.DAYS: 24.0, } return value * multipliers[self]
[docs] def from_hours(self, hours: float) -> float: """Convert hours to this time unit.""" divisors = { TimeUnit.SECONDS: 1 / 3600, TimeUnit.MINUTES: 1 / 60, TimeUnit.HOURS: 1.0, TimeUnit.DAYS: 24.0, } return hours / divisors[self]
[docs] @dataclass class Clock: """ Simulation clock for centralized time management. The clock tracks the current simulation time, warmup status, and provides utilities for time conversion and formatting. Attributes ---------- now : float Current simulation time in the base time unit (hours by default) time_unit : TimeUnit The base time unit for the simulation start_datetime : Optional[datetime] Optional real-world start time for datetime mapping warmup_end : float Time at which warmup period ends (statistics start being collected) is_warmed_up : bool Whether the simulation has completed its warmup period Examples -------- >>> clock = Clock() >>> clock.advance(1.5) # Advance 1.5 hours >>> print(clock.now) 1.5 >>> print(clock.now_in(TimeUnit.MINUTES)) 90.0 """ now: float = 0.0 time_unit: TimeUnit = TimeUnit.HOURS start_datetime: Optional[datetime] = None warmup_end: float = 0.0 _initial_time: float = field(default=0.0, repr=False) def __post_init__(self) -> None: """Initialize internal state.""" self._initial_time = self.now @property def is_warmed_up(self) -> bool: """Check if simulation has passed warmup period.""" return self.now >= self.warmup_end @property def elapsed(self) -> float: """Get total elapsed simulation time.""" return self.now - self._initial_time @property def elapsed_since_warmup(self) -> float: """Get elapsed time since warmup ended.""" if not self.is_warmed_up: return 0.0 return self.now - self.warmup_end
[docs] def advance(self, delta: float) -> None: """ Advance the clock by a time delta. Parameters ---------- delta : float Time to advance in base time units """ if delta < 0: raise ValueError("Cannot advance clock by negative time") self.now += delta
[docs] def advance_to(self, time: float) -> None: """ Advance the clock to a specific time. Parameters ---------- time : float Target time in base time units Raises ------ ValueError If target time is before current time """ if time < self.now: raise ValueError(f"Cannot move clock backwards from {self.now} to {time}") self.now = time
[docs] def now_in(self, unit: TimeUnit) -> float: """ Get current time in specified units. Parameters ---------- unit : TimeUnit Target time unit Returns ------- float Current time converted to specified unit """ hours = self.time_unit.to_hours(self.now) return unit.from_hours(hours)
[docs] def convert(self, value: float, from_unit: TimeUnit, to_unit: TimeUnit) -> float: """ Convert time value between units. Parameters ---------- value : float Time value to convert from_unit : TimeUnit Source time unit to_unit : TimeUnit Target time unit Returns ------- float Converted time value """ hours = from_unit.to_hours(value) return to_unit.from_hours(hours)
@property def datetime_now(self) -> Optional[datetime]: """ Get current simulation time as real-world datetime. Returns None if start_datetime is not set. """ if self.start_datetime is None: return None hours = self.time_unit.to_hours(self.now) return self.start_datetime + timedelta(hours=hours)
[docs] def set_warmup(self, duration: float) -> None: """ Set warmup period duration from current time. Parameters ---------- duration : float Warmup duration in base time units """ self.warmup_end = self.now + duration
[docs] def reset(self) -> None: """Reset clock to initial state.""" self.now = self._initial_time self.warmup_end = 0.0
def __str__(self) -> str: """Return string representation of current time.""" if self.start_datetime: return f"Clock(now={self.now:.4f} {self.time_unit.value}, datetime={self.datetime_now})" return f"Clock(now={self.now:.4f} {self.time_unit.value})" def __repr__(self) -> str: """Return detailed representation.""" return ( f"Clock(now={self.now}, time_unit={self.time_unit}, " f"warmup_end={self.warmup_end}, is_warmed_up={self.is_warmed_up})" )