Skip to main content

internals.loop

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