Flow types

Summer’s CompartmentalModel class offers a variety of intercompartmental flows that you can use to define the dynamics of your model. In this example we will cover:

First let’s define some utility functions to help with the example below.

[1]
from summer2 import CompartmentalModel
import pandas as pd
pd.options.plotting.backend = "plotly"

def build_model():
    """Returns a new SIR model"""
    return CompartmentalModel(
        times=[0, 20],
        compartments=["S", "I", "R"],
        infectious_compartments=["I"],
        timestep=0.1,
    )

Transition flow

With a “fractional” transition flow, some proportion of the people in the source compartment transition from this compartment to the destination per time unit. In the example below, 10% of infected people recover per day.

[2]
model = build_model()
model.set_initial_population(distribution={"I": 1000})

# Add a recovery flow where 10% of the source recover per time unit.
model.add_transition_flow("recovery", fractional_rate=0.1, source="I", dest="R")

model.run()
model.get_outputs_df().plot()

Time varying parameters (transition flow)

The rate at which people transition can be set as a constant scalar, or it can be defined by a GraphObject (Function); typically a function of time. This is the case for all of the flows: every parameter can be a scalar or a Function. summer2 provides a number of convenience utilities for building these functions - in this example we use get_piecewise_scalar_function, which takes sequences of breakpoints and values as arguments

[3]
from summer2.functions import get_piecewise_scalar_function

model = build_model()
model.set_initial_population(distribution={"S": 0, "I": 1000})

# Returns the recovery rate for a given time
# In this case there is a single breakpoint (t=10.0); before this the rate will be 0.1, and 0.4
# from then onwards - People recover faster after day ten due to a magic drug!
recovery_rate = get_piecewise_scalar_function([10.0], [0.1,0.4])

# Add a recovery flow where 10% of the source recover per time unit.
model.add_transition_flow("recovery", recovery_rate, "I", "R")

model.run()
model.get_outputs_df().plot()
/tmp/ipykernel_1529/1646785822.py:9: DeprecationWarning:

This method is deprecated and scheduled for removal, use get_piecewise_function instead

Infection density flow

This flow can be used to model infections using density-dependent disease transmission (as opposed to frequency dependent). This article may be helpful in understanding the difference between the two methods.

In unstratified models, the density-dependent infection flow rate (people infected per time unit) is calculated as:

# contact_rate: Rate at which effective contact happens between two individuals, i.e. contact that would result in transmission were it to occur between a susceptible and an infectious person
# num_source: Number of people in the (susceptible) source compartment
# num_infectious: Number of people infectious
force_of_infection = contact_rate * num_infectious
flow_rate = force_of_infection * num_source
[4]
model = build_model()
model.set_initial_population(distribution={"S": 990, "I": 10})

# Add a density dependent infection flow.
model.add_infection_density_flow("infection", contact_rate=1e-3, source="S", dest="I")

model.run()
model.get_outputs_df().plot()

Infection frequency flow

This flow can be used to model infections using frequency-dependent disease transmission.

In unstratified models, the frequency-dependent infection flow rate (the number of people infected per time unit) is calculated as:

# contact_rate: Rate at which contact happens between people and results in a transmission
# num_source: Number of people in the (susceptible) source compartment
# num_infectious: Number of people infected
# num_pop: Total number of people in the population
force_of_infection = contact_rate * num_infectious / num_pop
flow_rate = force_of_infection * num_source
[5]
model = build_model()
model.set_initial_population(distribution={"S": 990, "I": 10})

# Add a frequency dependent infection flow.
model.add_infection_frequency_flow("infection", contact_rate=1, source="S", dest="I")

model.run()
model.get_outputs_df().plot()

Death flow

With a death flow, some percent of people in a user-selected source compartment die and leave the system every time unit.

[6]
model = build_model()
model.set_initial_population(distribution={"S": 1000, "I": 1000})

# 3% of the infected population die per day due to the infection.
model.add_death_flow("infection_death", death_rate=0.03, source="I")

# 1% of the susceptible population die per day due to tiger attacks.
model.add_death_flow("tiger_death", death_rate=0.01, source="S")

model.request_output_for_flow("infection_death", "infection_death")
model.request_output_for_flow("tiger_death", "tiger_death")

model.run()
model.get_outputs_df().plot()
[7]
model.get_derived_outputs_df().plot()

Universal death flow

Adding “universal deaths” is a convenient way to set up a death flow for every compartment, which can account for non-disease mortality (heart disease and getting hit by a bus). This is functionally the same as manually adding a death flow for every compartment. You can adjust the universal death rate for particlar strata later during the stratification process (e.g. age-based mortality).

[8]
model = build_model()
model.set_initial_population(distribution={"S": 700, "I": 200, "R": 100})

# 2% of the population die per day for non-infection-related reasons.
model.add_universal_death_flows("universal_death", death_rate=0.02)

model.run()
model.get_outputs_df().plot()

Crude birth flow

Some percentage of the total population are born into the destination compartment every time unit.

[9]
model = build_model()
model.set_initial_population(distribution={"S": 700, "I": 600, "R": 500})

# 5% of the total population per day are born as susceptible.
model.add_crude_birth_flow("birth", birth_rate=0.05, dest="S")

model.run()
model.get_outputs_df().plot()

Importation flow

An absolute number of people arrive in the destination per time unit. This can be used to model arrivals from outside of the modelled region.

Note that ‘split_imports’ determines whether this number is split over the existing destination compartments (True), or the full number of people sent to each (False). In this example the behaviour is the same (since the flows are to a single compartment), but for stratified models, this can be an important distinction - we will cover this in more detail in the Stratification notebook.

[10]
model = build_model()
model.set_initial_population(distribution={"S": 700, "I": 600, "R": 500})

# 12 susceptible people arrive per year.
model.add_importation_flow("imports", num_imported=12, dest="S", split_imports=True)

# 6 infected people arrive per year.
model.add_importation_flow("imports", num_imported=6, dest="I", split_imports=True)

model.run()
model.get_outputs_df().plot()

Replacement birth flow

Add a flow to replace the number of deaths into the destination compartment. This means the total population should be conserved over time.

[11]
model = build_model()
model.set_initial_population(distribution={"S": 650, "I": 600, "R": 0})

# 5% of the infected population die per year due to infection.
model.add_death_flow("infection_death", death_rate=0.05, source="I")

# The infected people who have died arrive back in the susceptible compartment.
model.add_replacement_birth_flow("births", dest="S")

model.run()
model.get_outputs_df().plot()

Summary

That’s it for now, now you know how to use all the flow types available in summer to define the dynamics of your compartmental model. In future examples you will see how to use these flows in a stratified model.

A detailed API reference of the CompartmentalModel class can be found here