# import the necessary libraries
import matplotlib.pyplot as plt
import numpy as np
from sympy import Max
from epymorph.kit import *
from epymorph.adrio import acs5
Introductory usage of epymorph
SCENARIO:
Welcome to epymorph! Let’s jump into the basics of using epymorph by building a simple model for a basic scenario.
Imagine that you are investigating the dynamics of measles transmission in the children of a single US County. For simplicity, we’ll assume that the population of children within the county is mixed homogenously and all children are susceptible (i.e., a naive population). Suppose we are investigating Mohave County in Arizona.
Exercise 1
Build a simple Susceptible-Infectious-Recovered (SIR) model for a single population. Once you recover from measles, you are typically immune for life. Thus, a simple SIR model provides a suitable basis for modeling this case study.
class Sir(CompartmentModel):
"""Defines a compartmental IPM for a generic SIR model."""
= [
compartments 'S'),
compartment('I'),
compartment('R'),
compartment(
]
= [
requirements 'beta', type=float, shape=Shapes.TxN,
AttributeDef(='infectivity'),
comment'gamma', type=float, shape=Shapes.TxN,
AttributeDef(='recovery rate'),
comment
]
def edges(self, symbols):
= symbols.all_compartments
[S, I, R] = symbols.all_requirements
[β, γ]
= Max(1, S + I + R)
N
return [
=β * S * I / N),
edge(S, I, rate=γ * I),
edge(I, R, rate
]
= Sir() my_ipm
= CountyScope.in_counties(["Mohave, AZ"], year=2020) scope
= mm.No() null_mm
= init.SingleLocation(location=0, seed_size=5) my_init
= TimeFrame.of("2020-01-01", duration_days=200) time
Now we can set up the RUME for this exercise. - To do this, you will need to specify the ‘population’ that is required by the IPM. Use the CensusAdrio to specify populations in the age range of children (0 - 9 years old) - Use the following values for the required parameters. - beta = 0.4 - gamma = 1/4
= 0
minage = 9
maxage = acs5.PopulationByAge(minage, maxage)
pop = acs5.PopulationByAgeTable() age
= 0.4
beta = 1 / 4 gam
# create the simulation
= SingleStrataRUME.build(
rume =scope,
scope=my_ipm,
ipm=null_mm,
mm=my_init,
init=time,
time_frame={
params"beta": beta,
"gamma": gam,
"population": pop,
"population_by_age_table": age,
},
)
= BasicSimulator(rume)
sim # here you may add the live = False to make the results look cleaner
with sim_messaging(live= False):
= sim.run(
out =default_rng(5),
rng_factory )
Loading gpm:all::init::population_by_age_table (epymorph.adrio.acs5.PopulationByAgeTable):
|####################| 100% (19.009s)
Loading gpm:all::init::population (epymorph.adrio.acs5.PopulationByAge):
|####################| 100% (0.000s)
Running simulation (BasicSimulator):
• 2020-01-01 to 2020-07-18 (200 days)
• 1 geo nodes
|####################| 100%
Runtime: 0.044s
rume.ipm.diagram()
sum(
out.table.=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all(),
time=out.rume.ipm.select.events(),
quantity )
geo | quantity | sum | |
---|---|---|---|
0 | Mohave, AZ | S → I | 12237 |
1 | Mohave, AZ | I → R | 12242 |
out.table.quantiles(=[0, 0.5, 1.0],
quantiles=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all(),
time=out.rume.ipm.select.compartments(),
quantity )
geo | quantity | 0 | 0.5 | 1.0 | |
---|---|---|---|---|---|
0 | Mohave, AZ | S | 6152.0 | 6154.0 | 18386.0 |
1 | Mohave, AZ | I | 0.0 | 9.0 | 1643.0 |
2 | Mohave, AZ | R | 0.0 | 12227.5 | 12242.0 |
Exercise 2
In textual terms, the output of a model is just a large array detailing the number of individuals in each model compartment over time, as well as the number of specific events that occur over time. To better represent these data, we usually want to view the model outputs graphically, i.e., as a graph of each compartment over time.
out.plot.line(=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all(),
time=out.rume.ipm.select.compartments(),
quantity="Compartment values in Mohave County",
title )
out.plot.line(=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all(),
time=out.rume.ipm.select.events(),
quantity="Model events in Mohave County",
title )
Exercise 3
An important feature of all epymorph models is that they are stochastic, meaning that at each time step, a draw from a probability distribution determines which and how many transitions between model compartments (called “events”) occur. Thus, to get an accurate picture of the likely infections dynamics, one usually runs the model many times (called “realizations”); all of these runs are shown on the output graph, with the average of them highlighted and shown to show the most likely overall pattern.
= []
stored_sim_results = 20
length for x in range(length):
= sim.run()
out stored_sim_results.append(out)
= plt.subplots()
fig, ax
# This uses the more advanced line_plt() functionality
for j in stored_sim_results:
j.plot.line_plt(= ax,
ax =j.rume.scope.select.all(),
geo=j.rume.time_frame.select.all(),
time=j.rume.ipm.select.events("S->I"),
quantity
)
"day")
ax.set_xlabel("S->I Events")
ax.set_ylabel("Graph of 100 Simulation Results")
ax.set_title(True)
ax.grid(
fig.tight_layout() plt.show()
Exercise 4
ADVANCED Now that we have the basic model up and running, let’s use it to explore various disease dynamics in more detail.Because infected individuals are immune to measles after they recover, diseases like measles exhibit a disease “burnout”, where the infection rate drops to zero as the number of available susceptible individuals drops.
= [0.33, 0.45, 0.8]
beta_array = []
transmission_rates for b in beta_array:
= SingleStrataRUME.build(
rume =my_ipm,
ipm=null_mm,
mm=my_init,
init=scope,
scope=time,
time_frame={
params"beta": b,
"gamma": gam,
"population": pop,
"population_by_age_table": age,
},
)= BasicSimulator(rume)
sim = sim.run(
out =default_rng(5),
rng_factory
)
transmission_rates.append(out)
= plt.subplots()
fig, ax
for j in transmission_rates:
j.plot.line_plt(= ax,
ax =j.rume.scope.select.all(),
geo=j.rume.time_frame.select.all(),
time=j.rume.ipm.select.compartments("I"),
quantity
)
# custom graph
"days")
ax.set_xlabel("Infected")
ax.set_ylabel("Burnout")
ax.set_title(True)
ax.grid(
fig.tight_layout() plt.show()
The basic reproductive number of the pathogen (\(R0\)) in the SIR model is represented mathematically as the ratio of transmission rate to recovery rate: \(\beta / \gamma\). Iteratively alter the \(R0\) from ~0.01 to 10 and create a line plot to show how \(R0\) is associated with the total number of individuals that ultimately become infected during the outbreak. This number can be extracted as the number of Recovered individuals present at the last time step of the simulation, because remember that all Recovered individuals in this SIR model had been infected sometime in the past.
Here the code is creating a simulation with different basic reproductive numbers of the pathogen for 6 different simulations and these R0 vals are being used to calculate a different beta val for each simulation. Total_outputs is storing the infections for each simulation.
= []
total_outputs
= np.arange(0.75, 4.0, 0.25)
R0_vals = [x * gam for x in R0_vals]
beta_array
for b in beta_array:
= SingleStrataRUME.build(
rume =my_ipm,
ipm=null_mm,
mm=my_init,
init=scope,
scope=time,
time_frame={
params"beta": b,
"gamma": gam,
"population": pop,
"population_by_age_table": age,
},
)# run new sim here
= BasicSimulator(rume)
sim = sim.run(
out =default_rng(8),
rng_factory
)
# sum the I->R events over time to get cumulative
= out.table.sum(
current_output =rume.scope.select.all(),
geo=rume.time_frame.select.all(),
time=rume.ipm.select.events("I->R"),
quantity
)
# Extract the sum column from the data frame
'sum'][0]) total_outputs.append(current_output[
Now we plot the cumulative infections versus \(R0\). Note that the stochasticity will create a pattern that is close to the theoretical expectation, but not exactly.
=(5, 4))
plt.figure(figsize
plt.plot(R0_vals, total_outputs)"R0")
plt.xlabel("Cumulative Infections")
plt.ylabel(True)
plt.grid( plt.show()