Diseases

Diseases are the core of most Starsim models: they define the states an agent can be in (e.g. susceptible, infected, recovered), how agents progress between those states, and — for communicable diseases — how infection spreads across the network. This page covers the disease class hierarchy, the key states and parameters every infection shares (including rel_sus and rel_trans), and the patterns for customizing or building your own disease.

For a learning-oriented introduction, see Tutorial 4 - Diseases.

Disease class architecture

Starsim provides a small disease hierarchy, from general transmission machinery to common compartment patterns:

ss.Disease

  • Base class for all diseases
  • Defines step methods and basic disease structure
  • Does not include transmission logic
  • Used for non-communicable diseases (NCDs)
  • Key methods: define_states(), set_prognoses()

ss.Infection

  • Inherits from ss.Disease
  • Includes transmission logic via the infect() method
  • Used for all communicable diseases
  • Handles network-based transmission automatically
  • Applies network-specific betas and agent susceptibility/transmissibility

ss.SIR and ss.SEIR

  • Inherit from ss.Infection
  • Implement the standard S→I→R and S→E→I→R lifecycles
  • Are usually the best bases for diseases with these compartment structures

Important: Almost all diseases should inherit from ss.Infection. Do not write your own infect() method unless you have very specific requirements - the built-in method correctly handles:

  • Looping over agents in each network
  • Applying network- and disease-specific transmission probabilities
  • Managing agent transmissibility and susceptibility
  • Mixing pool logic

Key implementation methods

Method Purpose When to override
define_states() Initialize additional disease states When adding states not supplied by the base class
set_prognoses() Set outcomes for newly infected people For fully custom acquisition logic; call super()
step_state() Update states each timestep When adding new state transitions
step_die() Handle deaths When disease has custom states
infect() Handle transmission Rarely - use built-in version

Core states and parameters

Every ss.Infection shares a common set of per-agent states and parameters that govern transmission. The most important are:

Name Type Meaning
susceptible BoolState Whether an agent can be infected (default True)
infected BoolState Whether an agent currently has the infection
infectious alias Agents that can currently transmit (by default, an alias of infected)
rel_sus FloatArr Per-agent relative susceptibility (default 1.0)
rel_trans FloatArr Per-agent relative transmissibility (default 1.0)
ti_infected FloatArr Timestep at which each agent acquired the infection
beta parameter Base transmission probability per contact (per network)
init_prev parameter Initial fraction of the population infected

Susceptibility and transmissibility

When ss.Infection processes transmission, the probability that infection passes along a single network edge from an infectious source to a susceptible target is, in essence:

\[p_{\text{transmit}} = \beta \times \texttt{rel\_trans}_{\text{source}} \times \texttt{rel\_sus}_{\text{target}}\]

where \(\beta\) is the per-contact transmission probability for that network (scaled to the timestep). This factorization is the key invariant to understand:

  • Only infectious agents contribute their rel_trans; everyone else contributes 0.
  • Only susceptible agents contribute their rel_sus; everyone else contributes 0.
  • rel_sus and rel_trans both default to 1.0, so by default transmission is governed entirely by beta and the network structure.

These two arrays are the standard “knobs” that the rest of the framework turns to represent heterogeneity in risk. For example:

  • A vaccine product typically reduces rel_sus for vaccinated agents.
  • A treatment might reduce rel_trans for treated agents.
  • A connector between two diseases might increase rel_sus to one disease for agents infected with another (co-infection).

Because they’re ordinary per-agent arrays, you can also set them directly. Here we fully protect everyone under 20 by setting their rel_sus to 0, and see that they avoid infection (aside from the handful seeded as infected at the start via init_prev):

import starsim as ss
ss.options(jupyter=True)

sir = ss.SIR(init_prev=0.01)
sim = ss.Sim(n_agents=2000, diseases=sir, networks='random', copy_inputs=False, verbose=0)
sim.init()

# Fully protect under-20s: rel_sus = 0 means they cannot acquire infection
young = (sim.people.age < 20).uids
older = (sim.people.age >= 20).uids
sir.rel_sus[young] = 0.0

sim.run()
ever_infected = sir.ti_infected.notnan
print(f'Protected (under 20): {ever_infected[young].sum()} of {len(young)} ever infected')
print(f'Unprotected (20+):    {ever_infected[older].sum()} of {len(older)} ever infected')
Protected (under 20): 2 of 609 ever infected
Unprotected (20+):    1337 of 1391 ever infected
Note

beta can be specified per network and per direction. For sexually transmitted infections, for example, beta={'mf': [0.25, 0.15]} sets different male→female and female→male transmission probabilities on the mf network. See the Networks page for more.

Warning

