ODE model creation
The mathematical model
Before implementing a model in MEmilio, we need to do some math, in particular, define an initial value problem given by a system of ordinary differential equations. For example, we consider a SIRD model given by
and some initial values for \(t=0\). Here \(N_{\perp D} := S(t) + I(t) + R(t)\).
This type of model is called compartmental model, because the model population is represented by discrete infection states Susceptible, Infectious, Recovered, Deceased, also called compartments.
How to implement an ODE model
To define an ODE model in MEmilio, there are two options. You can define a CompartmentalModel or a FlowModel, which use different methods to define the right-hand side of the mathematical model above. Both classes need definitions for the infection states, population and parameters it uses. The FlowModel additionally requires a list of flows.
We start by creating a new directory for our model under “cpp/models”, in this case we can call it “ode_sird”. The name must be unique and start with “ode_”, so the type of model is obvious. The rest usually contains the compartments or other noteworthy features of the model in shortened form. All files in the following are put into this directory.
Infection states
First we define an enum class called InfectionState in the file “infection_state.h”, which contains an entry
for each infection state of the mathematical model, followed by an entry called Count. This enumerates the
compartments starting from 0, with Count being equal to the number of compartments. In our example we have:
enum class InfectionState
{
Susceptible,
Infectious,
Recovered,
Deceased,
Count
};
Expert’s settings
We use an enum class instead of a regular enum here, since it helps avoid unintended conversions, invalid
array accesses, and “magic numbers” - you cannot simply write “2” and use it as InfectionState::Recovered, since
an enum class does not allow implicit conversions.
Parameters
Next, we define the parameters in “parameters.h”, which consist of a struct for each constant used in the mathematical model. This struct must define the data type, name and default value of the constant. For example, for the time a person stays infectious, \(T_I\), we define a struct
template <typename FP>
struct TimeInfectious {
using Type = UncertainValue<FP>;
static Type get_default()
{
return Type(6.73);
}
static std::string name()
{
return "TimeInfectious";
}
};
and for the contact rate \(\phi\) a struct
template <typename FP>
struct ContactRate {
using Type = FP;
static Type get_default()
{
return Type(10.0);
}
static std::string name()
{
return "ContactRate";
}
};
Avoid using the mathematical symbols of the constant as names for the struct. Their connection can be noted in the documentation of these structs.
The template FP and the type UncertainValue<FP> in these examples are commonly used throughout MEmilio.
FP is a floating point type, usually double. An UncertainValue<FP> holds a value of type
FP as well as (optionally) a distribution to sample new values from, e.g. for a parameter study.
Finally, define a type Parameters by listing all parameter structs as template arguments of a
mio::ParameterSet:
template <typename FP>
using Parameters = mio::ParameterSet<TimeInfectious<FP>, RecoveryRate<FP>, LethalityRate<FP>, ContactRate<FP>,
TransmissionRisk<FP>>;
For more complex models, Parameters allows passing arguments from its constructor to the get_default
functions. Make sure that all of these functions take the exact types as function arguments that you want to pass to
the constructor.
Population
The population will be stored in a vector, with a component for each infection state. We define it using the class
mio::Populations.
template <typename FP>
using Population = mio::Populations<FP, InfectionState>;
Importantly, this class allows further stratifying the population vector, with the most common
example being adding mio::AgeGroups to the template.
Expert’s settings
The type mio::AgeGroup is a type-safe size_t, meaning an integer that cannot be confused with other integer
types. So assignment, addition, etc. only works with another mio::AgeGroup, not size_t or another integer
type. This is useful for function interfaces or indexing, as it makes it (nearly) impossible to mix up, e.g., age
groups with infection states. Check out mio::Index if you want to learn more.
The type mio::Populations is an extension of a mio::CustomIndexArray, which is a template type that manages
a flat array. Its main purpose is to allow multidimensional indexing into this array, using type-safe indices like
a mio::Index or a enum class.
The definition of our Population then changes to
template <typename FP>
using Population = mio::Populations<FP, InfectionState, AgeGroup>;
and the access (compare with the model definition below) changes to, e.g.,
const AgeGroup i = . . .;
const size_t Ri = this->populations.get_flat_index({i, InfectionState::Susceptible});
dydt[Ri] = . . . * y[Ri];
where we use populations.get_flat_index to get the correct index in the flat state and derivative vectors.
You may also want to change the Parameters to use age groups, check out the available ODE models as reference.
Define a CompartmentalModel class
Now we can define the model:
template <typename FP>
class Model : public mio::CompartmentalModel<FP, InfectionState, Population<FP>, Parameters<FP>>
{
public:
using Base = mio::CompartmentalModel<FP, InfectionState, Population<FP>, Parameters<FP>>;
using typename Base::ParameterSet;
using typename Base::Populations;
Model()
: Base(Populations({InfectionState::Count}), ParameterSet())
{
}
void get_derivatives(Eigen::Ref<const Eigen::VectorX<FP>> pop, Eigen::Ref<const Eigen::VectorX<FP>> y, FP t,
Eigen::Ref<Eigen::VectorX<FP>> dydt) const override
{
const Parameters<FP>& params = this->parameters;
const auto N = y[InfectionState::Susceptible] + y[InfectionState::Infectious] +
y[InfectionState::Recovered];
dydt[InfectionState::Susceptible] = -params.template get<TransmissionRisk<FP>>() *
params.template get<ContactRate<FP>>() *
y[InfectionState::Susceptible] * pop[InfectionState::Infectious] / N;
. . .
}
};
Here, create a new class Model that inherits from mio::CompartmentalModel, which predefines some functions (like
check_constraints) and members (like populations and parameters) for us. In the class body, we first add
a few using statements, that create shorthands for our base class and its types. Next, we define a constructor, that
creates populations and parameters with their default values, so they can be set later. Finally, we define the
right-hand side of the model equations through get_derivatives. Importantly, note that the value in populations
is only used as initial value for the IVP, the current state of the model at time t is given by y instead. The
derivative of y at t is to be stored in dydt.
Note that the argument pop is used once instead of y. As a general rule, use y when the index matches with
the one used for dydt, and pop otherwise.
Essentially, pop is the population that is interacted with, while y is the acting/changing population. Our graph
models use this to model the exchange between multiple models, e.g. by setting pop = y ± commuters. Outside of graph
models both y and pop will have the same value.
Continue here to learn how to use this model, or read on if you want to use a flow based ODE model.
Implement the ODE model using flows
A FlowModel is a special case of a CompartmentalModel, where the derivative of each compartment over time \(Z_i'(t)\) can be written as
where the flows \(f_{Z_i \rightarrow Z_j} \gt 0\) are the amount of population changing from compartment \(Z_i\) to \(Z_j\) at time \(t\). So the first sum accumulates all inflows, the second subtracts all outflows.
The SIRD model from above can be expressed as a FlowModel with only three flows:
Note that all other possible flows, like \(f_{I \rightarrow S}\), are constant 0.
Flows
To use a FlowModel, we need to create a list of all flows. These are used by the model to automatically assemble the
compartments. We use a mio::TypeList with a mio::Flow for each mathematical flow. For the SIRD model
we get:
using Flows = mio::TypeList<mio::Flow<InfectionState::Susceptible, InfectionState::Infectious>,
mio::Flow<InfectionState::Infectious, InfectionState::Recovered>,
mio::Flow<InfectionState::Infectious, InfectionState::Deceased>>;
The first term in each mio::Flow is the source compartment, the second the target. As a convention, we always
compute non-negative outflows. Hence, we only list the flow \(S \rightarrow I\), but not \(I \rightarrow S\).
InfectionStates, Parameters and Population
Since a FlowModel is just a special case of a CompartmentalModel, all of these are defined exactly as described above.
Define a FlowModel class
With the flows and classes also used by the CompartmentalModel, we can define a FlowModel as such:
template <typename FP>
class Model : public mio::FlowModel<FP, InfectionState, Population<FP>, Parameters<FP>, Flows>
{
public:
using Base = mio::FlowModel<FP, InfectionState, Population<FP>, Parameters<FP>, Flows>;
using typename Base::ParameterSet;
using typename Base::Populations;
void get_flows(Eigen::Ref<const Eigen::VectorX<FP>> pop, Eigen::Ref<const Eigen::VectorX<FP>> y, FP t,
Eigen::Ref<Eigen::VectorX<FP>> flows) const override
{
const Parameters<FP>& params = this->parameters;
const auto N = y[InfectionState::Susceptible] + y[InfectionState::Infectious] +
y[InfectionState::Recovered];
flows[this->template get_flat_flow_index<InfectionState::Susceptible, InfectionState::Infectious>()] =
params.template get<TransmissionRisk<FP>>() * params.template get<ContactRate<FP>>() *
y[InfectionState::Susceptible] * pop[InfectionState::Infectious] / N;
. . .
}
};
This is mostly analogous to the definition of a CompartmentalModel, with a few important differences. First, we now
inherit from FlowModel, which gets the Flows as an additional template argument. The Base alias changes
accordingly. Secondly, the function we implement is called get_flows and computes the derivative of y in terms of
its flows.
To index into the flows vector we use the function get_flat_flow_index, which takes the source and target
compartments as template arguments, in that order. Indexes from further stratification (like mio::AgeGroup) can be
passed as an optional function argument.
Note that FlowModel defines a get_derivatives function for you, that allows the Model class to be used as a
regular CompartmentalModel. It uses Flows and get_flows internally to assemble the derivatives. There is
also an overload get_derivatives(flows, dydt) that directly computes the derivatives dydt from a given vector
flows without calling get_flows.
To use this model, head here.