ODE-based SECIRTS-type model including multi-layer waning immunity
This model extends the ODE-SECIRVVS model by adding waning immunity and introducing temporary immunity states that change the meaning of recovery. Like the ODE-SECIRVVS model, the ODE-SECIRTS model has three layers of immunity: naive immunity, partial immunity, improved immunity.
Additionally, waning immunity is defined by the parameters TimeWaningPartialImmunity, TimeWaningImprovedImmunity, TimeTemporaryImmunityPI, and TimeTemporaryImmunityII. The parameters TimeWaningPartialImmunity and TimeWaningImprovedImmunity represent the (mean) duration after which an individual transitions from one immunity layer to the next weaker one due to waning immunity, assuming no vaccination or recovery from infection has occurred during this period. Similarly, the parameters TimeTemporaryImmunityPI and TimeTemporaryImmunityII denote the (mean) duration of temporary immunity following exposure to the virus, either through vaccination or recovery. During this state of temporary immunity, individuals are protected from reinfection and are incapable of transmitting the virus to others. Should individuals previously reside in the naive or partial immunity layer, their stay in the temporary immunity state results in a transition to the next stronger immunity layer.
For more details about the model, we refer to Zunker H, Schmieding R, Kerkmann D, Schengen A, Diexer S, Mikolajczyk R, Meyer-Hermann M, Kühn MJ. (2024). Novel travel time aware metapopulation models and multi-layer waning immunity for late-phase epidemic and endemic scenarios. PLOS Computational Biology 20(12): e1012630. DOI:10.1371/journal.pcbi.1012630.
The complete system of equations can be found in the supplementary material: doi:10.1371/journal.pcbi.1012630.s001.
Below is an overview of the model architecture and its compartments.
Infection States
The model extends the ODE-SECIRVVS model by adding temporary immunity states and flow paths for waning immunity. It contains the following list of InfectionStates:
// Naive immunity
`SusceptibleNaive`
`ExposedNaive`
`InfectedNoSymptomsNaive`
`InfectedNoSymptomsNaiveConfirmed`
`InfectedSymptomsNaive`
`InfectedSymptomsNaiveConfirmed`
`InfectedSevereNaive`
`InfectedCriticalNaive`
`DeadNaive`
// Temporary immunity after infection in naive state
`TemporaryImmunePartialImmunity`
// Partial immunity
`SusceptiblePartialImmunity`
`ExposedPartialImmunity`
`InfectedNoSymptomsPartialImmunity`
`InfectedNoSymptomsPartialImmunityConfirmed`
`InfectedSymptomsPartialImmunity`
`InfectedSymptomsPartialImmunityConfirmed`
`InfectedSeverePartialImmunity`
`InfectedCriticalPartialImmunity`
`DeadPartialImmunity`
// Temporary immunity after infection in partial or improved immunity state
`TemporaryImmuneImprovedImmunity`
// Improved immunity
`SusceptibleImprovedImmunity`
`ExposedImprovedImmunity`
`InfectedNoSymptomsImprovedImmunity`
`InfectedNoSymptomsImprovedImmunityConfirmed`
`InfectedSymptomsImprovedImmunity`
`InfectedSymptomsImprovedImmunityConfirmed`
`InfectedSevereImprovedImmunity`
`InfectedCriticalImprovedImmunity`
`DeadImprovedImmunity`
Infection State Transitions
The ODE-SECIRTS model is implemented as a FlowModel, which defines the derivatives of each flow between compartments. A key difference from the ODE-SECIRVVS model is that vaccinations in the ODE-SECIRTS model are implemented as flows within the ODE system rather than discrete events.
The model has the following state trnsitions:
// Naive immunity flows
`SusceptibleNaive, ExposedNaive`
`SusceptibleNaive, TemporaryImmunePartialImmunity` (vaccination)
`ExposedNaive, InfectedNoSymptomsNaive`
`InfectedNoSymptomsNaive, InfectedSymptomsNaive`
`InfectedNoSymptomsNaive, TemporaryImmunePartialImmunity` (recovery)
`InfectedNoSymptomsNaiveConfirmed, InfectedSymptomsNaiveConfirmed`
`InfectedNoSymptomsNaiveConfirmed, TemporaryImmunePartialImmunity` (recovery)
`InfectedSymptomsNaive, InfectedSevereNaive`
`InfectedSymptomsNaive, TemporaryImmunePartialImmunity` (recovery)
`InfectedSymptomsNaiveConfirmed, InfectedSevereNaive`
`InfectedSymptomsNaiveConfirmed, TemporaryImmunePartialImmunity` (recovery)
`InfectedSevereNaive, InfectedCriticalNaive`
`InfectedSevereNaive, TemporaryImmunePartialImmunity` (recovery)
`InfectedSevereNaive, DeadNaive`
`InfectedCriticalNaive, DeadNaive`
`InfectedCriticalNaive, TemporaryImmunePartialImmunity` (recovery)
// Temporary immunity to partial immunity transition
`TemporaryImmunePartialImmunity, SusceptiblePartialImmunity`
// Partial immunity flows
`SusceptiblePartialImmunity, ExposedPartialImmunity`
`SusceptiblePartialImmunity, TemporaryImmuneImprovedImmunity` (vaccination)
`ExposedPartialImmunity, InfectedNoSymptomsPartialImmunity`
// ... similar flows for other partial immunity states leading to recovery in TemporaryImmuneImprovedImmunity ...
// Temporary immunity to improved immunity transition
`TemporaryImmuneImprovedImmunity, SusceptibleImprovedImmunity`
// Improved immunity flows
`SusceptibleImprovedImmunity, ExposedImprovedImmunity`
`SusceptibleImprovedImmunity, TemporaryImmuneImprovedImmunity` (booster vaccination)
// ... similar flows for other improved immunity states leading to recovery in TemporaryImmuneImprovedImmunity ...
// Waning immunity flows
`SusceptibleImprovedImmunity, SusceptiblePartialImmunity`
`SusceptiblePartialImmunity, SusceptibleNaive`
Sociodemographic Stratification
Like the other ODE-SECIR models, the ODE-SECIRTS model can be stratified by one sociodemographic dimension, typically age groups. This stratification is important for modeling different vaccination rates, symptom severities, mortality risks, and immunity waning rates across age groups. The dimension is denoted AgeGroup but can also be used for other interpretations. For stratifications with two or more dimensions, see Model Creation.
Parameters
The model includes all parameters from the ODE-SECIRVVS model as well as additional parameters specific to waning and temporary immunity states:
Mathematical variable |
C++ variable name |
Description |
|---|---|---|
\(\lambda_{N,i} = \rho_{N,i} \sum_j \phi_{i,j}\frac{\xi_{I_{NS}} \Bigl(I_{NS,N,j} + I_{NS,PI,j} + I_{NS,II,j}\Bigr) + \xi_{I_{Sy}} \Bigl(I_{Sy,N,j} + I_{Sy,PI,j} + I_{Sy,II,j}\Bigr)}{N_j^{D^\perp}}\) |
|
Force of infection for susceptibles located in the naive immunity level. |
\(\lambda_{PI,i} = \rho_{PI,i}\sum_j \phi_{i,j}\frac{\xi_{I_{NS}} \Bigl(I_{NS,N,j} + I_{NS,PI,j} + I_{NS,II,j}\Bigr) + \xi_{I_{Sy}} \Bigl(I_{Sy,N,j} + I_{Sy,PI,j} + I_{Sy,II,j}\Bigr)}{N_j^{D^\perp}}\) |
|
Force of infection for susceptibles located in the partial immunity level. |
\(\lambda_{II,i} = \rho_{II}\sum_j \phi_{i,j}\frac{\xi_{I_{NS}} \Bigl(I_{NS,N,j} + I_{NS,PI,j} + I_{NS,II,j}\Bigr) + \xi_{I_{Sy}} \Bigl(I_{Sy,N,j} + I_{Sy,PI,j} + I_{Sy,II,j}\Bigr)}{N_j^{D^\perp}}\) |
|
Force of infection for susceptibles located in the improved immunity level. |
\(\phi\) |
|
Matrix of daily contact rates, i.e., number of daily contacts between different age groups. |
\(\rho\) |
|
Transmission risk for people located in one of the susceptible compartments. |
\(\xi_{I_{NS}}\) |
|
Proportion of asymptomatically infected people who are not isolated (time-dependent if |
\(\xi_{I_{Sy}}\) |
|
Proportion of symptomatically infected people who are not isolated (time-dependent if |
\(N_j^{D^\perp}\) |
|
Sum of all living individuals of age groups j. |
\(T_{E}\) |
|
Time in days an individual stays in the Exposed compartment. |
\(T_{I_{NS}}\) |
|
Time in days an individual stays in the InfectedNoSymptoms compartment. |
\(T_{I_{Sy}}\) |
|
Time in days an individual stays in the InfectedSymptoms compartment. |
\(T_{I_{Sev}}\) |
|
Time in days an individual stays in the InfectedSevere compartment. |
\(T_{I_{Cr}}\) |
|
Time in days an individual stays in the InfectedCritical compartment. |
\(T_{\mathcal{I}_{PI}}\) |
|
Time in days an individual stays in the TemporaryImmunePartialImmunity compartment. |
\(T_{\mathcal{I}_{PI}}\) |
|
Time in days an individual stays in the TemporaryImmuneImprovedImmunity compartment. |
\(T_{W_{PI}}\) |
|
Time in days an individual stays in the SusceptiblePartialImmunity compartment before waning to the SusceptibleNaive compartment assuming no exposure occurred during this period. |
\(T_{W_{II}}\) |
|
Time in days an individual stays in the SusceptibleImprovedImmunity compartment before waning to the SusceptiblePartialImmunity compartment assuming no exposure occurred during this period. |
\(\mu_{I_{NS}}^{I_{Sy}}\) |
|
Probability of transition from compartment InfectedNoSymptoms to InfectedSymptoms. |
\(\mu_{I_{Sy}}^{I_{Sev}}\) |
|
Probability of transition from compartment InfectedSymptoms to InfectedSevere. |
\(\mu_{I_{Sev}}^{I_{Cr}}\) |
|
Probability of transition from compartment InfectedSevere to InfectedCritical. |
\(\mu_{I_{Cr}}^{D}\) |
|
Probability of dying when located in compartment InfectedCritical. |
\(\kappa\) |
|
Reduction factor for time intervals for specific partial and improved immunity compartments. |
\(DV_{part,i}(t)\) |
|
Daily number of first-dose vaccinations per age group. |
\(DV_{full,i}(t)\) |
|
Daily number of second-dose vaccinations per age group. |
\(DV_{boost,i}(t)\) |
|
Daily number of booster vaccinations per age group. |
\(T_{V1}\) |
|
Time in days until first vaccine dose takes full effect. |
\(T_{V2}\) |
|
Time in days until second vaccine dose takes full effect. |
\(T_{V3}\) |
|
Time in days until booster vaccine dose takes full effect. |
\(s\) |
|
Seasonal variation factor affecting transmission. |
\(ICU_{cap}\) |
|
Maximum ICU capacity in the modeled region. |
\(\nu\) |
|
Relative infectiousness of a new variant compared to the original strain. |
\(t_{newVar}\) |
|
Day on which the new variant is introduced in the simulation. |
Initial conditions
The initial conditions of the model are represented by the class Populations which defines the number of individuals in each sociodemographic group and InfectionState. Before running a simulation, the initial values for each compartment across all immunity levels have to be set. This can be done via:
for (mio::AgeGroup i = 0; i < nb_groups; i++) {
// population
model.populations[{i, mio::osecirts::InfectionState::ExposedNaive}] = 20;
model.populations[{i, mio::osecirts::InfectionState::ExposedImprovedImmunity}] = 20;
model.populations[{i, mio::osecirts::InfectionState::ExposedPartialImmunity}] = 20;
model.populations[{i, mio::osecirts::InfectionState::InfectedNoSymptomsNaive}] = 30;
model.populations[{i, mio::osecirts::InfectionState::InfectedNoSymptomsNaiveConfirmed}] = 0;
model.populations[{i, mio::osecirts::InfectionState::InfectedNoSymptomsPartialImmunity}] = 30;
model.populations[{i, mio::osecirts::InfectionState::InfectedNoSymptomsPartialImmunityConfirmed}] = 0;
model.populations[{i, mio::osecirts::InfectionState::InfectedNoSymptomsImprovedImmunity}] = 30;
model.populations[{i, mio::osecirts::InfectionState::InfectedNoSymptomsImprovedImmunityConfirmed}] = 0;
model.populations[{i, mio::osecirts::InfectionState::InfectedSymptomsNaive}] = 40;
model.populations[{i, mio::osecirts::InfectionState::InfectedSymptomsNaiveConfirmed}] = 0;
model.populations[{i, mio::osecirts::InfectionState::InfectedSymptomsPartialImmunity}] = 40;
model.populations[{i, mio::osecirts::InfectionState::InfectedSymptomsPartialImmunityConfirmed}] = 0;
model.populations[{i, mio::osecirts::InfectionState::InfectedSymptomsImprovedImmunity}] = 40;
model.populations[{i, mio::osecirts::InfectionState::InfectedSymptomsImprovedImmunityConfirmed}] = 0;
model.populations[{i, mio::osecirts::InfectionState::InfectedSevereNaive}] = 30;
model.populations[{i, mio::osecirts::InfectionState::InfectedSevereImprovedImmunity}] = 30;
model.populations[{i, mio::osecirts::InfectionState::InfectedSeverePartialImmunity}] = 30;
model.populations[{i, mio::osecirts::InfectionState::InfectedCriticalNaive}] = 20;
model.populations[{i, mio::osecirts::InfectionState::InfectedCriticalPartialImmunity}] = 20;
model.populations[{i, mio::osecirts::InfectionState::InfectedCriticalImprovedImmunity}] = 20;
model.populations[{i, mio::osecirts::InfectionState::SusceptibleNaive}] = 1000;
model.populations[{i, mio::osecirts::InfectionState::SusceptiblePartialImmunity}] = 1200;
model.populations[{i, mio::osecirts::InfectionState::SusceptibleImprovedImmunity}] = 1000;
model.populations[{i, mio::osecirts::InfectionState::TemporaryImmunePartialImmunity}] = 60;
model.populations[{i, mio::osecirts::InfectionState::TemporaryImmuneImprovedImmunity}] = 70;
model.populations[{i, mio::osecirts::InfectionState::DeadNaive}] = 0;
model.populations[{i, mio::osecirts::InfectionState::DeadPartialImmunity}] = 0;
model.populations[{i, mio::osecirts::InfectionState::DeadImprovedImmunity}] = 0;
}
After setting the initial populations, the daily vaccination parameters, which are directly integrated into the ODE system in this model, also need to be configured:
const size_t daily_vaccinations = 10;
const size_t num_days = 300;
model.parameters.get<mio::osecirts::DailyPartialVaccinations<double>>().resize(mio::SimulationDay(num_days));
model.parameters.get<mio::osecirts::DailyFullVaccinations<double>>().resize(mio::SimulationDay(num_days));
model.parameters.get<mio::osecirts::DailyBoosterVaccinations<double>>().resize(mio::SimulationDay(num_days));
for (size_t i = 0; i < num_days; ++i) {
for (mio::AgeGroup j = 0; j < nb_groups; ++j) {
auto num_vaccinations = static_cast<double>(i * daily_vaccinations);
model.parameters.get<mio::osecirts::DailyPartialVaccinations<double>>()[{j, mio::SimulationDay(i)}] =
num_vaccinations;
model.parameters.get<mio::osecirts::DailyFullVaccinations<double>>()[{j, mio::SimulationDay(i)}] =
num_vaccinations;
model.parameters.get<mio::osecirts::DailyBoosterVaccinations<double>>()[{j, mio::SimulationDay(i)}] =
num_vaccinations;
}
}
Nonpharmaceutical Interventions
Like the other ODE-SECIR models, the ODE-SECIRTS model supports nonpharmaceutical interventions (NPIs) through dampings in the contact matrix. These dampings reduce the contact rates between different groups to simulate interventions like lockdowns.
Basic dampings can be added to the contact matrix as follows:
// Create a contact matrix with baseline contact rates
auto& contacts = model.parameters.get<mio::osecirts::ContactPatterns<double>>();
auto& contact_matrix = contacts.get_cont_freq_mat();
contact_matrix[0].get_baseline().setConstant(0.5);
contact_matrix[0].get_baseline().diagonal().setConstant(5.0);
// Add a damping that reduces contacts by 30% starting at day 5
contact_matrix[0].add_damping(0.3, mio::SimulationTime(5.0));
The model also supports dynamic NPIs based on epidemic thresholds:
// Set threshold-based triggers for NPIs
auto& dynamic_npis = model.parameters.get<mio::osecirts::DynamicNPIsInfectedSymptoms<double>>();
dynamic_npis.set_interval(mio::SimulationTime(3.0)); // Check every 3 days
dynamic_npis.set_duration(mio::SimulationTime(14.0)); // Apply for 14 days
dynamic_npis.set_base_value(100'000); // Per 100,000 population
dynamic_npis.set_threshold(200.0, dampings); // Trigger at 200 cases per 100,000
For more complex scenarios, such as real-world venue closures or lockdown modeling, detailed NPIs with location-specific dampings can be implemented. For further details, see the documentation of the ODE-SECIR model
Simulation
The ODE-SECIRTS model offers the same simulation functions as the other ODE-SECIR models:
simulate: Standard simulation that tracks the compartment sizes over time
simulate_flows: Extended simulation that additionally tracks the flows between compartments
Standard simulation:
double t0 = 0; // Start time
double tmax = 50; // End time
double dt = 0.1; // Time step
// Run a standard simulation
mio::TimeSeries<double> result = mio::osecirts::simulate<double>(t0, tmax, dt, model);
During simulation, the model handles several special processes:
Vaccinations: Unlike the ODE-SECIRVVS model, vaccinations are integrated directly into the ODE system through flows from susceptible compartments to temporary immunity compartments.
Variant Evolution: The apply_variant function updates the transmission probability based on the existance of a new variant over time, similar to other ODE-SECIR models.
For both simulation types, you can also specify a custom integrator:
auto integrator = std::make_unique<mio::RKIntegratorCore>();
integrator->set_dt_min(0.3);
integrator->set_dt_max(1.0);
integrator->set_rel_tolerance(1e-4);
integrator->set_abs_tolerance(1e-1);
mio::TimeSeries<double> result = mio::osecirts::simulate(t0, tmax, dt, model, std::move(integrator));
Output
The output of the simulation is a mio::TimeSeries object containing the sizes of each compartment at each time point. For a standard simulation, you can access the results as follows:
// Get the number of time points
auto num_points = static_cast<size_t>(result.get_num_time_points());
// Access data at a specific time point
Eigen::VectorXd value_at_time_i = result.get_value(i);
double time_i = result.get_time(i);
// Access the last time point
Eigen::VectorXd last_value = result.get_last_value();
You can print the simulation results as a formatted table:
// Print results to console with default formatting
result.print_table();
The order of the compartments follows the definition in the InfectionState enum.
Additionally, you can export the results to a CSV file for further analysis or visualization:
// Export results to CSV
result.export_csv("simulation_results.csv");
Visualization
To visualize the results of a simulation, you can use the Python package m-plot and its documentation.
Examples
To get started with the ODE-SECIRTS model, check out the code example in the MEmilio repository: examples/ode_secirts.cpp.
The code documentation for the model can be found at mio::osecirts .