beta is a per-contact transmission probability — a bare float (or a dict/list of floats), not a rate. Do not wrap it in ss.peryear() or ss.perday(): doing so reinterprets the number as a hazard and rescales it by the timestep, corrupting the transmission scale. Use beta=0.1, not beta=ss.perday(0.1). (Most other per-agent event parameters, such as p_death, are probabilities or distributions; the time-aware rate types are for quantities like birth and death rates. See Time.)

Infected vs. infectious

Transmission comes from infectious agents. For an SIR model “infected” and “infectious” are the same thing, so ss.Infection declares infectious as an alias of infected:

self.define_states(
    ss.BoolState('susceptible', default=True, label='Susceptible'),
    ss.BoolState('infected', label='Infected'),
    ...
    infectious = 'infected', # Everyone infected is infectious, unless a subclass says otherwise
)

An alias is only consulted when the attribute isn’t otherwise defined, so it’s a default that a subclass can replace, either with a state of its own or with another alias. That’s what ss.SEIR does: for a disease with a latent period the two names come apart, since E and I are both infected, but only I is infectious. So ss.SEIR makes exposed and infectious the literal E and I compartments, and derives infected from them:

self.define_states(
    ss.BoolState('exposed', label='Exposed'),
    ss.BoolState('infectious', label='Infectious'),
    ...
    reset = ['infected', 'infectious', 'ti_infected'],
    infected = lambda self: self.exposed | self.infectious, # Derived: E and I are both infected
)

A callable alias behaves like a property: it takes the module as its only argument and is recomputed on each access, so it can be read but not written to. Unlike a string alias, it generates an automatic result, so n_infected still exists and counts E plus I. See Aliases for more.

Incidence is a separate concept from any compartment. ss.Infection.set_prognoses() records each infection as it happens, so new_infections doesn’t depend on what a model does with the ti_ states or how many stages it has. Seed infections from init_prev are prevalent cases, and are excluded from incidence.

The result is that each name means one thing:

  • n_exposed is the E compartment and n_infectious is the I compartment, while n_infected and prevalence cover both — an agent in the latent period has the infection, and counts towards prevalence.
  • ti_exposed is the time of acquisition and ti_infectious is the time of becoming infectious. In general, ti_<state> is the time the agent entered <state>.
  • ss.SEIR deliberately does not define ti_infected, since it is too easily confused with ti_infectious; use ti_exposed for the time of acquisition. ti_infectious exists only where it means something different from ti_infected, so SIR and SIS have ti_infected alone.
  • new_infections counts infection events (S→E for SEIR, S→I for SIR).

If you wanted presymptomatic transmission instead, so that exposed agents transmit too, you would override the states that drive transmission rather than the ones that define infection — for example by giving exposed agents a nonzero rel_trans.

Implementation patterns

Pattern 1: Extending existing diseases

When you need to modify an existing disease model, inherit from it and override specific methods:

import starsim as ss
ss.options(jupyter=True)

class MyCustomSIR(ss.SIR):
    def __init__(self, **kwargs):
        super().__init__()
        # Add custom parameters
        self.define_pars(my_param=0.5)
        self.update_pars(**kwargs)
        
    def set_prognoses(self, uids, sources=None):
        # Custom progression logic
        super().set_prognoses(uids, sources)
        # Additional custom logic here

Pattern 2: Extending an SEIR model

For a disease with a latent period, start from ss.SEIR. A simple model needs only new parameter defaults:

class MySEIR(ss.SEIR):
    def __init__(self, **kwargs):
        super().__init__()
        self.define_pars(
            dur_exp = ss.lognorm_ex(0.5),
            dur_inf = ss.lognorm_ex(1.0),
        )
        self.update_pars(**kwargs)

sim = ss.Sim(diseases=MySEIR(init_prev=0.05), networks='random', dur=20, verbose=0)
sim.run()
sim.diseases.myseir.plot()
Figure(672x480)

For a richer natural history, ss.SIR.set_prognoses() splits into two methods you can override separately:

Method Purpose
set_infection(uids) Make agents infectious. ss.SEIR overrides this to put them in exposed first, and set ti_infectious to the end of the latent period.
set_progression(uids) Schedule what happens next: recovery, death, and any other stages. Schedule these relative to the onset of infectiousness (ti_infected in ss.SIR, ti_infectious in ss.SEIR), so that a latent period delays them rather than eating into them.

ssl.Ebola and ssl.Cholera both override set_progression() for their extra states (severe/buried and symptomatic respectively), and inherit everything else from ss.SEIR.

If your model adds a compartment, list it in plot_states so that disease.plot() shows it. This is a class attribute holding the names of the results to plot, so it can be set in one line at the top of the class body — ss.SEIR sets plot_states = ['n_susceptible', 'n_exposed', 'n_infectious', 'n_recovered']. Assign a new list rather than modifying the inherited one, since a class attribute is shared by every instance of the class.