internals.loop
The integration loop: the order in which each module’s code runs in each timestep
It exists so that modules with different timesteps run together in one fixed order, which can be inspected with sim.loop.
Used with: agents and compartments; declared and coded modules
Classes
| Name | Description |
|---|---|
| Loop | Define the integration loop |
| LoopEntry | One executable event in the integration loop |
Loop
internals.loop.Loop(sim)Define the integration loop
The Loop handles the order in which each function is called in the sim. The order is defined in Loop.collect_funcs(), which searches through the sim and collects all methods to call, in order, in the integration loop.
Each type of module is called at a different time. Within each module type, they are called in the order listed. The default loop order is:
1. sim: advance() # Move the time to this step's time point (from the second step on)
2. sim: start_step() # Initialize the sim, including plotting progress
3. all modules: start_step() # Initialize the modules, including the random number distribution
4. people: record_states() # Record the basic states at the start of the step
5. all modules: record_states() # Record each module's states at the start of the step
6. sim.custom: step() # Run any custom modules
7. sim.demographics: step() # Update the demographics, including adding new agents
8. sim.diseases: step_state() # Update the disease states, e.g. exposed -> infected
9. sim.connectors: step() # Run the connectors
10. sim.networks: step() # Run the networks, including adding/removing edges
11. sim.interventions: step() # Run the interventions
12. sim.diseases: step() # Run the diseases, including transmission
13. people: step_die() # Figure out who died on this timestep
14. people: record_events() # Record who died or left in the step
15. all modules: record_events() # Record what happened in the step
16. sim.analyzers: step() # Run the analyzers
17. all modules: finish_step() # Do any final tidying
18. people: finish_step() # Clean up dead agents
19. sim: finish_step() # Finish the step
With ss.Sim(step_order='given'), steps 4 to 10 are instead each module’s step in the order the modules were given (a disease’s step_state() and then its step()).
A sim that is nothing but compartments the kernels can run from start to finish goes through them in one call instead, not this order step by step (see sim.describe()).
One time index holds for every function of a step, in every module: the step from time point k to k+1 is step k, and throughout it sim.ti == k and sim.now is time point k, which is when the step happens, what it reads time-varying inputs and dates at, and where its results go. A step is taken from every time point, the last included, so each time point is a period: its stocks are the state at its start (recorded by record_states(), before any module’s step), and its flows the totals over the step (recorded by record_events(), after them). The state after the last step is not recorded.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| sim | Sim | the sim to run (usually made by the sim itself) | required |
Attributes
| Name | Description |
|---|---|
| plan | The list of LoopEntry events in execution order; made the first time it is needed, since a sim run by the kernels in one call never needs it |
Methods
| Name | Description |
|---|---|
| collect_abs_tvecs | Collect time arrays for each module |
| collect_by_type | The steps of the modules, each kind at its place in the loop (the default; see ss.Loop) |
| collect_funcs | Collect all the callable functions (methods) that comprise the step |
| collect_given | The steps of the modules in the order they were given in, with ss.Sim(step_order='given'); a disease takes its state changes and then its step together, and analyzers still step last |
| init | Parse the sim into the integration plan |
| insert | Insert a function into the loop plan at the specified location. |
| make_plan | Combine the module ordering and the time vectors into the integration plan |
| plan_metadata | Return the dataframe view of the plan used for matching and display |
| plot | Plot a diagram of all the events |
| plot_cpu | Plot the CPU time spent on each event; visualization of Loop.cpu_df. |
| run | Actually run the integration loop; usually called by sim.run() |
| run_one_step | Take a single step, i.e. call a single function; only used for debugging purposes. |
| shrink | Shrink the size of the loop for saving to disk |
| store_time | Store the time elapsed since start in as high resolution as possible (only called when profiling) |
| to_df | Return a user-friendly version of the plan, omitting object columns |
collect_abs_tvecs
internals.loop.Loop.collect_abs_tvecs()Collect time arrays for each module
Two parallel arrays are stored per module: abs_tvecs holds the ground-truth time objects (ss.date/ss.Duration, used verbatim as each plan entry’s time), and abs_numvecs holds the canonical numeric representation (float years). The numeric arrays are what make_plan() uses to decide uniformity and to sort, so that date/dur objects are never compared during plan construction.
collect_by_type
internals.loop.Loop.collect_by_type(sim)The steps of the modules, each kind at its place in the loop (the default; see ss.Loop)
collect_funcs
internals.loop.Loop.collect_funcs()Collect all the callable functions (methods) that comprise the step
collect_given
internals.loop.Loop.collect_given(sim)The steps of the modules in the order they were given in, with ss.Sim(step_order='given'); a disease takes its state changes and then its step together, and analyzers still step last
init
internals.loop.Loop.init()Parse the sim into the integration plan
insert
internals.loop.Loop.insert(func, label=None, match_fn=None, before=False)Insert a function into the loop plan at the specified location.
The loop plan metadata view is a dataframe with columns including time (e.g. date('2025-05-05')), label (e.g. 'randomnet.step'), module (’randomnet'), and function name ('step'). By default, this method will match the conditions in the plan based on the criteria specified.
This functionality is similar to an analyzer or an intervention, but gives additional flexibility since can be inserted at (almost) any point in a sim.
Note: the loop must be initialized (sim.init()) before you can call this.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| func | func / Module |
the function to insert, which must take a single argument, sim; or a module of the sim, whose step() is moved there |
required |
| label | str |
the label (module.name) of the function to match; see sim.loop.to_df().label.unique() for choices |
None |
| match_fn | func |
if supplied, use this function to perform the matching on the plan dataframe, returning a boolean array or list of indices of matching rows (see example below) | None |
| before | bool |
if true, insert the function before rather than after the match | False |
Examples
# Simple label matching with analyzer-like functionality
def check_pop_size(sim):
print(f'Population size is {sim.people.n:n}')
sim = ss.Sim(diseases='sir', networks='random', demographics=True)
sim.init()
sim.loop.insert(check_pop_size, label='people.finish_step')
sim.run()
# Function-based matching with intervention-like functionality
def match_fn(plan):
past_2010 = plan.time > ss.date(2010)
is_step = (plan.label == 'sis.step') | (plan.label == 'randomnet.step')
return past_2010 * is_step
def update_betas(sim):
if not sim.metadata.get('updated'):
print(f'Updating beta values on {sim.now}')
sim.diseases.sis.pars.beta = 0.1
sim.networks.randomnet.edges.weight[:] = 0.5
sim.metadata.updated = True
return
sim = ss.Sim(diseases='sis', networks='random')
sim.init()
sim.loop.insert(update_betas, match_fn=match_fn, before=True)
sim.run()make_plan
internals.loop.Loop.make_plan()Combine the module ordering and the time vectors into the integration plan
The plan is the list of LoopEntry events in execution order: tick-major, and within each tick ordered by function order. When every module shares the sim’s canonical timeline (the common case) this order can be generated directly, with no sort. Otherwise a numeric sort is used. In neither case are date/dur objects compared (which is slow); the canonical numeric time vectors (abs_numvecs) are used instead.
plan_metadata
internals.loop.Loop.plan_metadata()Return the dataframe view of the plan used for matching and display
plot
internals.loop.Loop.plot(
simplify=False,
max_len=100,
fig_kw=None,
plot_kw=None,
scatter_kw=None,
)Plot a diagram of all the events
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| simplify | bool |
if True, skip advance, record_states, record_events, and finish_step events, which are automatically applied | False |
| max_len | int |
maximum number of entries to plot | 100 |
| fig_kw | dict |
passed to plt.figure() |
None |
| plot_kw | dict |
passed to plt.plot() |
None |
| scatter_kw | dict |
passed to plt.scatter() |
None |
plot_cpu
internals.loop.Loop.plot_cpu(
bytime=True,
max_entries=10,
fig_kw=None,
bar_kw=None,
)Plot the CPU time spent on each event; visualization of Loop.cpu_df.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| bytime | bool |
if True, order events by total time rather than actual order | True |
| max_entries | int |
the number of events to show | 10 |
| fig_kw | dict |
passed to plt.figure() |
None |
| bar_kw | dict |
passed to plt.bar() |
None |
run
internals.loop.Loop.run(until=None, verbose=None, profile=None)Actually run the integration loop; usually called by sim.run()
By default, per-entry CPU timing is not recorded and the plan DataFrame is not built at the end of the run – both are pure overhead (~15–25 ms for a bare 10-year sim) that most runs do not use. Pass profile=True to record per-entry timing (which to_df()/plot_cpu() then expose as the cpu_time column); otherwise cpu_time is NaN. The plan metadata (via to_df()) is always available on demand regardless.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| until | str / date / Expr / func |
if a time, stop after the step that starts at it (used by sim.run_one_step); if a condition (see sim.run()), stop once it holds at the end of a step |
None |
| verbose | bool |
if True, print each function call as it runs | None |
| profile | bool |
if True, record per-entry CPU timing (default: keep the current setting) | None |
run_one_step
internals.loop.Loop.run_one_step()Take a single step, i.e. call a single function; only used for debugging purposes.
Compare sim.run_one_step(), which runs a full timestep (which involves multiple function calls).
shrink
internals.loop.Loop.shrink()Shrink the size of the loop for saving to disk
store_time
internals.loop.Loop.store_time(start)Store the time elapsed since start in as high resolution as possible (only called when profiling)
to_df
internals.loop.Loop.to_df()Return a user-friendly version of the plan, omitting object columns
The cpu_time column is populated only if the run recorded per-entry timing (i.e. sim.profile(kind="loop")); otherwise it is NaN. This is built lazily and cached in self.df/self.cpu_df.
LoopEntry
internals.loop.LoopEntry(time, ti, func_order, func, label, module, func_name)One executable event in the integration loop
It exists to give each step of the plan its time, module, and function in one small record.
Note: slots=True is used primarily to reduce the memory footprint across the 10⁴–10⁵ instances created for a typical plan (no per-instance __dict__).
Functions
| Name | Description |
|---|---|
| empty_function | Whether a function does nothing: pass, with or without a docstring |
empty_function
internals.loop.empty_function(func)Whether a function does nothing: pass, with or without a docstring