def step_state(self):
""" Progress infectious -> recovered """
= (self.infected & (self.ti_recovered <= self.ti)).uids
recovered self.infected[recovered] = False
self.susceptible[recovered] = True
self.update_immunity()
return
The Sim object
As noted on the Model Structure page, the Sim
is the main object for doing things with Starsim. Once you’ve created your modules for diseases and interventions, you will add them to the Sim
, which will ensure that everything gets run in a sensible order, and that everything is nicely accessible afterwards.
On this page, we explain what happens when you create a Sim
, and the particular order of steps taken when a Sim
is run. We then provide a few common examples of how to create and run a Sim
and when/why you might choose each one.
Creating and initializing a Sim
When you first create a sim, e.g. with sim = ss.Sim()
, the resulting Sim
object will be mostly empty with a few pre-set defaults. Most of the initialization is done when sim.init()
is called. This completes the initialization with the following steps:
- Validating the parameters;
- Adding a
Time
module to the sim (stored undersim.t
), which looks after converting parameters between timesteps; - Adding
People
to theSim
: if these are not supplied directly, Starsim will automatically addPeople
with default age/sex structure; - Adding all the parameters and results from each
Module
to theSim
, and adding the states from eachModule
to thePeople
; - Initializing any distributions contained anywhere within the parameters or states.
A Sim
is automatically initialized if sim.run()
is called.
Running a Sim
Order of operations
An important function of the Sim
class is that it controls the order in which the different steps of each module are executed. Starsim allows users lots of flexibility regarding the timesteps for individual modules. For example, a common usage pattern is that you may only want to update your demographic modules once a year, but you may want to update your disease modules once per month, week, or day. The Sim
class stores a plan for updating each module in the right order and with the right frequency. This is stored within a specialized Loop
class housed within the Sim
. Users typically won’t interact with this class directly, but it is worth knowing the order in which updates are made on each timestep.
Modules
have astart_step
method which is called first. This usually just advances random number generators forward.- Any
Demographic
modules have theirstep
method called - Any
Disease
modules have theirstep_state
method called - Any
Connectors
have theirstep
method called - Any
Networks
have theirstep
method called - Any
Interventions
have theirstep
method called - Any
Disease
modules have theirstep
method called - The
People
class has itsstep_die
method called: this updates any people who died on this time step. Deaths are handled by modules, not byPeople
directly, butstep_die
method assembles all the causes of death across disease modules and demographic modules and reconciles them. This ensures that each agent’s death is uniquely attributable to a cause. Results
are updated for thePeople
and theModules
- Any
Analyzer
modules have theirstep
method called - Calls are made to
finish_step
for theModules
, thePeople
, and theSim
.
If you want to see more details, have a look at sim.loop.to_df()
(after calling sim.init()
) and it will show a dataframe of every step in the simulation. You can also use sim.loop.plot()
to visualize each step.
Note that there are two separate update calls for diseases, step_state
and then step
. The step_state
method happens before transmission, and the step
method typically handles transmission. The step_state
method typically handles disease progression, as in this example from the SIR model:
To illustrate the ordering of all these updates, let’s consider an example of a woman infected with pre-symptomatic COVID who, in the course of a single busy day, started developing COVID symptoms, took a COVID test, moved out of her parents’ house and into her husband’s house, and became pregnant. Starsim’s model update logic would order her day as follows: (1) become pregnant, (2) develop symptoms, (3) move house, (4) transmit COVID to anyone still in her contact networks, which at this point would include her husband but not her parents, (5) get COVID test.
There are a few principles that guide the logic about the update ordering. Updating the demographics first makes sense because we want to make sure we capture mother-to-child transmission. Suppose that during one month, a woman became pregnant and also acquired malaria; we want to ensure that the effects of malaria on her pregnancy are captured. Updating the network before disease transmission occurs is fairly arbitrary, but it means that transmission can occur with new partners, which is particularly relevant for sexually transmitted infections, especially when it’s an instantaneously-dissolving partnership.
Printing progress
It can be useful to see how the Sim
is progressing, and the sim.run()
method accepts a verbose
argument that controls how frequently to provide updates. With verbose=1
, you will see updates on progree on every timestep; with verbose=0.1
you will see updates every 10 timesteps, and so on. If you are running monthly or daily timesteps you can also use verbose=1/12 or 1/365 to avoid having too much printed output in your Python terminal. You can also use verbose=-1
, which will only print output when the sim is complete.
Starting and stopping a run
Simulations can be partially run, stopped, and restarted using the sim.run(until=)
argument. This can be useful for a few different reasons:
- To inspect the state of a simulation for mid-run (although you can also use an analyzer or the Python debugger for this)
- To modify the state of a simulation before continuing (although you can also use an intervention for this)
- To run part of a simulation only once, before copying it and running different scenarios (although note that copying a simulation can often take almost as much time as running it!)
Of these, the third use case is probably the most common. The example below illustrates how to do this.
import sciris as sc
import starsim as ss
# Define custom intervention
class sis_vaccine(ss.Intervention):
def __init__(self, start=2040, eff=1.0):
super().__init__()
self.start = start
self.eff = eff
return
def step(self):
= self.sim.diseases.sis
sis if sis.now == self.start:
*= 1-self.eff
sis.rel_trans[:] return
# Default parameters
= dict(
pars = 2000,
start = 2050,
stop = 'sis',
diseases = 'random',
networks = 0,
verbose
)
# Define list of efficacies to run
= [0.0, 0.2, 0.5, 0.8, 1.0]
effs
#%% Standard parallel run
= sc.timer()
T = []
sims
for eff in effs:
= ss.Sim(pars, label=f'Efficacy={eff}', interventions=sis_vaccine(eff=eff))
s
sims.append(s)
= ss.parallel(sims)
sims 'Runtime using default')
T.toc(
#%% Shortcut using "until"
= sc.timer()
T = ss.Sim(pars, interventions=sis_vaccine())
sim =2039)
sim.run(until= []
sims
for eff in effs:
= sim.copy()
s = f'Efficacy={eff}'
s.label 0].eff = eff
s.interventions[
sims.append(s)
= ss.parallel(sims)
sims 'Runtime using "until"')
T.toc(
sims.plot() ss.show()
Runtime using default: 0.786 s
Runtime using "until": 1.10 s
Figure(768x576)
Here, there is relatively little time saved by using until
; however, with a longer burn-in period, or more scenarios, the time savings may be greater.
You can also use this approach to modify the parameters of a simulation before continuing:
import starsim as ss
= ss.Sim(diseases='sis', networks='random', start=2000, stop=2100, verbose=False)
base_sim =2030)
base_sim.run(until
= []
sims = [0.01, 0.02, 0.05, 0.10]
betas for beta in betas:
= base_sim.copy()
sim = beta
sim.diseases.sis.pars.beta = f'beta={beta}'
sim.label
sims.append(sim)
= ss.parallel(sims)
msim msim.plot()
Figure(768x576)