The Susceptible-Infectious-Recovered (SIR) model is the “hello, world!” model for infectious disease simulations, and here we describe how to build it in “individual”. This tutorial will illustrate the use of events, processes, and rendering output.
To start, we should define some constants. The epidemic will be
simulated in a population of 1000, where 5 persons are initially
infectious, whose indices are randomly sampled. The effective contact
rate
will be a function of the deterministic
and recovery rate
.
We also specify dt
, which is the size of the time step
.
Because individual’s time steps are all of unit length, we scale
transition probabilities by dt
to create models with
different sized steps, interpreting the discrete time model as a
discretization of a continuous time model. If the maximum time is
tmax
then the overall number of time steps is
tmax/dt
.
N <- 1e3
I0 <- 5
S0 <- N - I0
dt <- 0.1
tmax <- 100
steps <- tmax/dt
gamma <- 1/10
R0 <- 2.5
beta <- R0 * gamma
health_states <- c("S","I","R")
health_states_t0 <- rep("S",N)
health_states_t0[sample.int(n = N,size = I0)] <- "I"
Next, we will define the individual::CategoricalVariable
which should store the model’s “state”.
health <- CategoricalVariable$new(categories = health_states,initial_values = health_states_t0)
In order to model infection, we need a process. This is a function
that takes only a single argument, t
, for the current time
step (unused here, but can model time-dependent processes, such as
seasonality or school holiday). Within the function, we get the current
number of infectious individuals, then calculate the per-capita
force of infection on each susceptible person,
.
Next we get a individual::Bitset
containing those
susceptible individuals and use the sample
method to
randomly select those who will be infected on this time step. The
probability is given by
.
This is the same as the CDF of an exponential random variate so we use
stats::pexp
to compute that quantity. Finally, we queue a
state update for those individuals who were sampled.
infection_process <- function(t){
I <- health$get_size_of("I")
foi <- beta * I/N
S <- health$get_index_of("S")
S$sample(rate = pexp(q = foi * dt))
health$queue_update(value = "I",index = S)
}
Now we need to model recovery. For geometrically distributed
infectious periods, we could use another process that randomly samples
some individuals each time step to recover, but we’ll use a
individual::TargetedEvent
to illustrate their use. The
recovery event is quite simple, and the “listener” which is added is a
function that is called when the event triggers, taking
target
, a individual::Bitset
of scheduled
individuals, as its second argument. Those individuals are scheduled for
a state update within the listener function body.
recovery_event <- TargetedEvent$new(population_size = N)
recovery_event$add_listener(function(t, target) {
health$queue_update("R", target)
})
Finally, we need to define a recovery process that queues future
recovery events. We first get individual::Bitset
objects of
those currently infectious individuals and those who have already been
scheduled for a recovery. Then, using bitwise operations, we get the
intersection of already infectious persons with persons who have not
been scheduled, precisely those who need to have recovery times sampled
and recoveries scheduled. We sample those times from
stats::rgeom
, where the probability for recovery is
.
Note that we add one to the resulting vector, because by default R uses
a “number of failures” parameterization rather than “number of trials”,
meaning it would be possible for an individual to be infectious for 0
time steps without the correction. Finally we schedule the recovery
using the recovery event object.
We note at this point would be possible to queue the recovery event at the same time the infection state update was made, but we separate event and process for illustration of how the package works.
The last thing to do before simulating the model is rendering output
to plot. We use a individual::Render
object which stores
output from the model, which is tracked each time step. To do so
requires another process, for which we use the “prefab”
individual::categorical_count_renderer_process
.
health_render <- Render$new(timesteps = steps)
health_render_process <- categorical_count_renderer_process(
renderer = health_render,
variable = health,
categories = health_states
)
Finally, the simulation can be run by passing objects to the
individual::simulation_loop
function:
simulation_loop(
variables = list(health),
events = list(recovery_event),
processes = list(infection_process,recovery_process,health_render_process),
timesteps = steps
)
We can easily plot the results by accessing the renderer.
states <- health_render$to_dataframe()
health_cols <- c("royalblue3","firebrick3","darkorchid3")
matplot(
x = states[[1]]*dt, y = states[-1],
type="l",lwd=2,lty = 1,col = adjustcolor(col = health_cols, alpha.f = 0.85),
xlab = "Time",ylab = "Count"
)
legend(
x = "topright",pch = rep(16,3),
col = health_cols,bg = "transparent",
legend = health_states, cex = 1.5
)