コンテンツにスキップ

⏱️ 時間

実行ごとに時間軸は 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 を渡す。