⏱️ Time¶
Each run has one time axis, its timer: every node's dt, every message's time and every pause of the run is on it. This page covers the timers, their units, the time scale, pausing, and the timer of a resumed run.
The timers¶
| Name | What it is |
|---|---|
mujou.time.global_timer |
The timer of every run not given one of its own. It starts at 0 when the process first uses it |
mujou.time.Timer() |
A timer for a run of its own: mujou.run(net, timer=Timer()). It starts at start (0) with time_scale (1) |
mujou.time.VirtualTimer() |
A Timer that moves only when advanced (advance(seconds)); mujou.testing.run uses one |
mujou.time.ReadOnlyTimer |
The reading side of a timer: what a node sees as self.timer, with only now() and time_scale() |
mujou.time.Timestamp |
A time on a timer |
The host controls the time of a run through run.timer: now(), sleep(seconds), pause(), resume(), is_paused(), time_scale() and set_time_scale(scale). A node only reads it, with self.timer.now(), in its hooks.
Units¶
| Unit | Used by |
|---|---|
| Seconds of the run's timer | dt, period, Timer.sleep, VirtualTimer.advance, pc.show(seconds=), the time constants of mujou.pc |
| Real seconds | Every timeout |
A timeout is real time so that a stuck run is given up on whatever its time scale and pauses.
Timestamp¶
# A Timestamp: float(t) is seconds, .ns the exact nanoseconds; the difference of
# two is the seconds between them, a float (negative if the first is earlier).
t0, t1 = Timestamp(ns=1_500_000_000), Timestamp(ns=2_000_000_000)
assert float(t0) == 1.5 and t0.ns == 1_500_000_000
assert t1 - t0 == 0.5 and t0 - t1 == -0.5
write, read_with_time, subscribe, wait_for, recv_with_time and now() return Timestamps (or None for a value not sent yet). Compare them with < and the like, and subtract them for the seconds between them. A Timestamp does not record which timer it is on: compare only times of one timer, such as those of one run and its run.timer.now().
t_in = run.write({net.prev: x})
run.timer.sleep(0.1)
y, t_out = run.read_with_time(net.next)
if t_out is not None and t_out >= t_in: # y was sent after x was written
print(t_out - t_in) # seconds from the write until y was sent
A timer of a run's own¶
# A run on a timer of its own, ten times faster than the real time. Pausing this
# timer (or taking this run's state) stops only this run, not the global timer.
timer = Timer(time_scale=10.0) # starts at 0
clock = Clock()
start_real, start_global = time.perf_counter(), global_timer.now()
with mujou.run(clock, timer=timer) as run:
run.timer.sleep(2.0) # 2 s of the run's time: about 0.2 s of real time
# When the clock updates depends on how the threads get scheduled: wait until
# it has sent a time from 2 s on. The timeout is in real seconds.
seen, t = run.wait_for(clock.next, lambda now: now.item() >= 2.0, timeout=5.0)
assert t is not None # None only for the zeros before the first send
elapsed_real = time.perf_counter() - start_real
elapsed_global = global_timer.now() - start_global
print(
f"run's time {float(t):.2f} s, real {elapsed_real:.2f} s, "
f"global {elapsed_global:.2f} s"
)
# Compare times of the same timer only: a Timestamp does not say which it is on.
assert seen.item() <= float(t)
Give a run a timer of its own to speed up or slow down only that run (for debugging, or for trials faster than real time), or to keep it from pausing with other runs. The time scale is how many seconds of the timer pass per real second: at 10, a real second is 10 seconds of the timer. It is a finite number greater than 0.
The global timer can be scaled too, which changes every run on it:
# The global timer can be scaled too; it then applies to every run on it.
global_timer.set_time_scale(5.0)
start_real = time.perf_counter()
global_timer.sleep(0.5) # 0.5 s of the global time: about 0.1 s of real time
print(f"slept 0.5 s of global time in {time.perf_counter() - start_real:.2f} s real")
global_timer.set_time_scale(1.0)
Pausing the time¶
timer.pause() stops the time of the timer until timer.resume(); global_timer.pause() stops every run on the global timer. While it is paused:
- the nodes of every run on the timer do not update, and the first
dtafterresumedoes not count the pause. The nodes are not told:on_pauseandon_resumerun only around taking the state sleepon the timer does not return, andVirtualTimer.advanceraisesRuntimeErrorrun.state_dict()still works, and gives the state at the time of the pause
Taking the state pauses the run's timer too, apart from your pause: pause is a switch, not a count, and resume does not cut a pause for the state short. is_paused() is true while either one holds the time.
Use it to take the states of several runs on one timer at one time. Taken one after the other, the second state would be of a later time:
# Two runs on the global timer, such as two agents that should be saved together.
# Taken one after the other, the second state would be of a later time; pausing
# the timer they share stops both at one time T.
a, b = Clock(), Clock()
with mujou.run(a) as run_a, mujou.run(b) as run_b:
global_timer.sleep(0.1)
global_timer.pause() # the global time stops at one time T
try:
tree_a = run_a.state_dict() # still works while paused
tree_b = run_b.state_dict()
finally:
global_timer.resume() # the time goes on from T
t_ns = tree_a["timer"]["time_ns"]
assert tree_b["timer"]["time_ns"] == t_ns # both are of T
data = pickle.dumps({"a": tree_a, "b": tree_b}) # save them together
Put resume in finally
Then an exception does not leave the time stopped.
A pause is for taking the state and for observing. Do not make pausing part of how a network learns.
The timer of a resumed run¶
A run resumed with mujou.run(net, state=tree) runs on a new Timer that starts at the time and the time scale of the tree, so the global timer and the other runs do not go back:
# A run restored from one of them goes on from T on a timer of its own: the
# global timer does not go back.
saved = pickle.loads(data)
restored = Clock()
with mujou.run(restored, state=saved["a"]) as run:
assert run.timer is not global_timer
now, _ = run.wait_for(
restored.next, lambda now: now.item() > t_ns / 1e9, timeout=5.0
)
# After the run, its nodes' attributes are the host's to read.
at_save = saved["a"]["nodes"][""]["state"]["elapsed"].item()
print(
f"run a went on from t = {t_ns / 1e9:.3f} s to {now.item():.3f} s; "
f"its dt summed to {at_save:.3f} s at the save, {restored.elapsed.item():.3f} s now"
)
Passing timer= with state= raises ValueError, and global_timer.pause() does not pause a resumed run: pause its run.timer. Runs resumed from trees taken at one time therefore no longer share a time axis: their timers drift apart, and pausing one does not pause the others. Runs that must stay on one axis after a resume run as one run (mujou.run(Group(a=a, b=b))), whose single tree resumes onto one timer.
Nodes only read the time¶
A node's self.timer is a ReadOnlyTimer of its run, readable in every hook (on_start included) and nowhere else: reading it in __init__ or from the host raises RuntimeError. It stops at the time the run ends, so on_exception and on_stop see the time stand still.
Only the host moves the time (rules a node keeps). To update at intervals, give the node a period.