Source code for de_twin.clock
"""The twin's clock.
Every time-dependent part of the twin (stage motion, holder thermal response,
in-situ specimen evolution, frame pacing) reads time from one clock, so the
whole twin can run in real time or be fast-forwarded for automation testing.
"""
from __future__ import annotations
import threading
import time
[docs]
class Clock:
"""Real-time clock, optionally scaled.
``time_scale > 1`` makes simulated time run faster than wall time: an
in-situ anneal that takes 10 minutes on the microscope can be replayed in
one minute with ``time_scale=10``. Frame pacing still sleeps in *wall*
time (``frame_time / time_scale``), so the whole twin speeds up consistently.
"""
[docs]
def __init__(self, time_scale: float = 1.0):
if time_scale <= 0:
raise ValueError("time_scale must be > 0")
self.time_scale = float(time_scale)
self._t0_wall = time.perf_counter()
self._t0_sim = 0.0
self._lock = threading.Lock()
def __call__(self) -> float:
return self.now()
[docs]
def now(self) -> float:
"""Simulated seconds since the clock was created."""
with self._lock:
return self._t0_sim + (time.perf_counter() - self._t0_wall) * self.time_scale
[docs]
def set_time_scale(self, time_scale: float) -> None:
with self._lock:
now_wall = time.perf_counter()
self._t0_sim += (now_wall - self._t0_wall) * self.time_scale
self._t0_wall = now_wall
self.time_scale = float(time_scale)
[docs]
def sleep(self, sim_seconds: float) -> None:
if sim_seconds > 0:
time.sleep(sim_seconds / self.time_scale)
[docs]
def sleep_until(self, sim_time: float) -> None:
self.sleep(sim_time - self.now())
@property
def virtual(self) -> bool:
return False
[docs]
class ManualClock(Clock):
"""Clock that only moves when told to (deterministic tests, offline studies).
``sleep`` advances the clock instead of blocking, so paced frame loops run
as fast as the CPU allows while still seeing correct simulated time.
"""
[docs]
def __init__(self, start: float = 0.0):
self._t = float(start)
self._lock = threading.Lock()
self.time_scale = float("inf")
[docs]
def now(self) -> float:
with self._lock:
return self._t
[docs]
def advance(self, seconds: float) -> float:
with self._lock:
self._t += max(0.0, float(seconds))
return self._t
[docs]
def set(self, t: float) -> None:
with self._lock:
self._t = float(t)
[docs]
def set_time_scale(self, time_scale: float) -> None: # pragma: no cover - no-op
pass
[docs]
def sleep(self, sim_seconds: float) -> None:
if sim_seconds > 0:
self.advance(sim_seconds)
@property
def virtual(self) -> bool:
return True