🧪 3. Test a Node¶
In a run started with mujou.run, dt follows the real time that passes, and the order in which the nodes update differs every time, so the results differ slightly from run to run. This page tests the node of 2. Write a Node with mujou.testing, which runs nodes with the same result every time.
The tools¶
Import it with import mujou.testing; import mujou alone does not.
| Name | Role |
|---|---|
mujou.testing.run(root, ...) |
Runs the tree with the timer, inputs and state of mujou.run (no workers) and returns a ManualRun. No node updates until step updates it. It runs on a new mujou.time.VirtualTimer by default |
ManualRun.step(dt, *nodes) |
Moves the timer dt seconds forward, then updates the given nodes once each, in the given order |
ManualRun.timer |
The run's VirtualTimer |
ManualRun is a Run, so write, read, state_dict and the rest work as with mujou.run.
A test¶
class TestLeaky:
def test_follows_the_exact_solution(self) -> None:
tau, h, steps = 0.1, 0.001, 100
node = Leaky(tau)
one = torch.tensor(1.0, dtype=torch.float64)
expected = 0.0
with mujou.testing.run(node, inputs={node.prev: one}) as run:
for _ in range(steps):
run.step(h) # 1 ms later, then every node once (period is ignored)
expected += (1 - math.exp(-h / tau)) * (1.0 - expected)
y, t = run.read_with_time(node.next)
assert t is not None and t.ns == steps * 1_000_000 # sent at the last step
torch.testing.assert_close(y, torch.tensor(expected, dtype=torch.float64))
mujou.testing.run(node, inputs=...)runs the node on a newVirtualTimerat time 0.inputsare the values of the input ports from the start- Each
run.step(h)moves the timehseconds forward and updates every node once, so thedtof the node is exactlyh - The reference is the exact solution, computed with the same coefficient. Compare it with
torch.testing.assert_close, or withtorch.equalwhen the reference does the same operations in the same order (below) - The time the node sent the output is
stepsmilliseconds: no real time enters a manual run
The order of the updates¶
class TestLeakyChain:
def test_follows_the_discrete_algorithm(self) -> None:
tau, h, steps = 0.01, 0.001, 50
u = torch.tensor(1.0, dtype=torch.float64)
# The reference: the discrete algorithm, a then b at every step.
a, b = SCALAR.zeros(), SCALAR.zeros()
for _ in range(steps):
a += (1 - math.exp(-h / tau)) * (u - a)
b += (1 - math.exp(-h / tau)) * (a - b) # b reads the new a
net = LeakyChain(tau)
with mujou.testing.run(net, inputs={net.prev: u}) as run:
for _ in range(steps):
run.step(h, net.a, net.b) # a, then b, which reads a's new value
y = run.read(net.next)
assert torch.equal(y, b) # the same operations: equal to the last bit
step(dt)without nodes updates every node once, in the order of the tree (the order the attributes were assigned)- The order changes the result. With
step(h, net.a, net.b),breads the valueasent in this step; withstep(h, net.b, net.a), the one from the step before - A node's
dtis the time since its last update, the first from the start of the run. A node passed twice in onestepupdates once: the second time is skipped, since its time has not moved - The period of a node is not used in a manual run
- A node that raises makes
stepraiseNetworkErrorat once
Save and restore¶
A run that takes the state midway and goes on in a new instance matches a run that went on without stopping, to the last bit:
def run_steps(n: int, state: dict[str, Any] | None = None) -> dict[str, Any]:
"""Run a new ``LeakyChain`` for ``n`` steps of 1 ms and return its state."""
net = LeakyChain(0.01)
u = torch.tensor(1.0, dtype=torch.float64)
with mujou.testing.run(net, inputs={net.prev: u}, state=state) as run:
for _ in range(n):
run.step(0.001)
return run.state_dict()
class TestRestore:
def test_a_restored_run_goes_on_as_one_that_did_not_stop(self) -> None:
straight = run_steps(20)
# Ten steps, then ten more in a new instance restored from the first half,
# on a VirtualTimer of its own that starts at the time of the state.
resumed = run_steps(10, state=run_steps(10))
x = resumed["nodes"]["b"]["state"]["x"]
assert torch.equal(x, straight["nodes"]["b"]["state"]["x"])
A run restored from a state runs on a VirtualTimer of its own, which starts at the time of the state.
Exceptions¶
When a node raises, step raises NetworkError, whose __cause__ is the node's exception. Every later call of the run raises it again, including the shutdown at the end of the with block, so put pytest.raises around the whole block, in the same with statement:
with pytest.raises(NetworkError) as info, mujou.testing.run(net) as run:
run.step(0.001) # a node raises ValueError here
assert isinstance(info.value.__cause__, ValueError)
Other conditions for determinism¶
- Give a
seedto the nodes that draw random numbers, and draw them fromself.generator(see Threads and Devices) - Move the time only with
step. Nothing is sent unlessstepis called, sowait_forandsubscribewithout atimeoutwait forever on the thread that steps - To put several runs on one time axis, pass the same
mujou.time.VirtualTimer()to each: astepof one moves the others too
Next¶
- A node that learns: 4. Learn in a Node