Skip to main content

Design

🧠0% 🤖100%

This section is the specification of Starsim v4: what a model is made of, how it is run and fitted, and what the library says when something is wrong. It turns the best practices into definite decisions. To learn to use Starsim, start with the docs instead.

The language in one example

import starsim as ss

# --- ingredients: parameters and states -----------------------------------
beta = ss.par(0.05, bounds=[0.01, 0.5])     # per day; it has bounds, so it can be fitted
dur  = ss.days(10)
S, I, R = ss.states(3)                      # named from the variables they are assigned to

# --- the disease: transitions, written like the equations ------------------
infection = S >> I @ beta * I               # "S goes to I at rate beta times I"
recovery  = I >> R @ dur                    # a duration after @ is a mean dwell time
cases     = ss.outcome(0.5 * infection, delay=ss.days(5))   # half of infections are reported, 5 days later; weekly data is compared with the total over each week
sir = ss.Disease(infection, recovery, cases, init_prev=0.01)  # names come from the variable names

# --- a sim: age groups, the mixing that goes with them, and data attached to results by name
kw  = dict(stratify=dict(age=[0, 15, 65]), networks=ss.contacts('Kenya'), total_pop=50e6, start='2020-01-01', stop='2021-01-01')
sim = ss.Sim(sir, data='weekly_cases.csv', **kw)     # the file has a column called 'cases'
sim.describe()                              # the resolved model, with every guessed default marked (it initializes the sim quietly)
sim.run()                                   # prints what is run and how (here as an ODE), R0, and any notes
sim.plot()                                  # results, with the data drawn over 'cases'

# --- calibration: fit what has bounds to what has data ---------------------
fit = sim.fit()

# --- an intervention, and scenarios ----------------------------------------
masks = ss.modify(sir.pars.beta, multiply=0.6, start='2020-03-15')
sims  = ss.parallel(
    baseline = fit.sim,                     # the calibrated sim
    masks    = ss.Sim(sir, pars=fit.pars, interventions=masks, **kw),
)
sims.plot()                                 # ss.parallel runs the sims

# --- chance: a sim is one run; many runs are a MultiSim --------------------
msim = ss.parallel(ss.Sim(sir, solver='tau', **kw), n_runs=100)
msim.plot()                                 # the median and an interval

This is the opening example of 00-overview.md. An SIR is written as states and transitions that read like the equations, given age groups, a contact matrix, and data, and then described, run, calibrated, and compared across scenarios. The last lines run the same sir 100 times under a stochastic solver without changing the disease.

The main ideas

  • A model is states, transitions, and parameters. A disease is an ss.Disease holding ss.State objects and the transitions between them, inside an ss.Sim that adds people, mixing, a timeline, and a solver (01-objects.md).
  • One transition class. S >> I @ beta * I and ss.Transition(S, I, rate=beta * I) build the same thing; whether it is driven by contact or spontaneous follows from whether its rate mentions a bare state (03-states-and-transitions.md).
  • A bare state in a rate means contact. beta * I is exposure to people in I through whatever mixing the sim has, so the same disease runs with homogeneous mixing, a contact matrix, or a network, while I.n is the plain count used for reservoirs and vectors (06-routes.md).
  • Bare numbers work in every slot. Starsim’s time types are kept, a bare number takes its type from its slot and its unit from the sim, and when nothing in the model has a unit, time is counted in timesteps (02-types.md).
  • Stratification is one operation. stratify splits a finished model by age, space, risk, or strain, in place by default, and ss.transfer moves people between levels (05-dimensions.md).
  • Interactions and interventions are small. ss.modify multiplies, adds to, or sets a named quantity while a condition holds without overwriting its base value, and anything else is Python in a module’s step() (07-effects.md); an intervention is a product or a change to a quantity, plus a delivery (08-interventions.md).
  • The solver is chosen for you and can be changed. solver='auto' runs the model as an ODE if every part has a compartmental form and as agents otherwise, and prints the choice; asking for another solver uses the standard approximation where one exists and says so in one line (12-backends.md).
  • Results are automatic, and data attaches by name. Every state gets a count and every named transition a flow, extra results such as ss.outcome are declared with the model, and a data column is matched to the result of the same name (09-results.md).
  • Calibration is one call. sim.fit() fits every parameter that has bounds to every result that has data, with no likelihood required and the fitting method chosen from the model and printed (13-inference.md).
  • The model can be inspected, and defaults are printed. The model is the code that declares it, and it can be read back, compared, and exported (14-model-spec.md); the library prefers guessing a default and marking it [guessed] in describe() to raising an error (15-errors.md).

The reading guide lists every document and the claims the specification makes; 19-open-questions.md has what is unsettled.