When developing a movement model you will likely need to inspects its inner-workings to fix issues and verify it does what you expect it to do. Here are a few useful methods for doing so.
Running evaluate yourself
A good place to start is verifying that your clauses’ evaluate functions produce results as expected. But because these functions are almost certain to depend on the simulation context, we have to find a way to supply that context. Normally, these would come from being run with a fully-defined RUME, but in this case that might be pretty tedious — defining a bunch of extra info we don’t really need. To evaluate a simulation function we only have to provide the info it is actually going to use, and since we wrote the clause we probably have a pretty good idea of what’s needed. Note you don’t evaluate a whole model, but rather individual clauses. Here’s how we can inspect the clause from Centroids:
from datetime import dateimport numpy as npfrom epymorph.data.mm.centroids import CentroidsClausefrom epymorph.simulation import Tickfrom epymorph.adrio import us_tiger, acs5from epymorph.geography.us_census import StateScope# 1. Instantiate the clause object.clause = CentroidsClause()# 2. Provide the parts of the simulation context that will be used:# Movement clauses always use the geo scope, and this one uses the RNG.# Additionally we specify parameter values, just like we would for a RUME.clause_with_context = clause.with_context( scope=StateScope.in_states(["AZ", "CO", "NM", "UT"], year=2020), params={"centroid": us_tiger.InternalPoint(),"phi": 40.0,"commuter_proportion": 0.1, }, rng=np.random.default_rng(42),)# 3. `with_context` returns a new instance of the clause with the context values# set, so it's ready for evaluation.clause_with_context.evaluate(# We just have to define which `Tick` we want to evaluate.# This clause ignores the tick, so it doesn't matter but it might matter# for your use-case. tick=Tick( sim_index=0, day=0, date=date(2020, 1, 1), step=0, tau=1/3, ), available=[5_000_000] *4,# NOTE: it's easier to simplify the available population here)
Some situations may require you to inspect the detailed movement data from a simulation run. This is a lot of information which isn’t typically useful, so epymorph doesn’t record it in the simulation output. But we do have a way to capture it: run your simulation inside a movement_data context.
from epymorph.kit import ( ipm, mm, init, BasicSimulator, SingleStrataRUME, StateScope, TimeFrame,)from epymorph.adrio import acs5, us_tigerfrom epymorph.log.movement import movement_data# A typical RUME setup:rume = SingleStrataRUME.build( ipm=ipm.SIRS(), mm=mm.Centroids(), scope=StateScope.in_states(["AZ", "CO", "NM", "UT"], year=2020), init=init.SingleLocation(location=0, seed_size=5_000), time_frame=TimeFrame.rangex("2020-01-01", "2020-02-01"), params={"beta": 0.4,"gamma": 1/4,"xi": 1/45,"centroid": us_tiger.InternalPoint(),"phi": 40.0,"commuter_proportion": 0.1,"population": acs5.Population(), },)# But now we run it in a `movement_data` context:with movement_data() as mm_data: sim = BasicSimulator(rume) output = sim.run()
Movement that happens inside the context will be collected and stored in the mm_data object. Its methods will allow you to inspect both requested and actual movement for all time steps in the simulation, by clause or totaled for the whole movement model.
Here’s the movement requested by the CentroidsClause (the output from its evaluate method):
requested = mm_data.requested_by("CentroidsClause")print(f"{requested.shape=}")print("\n--- requested movement at time step 0")print(requested[0, ...])print("\n--- requested movement at time step 1 (clause isn't triggered!)")print(requested[1, ...])
requested.shape=(62, 4, 4)
--- requested movement at time step 0
[[ 0 3 266 97]
[ 3 0 230 161]
[ 78 79 0 2]
[ 45 89 1 0]]
--- requested movement at time step 1 (clause isn't triggered!)
[[0 0 0 0]
[0 0 0 0]
[0 0 0 0]
[0 0 0 0]]
And here’s how epymorph fulfilled those requests:
actual = mm_data.actual_by("CentroidsClause")print(f"\n{actual.shape=}")print("\n--- actual movement at time step 0 (compartments aggregated)")print(actual[0, ...].sum(-1))print("\n--- actual movement at time step 1 (compartments aggregated)")print(actual[1, ...].sum(-1))
actual.shape=(62, 4, 4, 3)
--- actual movement at time step 0 (compartments aggregated)
[[ 0 3 252 111]
[ 4 0 224 166]
[ 70 86 0 3]
[ 41 94 0 0]]
--- actual movement at time step 1 (compartments aggregated)
[[0 0 0 0]
[0 0 0 0]
[0 0 0 0]
[0 0 0 0]]