⏱️ 時間¶
実行ごとに時間軸は 1 本で、それが実行のタイマーである。各ノードの dt、各メッセージの時刻、実行の一時停止はすべてその上にある。このページではタイマー、時間の単位、時間の速さ、一時停止、再開した実行のタイマーを説明する。
タイマー¶
| 名前 | 何か |
|---|---|
mujou.time.global_timer |
自分のタイマーを渡さないすべての実行のタイマー。プロセスで初めて使ったときに 0 から始まる |
mujou.time.Timer() |
1 つの実行専用のタイマー(mujou.run(net, timer=Timer()))。start(0)から time_scale(1)で進む |
mujou.time.VirtualTimer() |
進めたとき(advance(seconds))だけ進む Timer。mujou.testing.run が使う |
mujou.time.ReadOnlyTimer |
タイマーを読むだけのビュー。ノードの self.timer がこれで、now() と time_scale() だけを持つ |
mujou.time.Timestamp |
タイマーの上の時刻 |
ホストは run.timer で実行の時間を操作する。now()・sleep(seconds)・pause()・resume()・is_paused()・time_scale()・set_time_scale(scale) がある。ノードは hook の中で self.timer.now() で読むだけ。
単位¶
| 単位 | 使うもの |
|---|---|
| 実行のタイマーの秒 | dt、period、Timer.sleep、VirtualTimer.advance、pc.show(seconds=)、mujou.pc の時定数 |
| 実時間の秒 | すべての timeout |
timeout が実時間なのは、時間の速さや一時停止によらず、止まった実行を見切れるようにするため。
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・now() は Timestamp を返す(まだ送られていない値なら None)。< などで前後を比べ、引き算でその間の秒数を得る。Timestamp はどのタイマーの時刻かを持たないので、1 つの実行の時刻とその 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 は x を書いた後に送られた
print(t_out - t_in) # 書いてから y が送られるまでの秒数
実行専用のタイマー¶
# 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)
その実行だけ時間を速めたり遅くしたりしたいとき(デバッグや実時間より速い試行)や、ほかの実行と一緒に止まらないようにしたいときに、実行専用のタイマーを渡す。時間の速さは実時間 1 秒あたりにタイマーが進む秒数で、10 なら実時間の 1 秒がタイマーの 10 秒。有限で 0 より大きい数に限る。
グローバルタイマーも速さを変えられ、それで走るすべての実行が変わる。
# 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)
時間を止める¶
timer.pause() は timer.resume() までそのタイマーの時間を止める。global_timer.pause() はグローバルタイマーで走るすべての実行を止める。止まっている間は次のようになる。
- そのタイマーで走る全実行のノードは更新せず、
resume後の最初のdtに停止中の時間は入らない。ノードには知らされない。on_pauseとon_resumeが呼ばれるのは状態の取得の前後だけ - そのタイマーの
sleepは戻らず、VirtualTimer.advanceはRuntimeErrorになる run.state_dict()は取得でき、止めた時刻の状態が返る
状態の取得も、利用者の pause とは別に実行のタイマーを止める。pause は回数を数えないスイッチで、resume を呼んでも取得中の停止は解けない。is_paused() はどちらかで止まっていれば真になる。
1 つのタイマーで走る複数の実行の状態を同じ時刻で取得するときに使う。1 つずつ取得すると、後の状態ほど時刻が遅れる。
# 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
resume は finally に書く
例外で時間が止まったままにならない。
一時停止は状態の取得と観測のためのもの。学習の手順の一部にしない。
再開した実行のタイマー¶
mujou.run(net, state=tree) で再開した実行は、木の時刻と時間の速さから始まる新しい Timer で走る。グローバルタイマーやほかの実行の時刻は戻らない。
# 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"
)
state= と一緒に timer= を渡すと ValueError になる。global_timer.pause() では再開した実行は止まらないので、その実行の run.timer を止める。そのため、同じ時刻で取得した木から再開した実行同士は、もう時間軸を共有しない。それぞれのタイマーは少しずつずれ、1 つを止めてもほかは止まらない。再開した後も 1 本の時間軸に載せたい実行は 1 つの実行(mujou.run(Group(a=a, b=b)))として走らせる。その 1 つの木は 1 つのタイマーに復元される。
ノードは時間を読むだけ¶
ノードの self.timer は実行の ReadOnlyTimer で、on_start を含むどの hook の中でも読めるが、それ以外では読めない。__init__ の中やホストから読むと RuntimeError になる。実行が終わった時刻で止まるので、on_exception と on_stop からは時間が止まって見える。
時間を動かせるのはホストだけ(ノードが守る規則)。一定間隔で更新したいなら、ノードに period を渡す。