library(odin2)
library(dust2)
4 Arrays
4.1 When scalars are just not enough
The aim of this section is to show you how you use odin’s array syntax, via some motivating examples. The examples here are necessarily a bit longer than in the previous sections because we will generally need a few more moving parts.
4.2 Example: a two-group age structured SIR model
Let’s take the simple stochastic SIR model from Section 3.2 and add age structure to it by having two groups (children and adults), with heterogeneous mixing between the groups.
<- odin({
sir
# Equations for transitions between compartments by age group
update(S[]) <- S[i] - n_SI[i]
update(I[]) <- I[i] + n_SI[i] - n_IR[i]
update(R[]) <- R[i] + n_IR[i]
update(incidence) <- incidence + n_SI[1] + n_SI[2]
# Individual probabilities of transition:
<- 1 - exp(-lambda[i] * dt) # S to I
p_SI[] <- 1 - exp(-gamma * dt) # I to R
p_IR
# Calculate force of infection
# age-structured contact matrix: m[i, j] is mean number of contacts an
# individual in group i has with an individual in group j per time unit
<- parameter()
m
# here s_ij[i, j] gives the mean number of contacts and individual in group
# i will have with the currently infectious individuals of group j
<- m[i, j] * I[j]
s_ij[, ]
# lambda[i] is the total force of infection on an individual in group i
<- beta * (s_ij[i, 1] + s_ij[i, 2])
lambda[]
# Draws from binomial distributions for numbers changing between
# compartments:
<- Binomial(S[i], p_SI[i])
n_SI[] <- Binomial(I[i], p_IR)
n_IR[]
initial(S[]) <- S0[i]
initial(I[]) <- I0[i]
initial(R[]) <- 0
initial(incidence, zero_every = 1) <- 0
# User defined parameters - default in parentheses:
<- parameter()
S0 <- parameter()
I0 <- parameter(0.0165)
beta <- parameter(0.1)
gamma
# Dimensions of arrays
dim(S0) <- 2
dim(I0) <- 2
dim(S) <- 2
dim(I) <- 2
dim(R) <- 2
dim(n_SI) <- 2
dim(n_IR) <- 2
dim(p_SI) <- 2
dim(m) <- c(2, 2)
dim(s_ij) <- c(2, 2)
dim(lambda) <- 2
})
In the odin code above
update(S[]) <- S[i] - n_SI[i]
becomes (approximately)
for (int i = 0; i < S_length; ++i) {
= S[i] + n_SI[i];
update_S[i] }
so there is an implicit indexing by i
on the LHS in that equation of the odin code, and the generated code will then produce a for loop over all values of i
.
4.3 Declaring dimensions
We can see in our first example that it is necessary to declare the dimensions of all arrays within your odin
code using a dim
equation.
Dimensions can be hard-coded:
dim(S) <- 2
or can be generalised so that the dimensions themselves become parameters:
dim(S) <- n_age
<- parameter() n_age
You may have array parameters, these can have dimensions explicitly declared
dim(m) <- c(n_age, n_age)
or they can also have their dimensions detected when the parameters are passed into the system - this still needs a dim
equation where you must explicitly state the rank:
dim(m) <- parameter(rank = 2)
You can also declare the dimensions of one array to be the same as the dimensions of another:
dim(m) <- c(n_age, n_age)
dim(s_ij) <- dim(m)
or collectively declare dimensions of arrays that have the same dimensions:
dim(m, s_ij) <- c(n_age, n_age)
4.4 Summing over arrays
Frequently, you will want to take a sum over an array, or part of an array, using sum
. To sum over all elements of an array, use sum()
with the name of the array you would like to sum over:
dim(x) <- 10
<- sum(x) x_tot
If m
is a matrix you can compute the sums over the second column by writing:
<- sum(m[, 2]) m1_tot
This partial sum approach is frequently used within implicit loops:
<- sum(m[, i]) m_col_totals[]
In our example we calculated the force of infection by summing over the two age groups
<- beta * (s_ij[i, 1] + s_ij[i, 2]) lambda[]
so we could have used sum
to write this as
<- beta * sum(s_ij[i, ]) lambda[]
and this equation has been generalised to any number of age groups now!
The same syntax applies to other functions that reduce arrays: min
, max
and prod
.
4.5 Example: a generalised age structured SIR model
Let’s now take the two group SIR model and generalise it to n_age
age groups
<- odin({
sir_age # Equations for transitions between compartments by age group
update(S[]) <- S[i] - n_SI[i]
update(I[]) <- I[i] + n_SI[i] - n_IR[i]
update(R[]) <- R[i] + n_IR[i]
update(incidence) <- incidence + sum(n_SI)
# Individual probabilities of transition:
<- 1 - exp(-lambda[i] * dt) # S to I
p_SI[] <- 1 - exp(-gamma * dt) # I to R
p_IR
# Calculate force of infection
# age-structured contact matrix: m[i, j] is mean number of contacts an
# individual in group i has with an individual in group j per time unit
<- parameter()
m
# here s_ij[i, j] gives the mean number of contacts and individual in group
# i will have with the currently infectious individuals of group j
<- m[i, j] * I[j]
s_ij[, ]
# lambda[i] is the total force of infection on an individual in group i
<- beta * sum(s_ij[i, ])
lambda[]
# Draws from binomial distributions for numbers changing between
# compartments:
<- Binomial(S[i], p_SI[i])
n_SI[] <- Binomial(I[i], p_IR)
n_IR[]
initial(S[]) <- S0[i]
initial(I[]) <- I0[i]
initial(R[]) <- 0
initial(incidence, zero_every = 1) <- 0
# User defined parameters - default in parentheses:
<- parameter()
S0 <- parameter()
I0 <- parameter(0.0165)
beta <- parameter(0.1)
gamma
# Dimensions of arrays
<- parameter()
n_age dim(S, S0, n_SI, p_SI) <- n_age
dim(I, I0, n_IR) <- n_age
dim(R) <- n_age
dim(m, s_ij) <- c(n_age, n_age)
dim(lambda) <- n_age
})
With the equations
<- m[i, j] * I[j]
s_ij[, ] <- beta * sum(s_ij[i, ]) lambda[]
what we are really doing is matrix multiplication, and in R this would be
<- beta * (m %*% I) lambda
We are aiming to support matrix multiplication in future to help simplify this code.
4.6 Indexing
We have seen in the above examples uses of 1-dimensions and 2-dimensional arrays, making use of index variables i
and j
. Note that while we never use these index variables on the LHS, it is implicit that on the LHS we are indexing by i
for 1-dimensional arrays and i
and j
for 2-dimensional arrays! Use of these index variables on the RHS corresponds to the indexing on the LHS.
Of course you may want to use higher-dimensional arrays! We currently supporting up to 8 dimensions, with index variables i
, j
, k
, l
, i5
, i6
, i7
and i8
.
Be careful with array equations! It’s possible that in an array equation you end up going out of bounds with an array used on the RHS. This can crash the program when running.
4.7 Example: an age-structured SIR model with vaccination
Let’s take the above model and additionally add some vaccination to it.
This should not be considered a guide on how to model vaccination with odin
, as it merely presents one model for the purposes of illustrating how arrays work in odin
- there are many other ways one could model vaccination!
<- odin({
sir_age_vax # Equations for transitions between compartments by age group
update(S[, ]) <- new_S[i, j]
update(I[, ]) <- I[i, j] + n_SI[i, j] - n_IR[i, j]
update(R[, ]) <- R[i, j] + n_IR[i, j]
update(incidence) <- incidence + sum(n_SI)
# Individual probabilities of transition:
<- 1 - exp(-rel_susceptibility[j] * lambda[i] * dt) # S to I
p_SI[, ] <- 1 - exp(-gamma * dt) # I to R
p_IR <- 1 - exp(-eta[i, j] * dt)
p_vax[, ]
# Force of infection
<- parameter() # age-structured contact matrix
m <- m[i, j] * sum(I[j, ])
s_ij[, ] <- beta * sum(s_ij[i, ])
lambda[]
# Draws from binomial distributions for numbers changing between
# compartments:
<- Binomial(S[i, j], p_SI[i, j])
n_SI[, ] <- Binomial(I[i, j], p_IR)
n_IR[, ]
# Nested binomial draw for vaccination in S
# Assume you cannot move vaccine class and get infected in same step
<- Binomial(S[i, j] - n_SI[i, j], p_vax[i, j])
n_S_vax[, ] 1] <- S[i, j] - n_SI[i, j] - n_S_vax[i, j] + n_S_vax[i, n_vax]
new_S[, 2:n_vax] <- S[i, j] - n_SI[i, j] - n_S_vax[i, j] + n_S_vax[i, j - 1]
new_S[,
initial(S[, ]) <- S0[i, j]
initial(I[, ]) <- I0[i, j]
initial(R[, ]) <- 0
initial(incidence, zero_every = 1) <- 0
# User defined parameters - default in parentheses:
<- parameter()
S0 <- parameter()
I0 <- parameter(0.0165)
beta <- parameter(0.1)
gamma <- parameter()
eta <- parameter()
rel_susceptibility
# Dimensions of arrays
<- parameter()
n_age <- parameter()
n_vax dim(S, S0, n_SI, p_SI) <- c(n_age, n_vax)
dim(I, I0, n_IR) <- c(n_age, n_vax)
dim(R) <- c(n_age, n_vax)
dim(m, s_ij) <- c(n_age, n_age)
dim(lambda) <- n_age
dim(eta) <- c(n_age, n_vax)
dim(rel_susceptibility) <- c(n_vax)
dim(p_vax, n_S_vax, new_S) <- c(n_age, n_vax)
})
We see we can use multiple lines to deal with boundary conditions:
1] <- S[i, j] - n_SI[i, j] - n_S_vax[i, j] + n_S_vax[i, n_vax]
new_S[, 2:n_vax] <- S[i, j] - n_SI[i, j] - n_S_vax[i, j] + n_S_vax[i, j - 1] new_S[,
which we could also write as
<- S[i, j] - n_SI[i, j] - n_S_vax[i, j]
new_S[, ] 1] <- new_S[i, j] + n_S_vax[i, n_vax]
new_S[, 2:n_vax] <- new_S[i, j] + n_S_vax[i, j - 1] new_S[,
or another alternative way of writing this would be to use if else
<- S[i, j] - n_SI[i, j] - n_S_vax[i, j] +
new_S[, ] if (j == 1) n_S_vax[i, n_vax] else n_S_vax[i, j - 1]) (
Note that in odin, an if
always requires an else
!
4.8 Semantics of random number draws
Stochastic functions are called for each element in an array they are assigned to, at each time. So here:
<- Normal(0, 1) x[]
x
will be filled with each element having a different draw from a standard normal. In contrast, in:
<- Normal(0, 1)
a <- a x[]
x
will be a vector where every element is the same, the result of a single draw from a standard normal.
4.9 Using arrays for Erlang durations
In our examples we have assumed that infectious periods follow a discretised exponential distribution (rounded up to the nearest dt
), for instance in Section 4.7 we see this in the equations governing movement from I
to R
:
<- 1 - exp(-gamma * dt) # I to R
p_IR <- Binomial(I[i, j], p_IR)
n_IR[, ]
update(I[, ]) <- I[i, j] + n_SI[i, j] - n_IR[i, j]
update(R[, ]) <- R[i, j] + n_IR[i, j]
dim(I, I0, n_IR) <- c(n_age, n_vax)
However, exponential distributions often do not capture the true distribution of infectious periods or other such delays you may be interested in modelling. Arrays in odin can be used to implement (discretised) Erlang distributions by breaking them down into stages of iid exponential distributions by adding a dimension to an array corresponding to these stages. For instance we could generalise the above to an Erlang with shape parameter k_I
(the number of stages) and rate gamma
:
<- parameter()
k_I <- 1 - exp(-gamma * dt)
p_I_progress <- Binomial(I[i, j, k], p_I_progress)
n_I_progress[, , ]
update(I[, , ]) <- I[i, j, k] - n_I_progress[i, j, k] +
if (k == 1) n_SI[i, j] else n_I_progress[i, j, k - 1])
(update(R[, ]) <- R[i, j] + n_I_progress[i, j, k_I]
dim(I, I0, n_I_progress) <- c(n_age, n_vax, k_I)
and there would be some other bits we’d need to change to deal with the increased dimensionality of I
:
<- m[i, j] * sum(I[j, ,])
s_ij[, ] initial(I[, , ]) <- I0[i, j, k]