Skip to content

Commit 125e1d2

Browse files
lenaploetzkeannawendlerreneSchm
authored
896 add age resolved lct model (#1120)
- Added the possibility to use groups (e.g. age groups) in the LCT-SECIR model. The parameters and the number of subcompartments can be set differently for each group. - Adapted functionality to initialize the model. - Added tests. Co-authored-by: annawendler <106674756+annawendler@users.noreply.github.com> Co-authored-by: reneSchm <49305466+reneSchm@users.noreply.github.com>
1 parent 9020edf commit 125e1d2

File tree

13 files changed

+2162
-987
lines changed

13 files changed

+2162
-987
lines changed

cpp/examples/lct_secir.cpp

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
*/
2020

2121
#include "lct_secir/model.h"
22+
#include "lct_secir/infection_state.h"
2223
#include "lct_secir/initializer_flows.h"
2324
#include "memilio/config.h"
2425
#include "memilio/utils/time_series.h"
2526
#include "memilio/epidemiology/uncertain_matrix.h"
27+
#include "memilio/epidemiology/lct_infection_state.h"
2628
#include "memilio/math/eigen.h"
2729
#include "memilio/utils/logging.h"
2830
#include "memilio/compartments/simulation.h"
@@ -33,14 +35,14 @@
3335
int main()
3436
{
3537
// Simple example to demonstrate how to run a simulation using an LCT-SECIR model.
38+
// One single AgeGroup/Category member is used here.
3639
// Parameters, initial values and the number of subcompartments are not meant to represent a realistic scenario.
3740
constexpr size_t NumExposed = 2, NumInfectedNoSymptoms = 3, NumInfectedSymptoms = 1, NumInfectedSevere = 1,
3841
NumInfectedCritical = 5;
39-
using Model = mio::lsecir::Model<NumExposed, NumInfectedNoSymptoms, NumInfectedSymptoms, NumInfectedSevere,
40-
NumInfectedCritical>;
41-
using LctState = Model::LctState;
42-
using InfectionState = LctState::InfectionState;
43-
42+
using InfState = mio::lsecir::InfectionState;
43+
using LctState = mio::LctInfectionState<InfState, 1, NumExposed, NumInfectedNoSymptoms, NumInfectedSymptoms,
44+
NumInfectedSevere, NumInfectedCritical, 1, 1>;
45+
using Model = mio::lsecir::Model<LctState>;
4446
Model model;
4547

4648
// Variable defines whether the class Initializer is used to define an initial vector from flows or whether a manually
@@ -50,34 +52,33 @@ int main()
5052
ScalarType tmax = 20;
5153

5254
// Set Parameters.
53-
model.parameters.get<mio::lsecir::TimeExposed>() = 3.2;
54-
model.parameters.get<mio::lsecir::TimeInfectedNoSymptoms>() = 2.;
55-
model.parameters.get<mio::lsecir::TimeInfectedSymptoms>() = 5.8;
56-
model.parameters.get<mio::lsecir::TimeInfectedSevere>() = 9.5;
57-
// It is also possible to change values with the set function.
58-
model.parameters.set<mio::lsecir::TimeInfectedCritical>(7.1);
55+
model.parameters.get<mio::lsecir::TimeExposed>()[0] = 3.2;
56+
model.parameters.get<mio::lsecir::TimeInfectedNoSymptoms>()[0] = 2.;
57+
model.parameters.get<mio::lsecir::TimeInfectedSymptoms>()[0] = 5.8;
58+
model.parameters.get<mio::lsecir::TimeInfectedSevere>()[0] = 9.5;
59+
model.parameters.get<mio::lsecir::TimeInfectedCritical>()[0] = 7.1;
5960

60-
model.parameters.get<mio::lsecir::TransmissionProbabilityOnContact>() = 0.05;
61+
model.parameters.get<mio::lsecir::TransmissionProbabilityOnContact>()[0] = 0.05;
6162

6263
mio::ContactMatrixGroup& contact_matrix = model.parameters.get<mio::lsecir::ContactPatterns>();
6364
contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10));
6465
// From SimulationTime 5, the contact pattern is reduced to 30% of the initial value.
6566
contact_matrix[0].add_damping(0.7, mio::SimulationTime(5.));
6667

67-
model.parameters.get<mio::lsecir::RelativeTransmissionNoSymptoms>() = 0.7;
68-
model.parameters.get<mio::lsecir::RiskOfInfectionFromSymptomatic>() = 0.25;
69-
model.parameters.get<mio::lsecir::RecoveredPerInfectedNoSymptoms>() = 0.09;
70-
model.parameters.get<mio::lsecir::SeverePerInfectedSymptoms>() = 0.2;
71-
model.parameters.get<mio::lsecir::CriticalPerSevere>() = 0.25;
72-
model.parameters.set<mio::lsecir::DeathsPerCritical>(0.3);
68+
model.parameters.get<mio::lsecir::RelativeTransmissionNoSymptoms>()[0] = 0.7;
69+
model.parameters.get<mio::lsecir::RiskOfInfectionFromSymptomatic>()[0] = 0.25;
70+
model.parameters.get<mio::lsecir::RecoveredPerInfectedNoSymptoms>()[0] = 0.09;
71+
model.parameters.get<mio::lsecir::SeverePerInfectedSymptoms>()[0] = 0.2;
72+
model.parameters.get<mio::lsecir::CriticalPerSevere>()[0] = 0.25;
73+
model.parameters.get<mio::lsecir::DeathsPerCritical>()[0] = 0.3;
7374

7475
if (use_initializer_flows) {
7576
// Example how to use the class Initializer for the definition of an initial vector for the LCT model.
7677

77-
ScalarType dt = 0.001;
78-
ScalarType total_population = 1000000;
79-
ScalarType deaths = 10;
80-
ScalarType total_confirmed_cases = 16000;
78+
ScalarType dt = 0.001;
79+
mio::Vector<ScalarType> total_population = mio::Vector<ScalarType>::Constant(1, 1000000.);
80+
mio::Vector<ScalarType> deaths = mio::Vector<ScalarType>::Constant(1, 10.);
81+
mio::Vector<ScalarType> total_confirmed_cases = mio::Vector<ScalarType>::Constant(1, 16000.);
8182

8283
// Create TimeSeries with num_transitions elements.
8384
int num_transitions = (int)mio::lsecir::InfectionTransition::Count;
@@ -120,22 +121,22 @@ int main()
120121
{50}, {10, 10, 5, 3, 2}, {20}, {10}};
121122

122123
// Assert that initial_populations has the right shape.
123-
if (initial_populations.size() != (size_t)InfectionState::Count) {
124+
if (initial_populations.size() != (size_t)InfState::Count) {
124125
mio::log_error(
125126
"The number of vectors in initial_populations does not match the number of InfectionStates.");
126127
return 1;
127128
}
128-
if ((initial_populations[(size_t)InfectionState::Susceptible].size() !=
129-
LctState::get_num_subcompartments<InfectionState::Susceptible>()) ||
130-
(initial_populations[(size_t)InfectionState::Exposed].size() != NumExposed) ||
131-
(initial_populations[(size_t)InfectionState::InfectedNoSymptoms].size() != NumInfectedNoSymptoms) ||
132-
(initial_populations[(size_t)InfectionState::InfectedSymptoms].size() != NumInfectedSymptoms) ||
133-
(initial_populations[(size_t)InfectionState::InfectedSevere].size() != NumInfectedSevere) ||
134-
(initial_populations[(size_t)InfectionState::InfectedCritical].size() != NumInfectedCritical) ||
135-
(initial_populations[(size_t)InfectionState::Recovered].size() !=
136-
LctState::get_num_subcompartments<InfectionState::Recovered>()) ||
137-
(initial_populations[(size_t)InfectionState::Dead].size() !=
138-
LctState::get_num_subcompartments<InfectionState::Dead>())) {
129+
if ((initial_populations[(size_t)InfState::Susceptible].size() !=
130+
LctState::get_num_subcompartments<InfState::Susceptible>()) ||
131+
(initial_populations[(size_t)InfState::Exposed].size() != NumExposed) ||
132+
(initial_populations[(size_t)InfState::InfectedNoSymptoms].size() != NumInfectedNoSymptoms) ||
133+
(initial_populations[(size_t)InfState::InfectedSymptoms].size() != NumInfectedSymptoms) ||
134+
(initial_populations[(size_t)InfState::InfectedSevere].size() != NumInfectedSevere) ||
135+
(initial_populations[(size_t)InfState::InfectedCritical].size() != NumInfectedCritical) ||
136+
(initial_populations[(size_t)InfState::Recovered].size() !=
137+
LctState::get_num_subcompartments<InfState::Recovered>()) ||
138+
(initial_populations[(size_t)InfState::Dead].size() !=
139+
LctState::get_num_subcompartments<InfState::Dead>())) {
139140
mio::log_error(
140141
"The length of at least one vector in initial_populations does not match the related number of "
141142
"subcompartments.");
@@ -148,15 +149,16 @@ int main()
148149
flat_initial_populations.insert(flat_initial_populations.end(), vec.begin(), vec.end());
149150
}
150151
for (size_t i = 0; i < LctState::Count; i++) {
151-
model.populations[mio::Index<LctState>(i)] = flat_initial_populations[i];
152+
model.populations[i] = flat_initial_populations[i];
152153
}
153154
}
154155

155156
// Perform a simulation.
156157
mio::TimeSeries<ScalarType> result = mio::simulate<ScalarType, Model>(0, tmax, 0.5, model);
158+
//result.print_table({"S", "E", "C", "I", "H", "U", "R", "D "}, 16, 8);
157159
// The simulation result is divided by subcompartments.
158160
// We call the function calculate_comparttments to get a result according to the InfectionStates.
159161
mio::TimeSeries<ScalarType> population_no_subcompartments = model.calculate_compartments(result);
160162
auto interpolated_results = mio::interpolate_simulation_result(population_no_subcompartments);
161163
interpolated_results.print_table({"S", "E", "C", "I", "H", "U", "R", "D "}, 16, 8);
162-
}
164+
}

cpp/memilio/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ add_library(memilio
1717
epidemiology/dynamic_npis.h
1818
epidemiology/dynamic_npis.cpp
1919
epidemiology/lct_infection_state.h
20+
epidemiology/lct_populations.h
2021
geography/regions.h
2122
geography/regions.cpp
2223
epidemiology/simulation_day.h

cpp/memilio/epidemiology/lct_infection_state.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ namespace mio
2626
{
2727
/**
2828
* @brief Provides the functionality to be able to work with subcompartments in an LCT model.
29+
30+
* This class just stores the number of subcompartments for each InfectionState and not the number of individuals in
31+
* each subcompartment.
2932
*
3033
* @tparam InfectionStates An enum class that defines the basic infection states.
3134
* @tparam Ns Number of subcompartments for each infection state defined in InfectionState.
32-
* The number of given template arguments must be equal to the entry Count from InfectionState.
35+
* The number of given template arguments must be equal to the entry Count from InfectionStates.
3336
*/
3437
template <class InfectionStates, size_t... Ns>
3538
class LctInfectionState
@@ -84,4 +87,4 @@ class LctInfectionState
8487

8588
} // namespace mio
8689

87-
#endif
90+
#endif
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* Copyright (C) 2020-2024 MEmilio
3+
*
4+
* Authors: Lena Ploetzke
5+
*
6+
* Contact: Martin J. Kuehn <Martin.Kuehn@DLR.de>
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
#ifndef MIO_EPI_LCT_POPULATIONS_H
21+
#define MIO_EPI_LCT_POPULATIONS_H
22+
23+
#include "boost/type_traits/make_void.hpp"
24+
#include "memilio/config.h"
25+
#include "memilio/utils/uncertain_value.h"
26+
#include "memilio/math/eigen.h"
27+
#include "memilio/epidemiology/lct_infection_state.h"
28+
#include "memilio/utils/type_list.h"
29+
#include "memilio/utils/metaprogramming.h"
30+
31+
namespace mio
32+
{
33+
34+
/**
35+
* @brief A class template for compartment populations of LCT models.
36+
*
37+
* Populations can be split up into different categories, e.g. by age group, yearly income group, gender etc.
38+
* In LCT models, we want to use different numbers of subcompartments, i.e. different LctStates,
39+
* for each group of a category.
40+
* (Therefore, we can't use the normal Populations class because it expects the same InfectionStates for each group.)
41+
*
42+
* This template must be instantiated with one LctState for each group of a category.
43+
* The purpose of the LctStates is to define the number of subcompartments for each InfectionState.
44+
* The number of LctStates also determines the number of groups.
45+
* If you want to use more than one category, e.g. age and gender, you have to define num_age_groups * num_genders
46+
* LctStates, because the number of subcompartments can be different
47+
* even for (female, A05-A14) and (female, A80+) or (male, A05-A14).
48+
*
49+
* The class created from this template contains a "flat array" of compartment populations and some functions for
50+
* retrieving or setting the populations. The order in the flat array is: First, all (sub-)compartments of the
51+
* first group, afterwards all (sub-)compartments of the second group and so on.
52+
*
53+
*/
54+
55+
template <typename FP = ScalarType, class... LctStates>
56+
class LctPopulations
57+
{
58+
public:
59+
using Type = UncertainValue<FP>;
60+
using InternalArrayType = Eigen::Array<Type, Eigen::Dynamic, 1>;
61+
using LctStatesGroups = TypeList<LctStates...>;
62+
static size_t constexpr num_groups = sizeof...(LctStates); ///< Number of groups.
63+
static_assert(num_groups >= 1, "The number of LctStates provided should be at least one.");
64+
65+
/// @brief Default constructor.
66+
LctPopulations()
67+
{
68+
set_count();
69+
m_y = InternalArrayType::Constant(m_count, 1, 0.);
70+
}
71+
72+
/**
73+
* @brief get_num_compartments Returns the number of compartments.
74+
* @return Number of compartments which equals the flat array size.
75+
*/
76+
size_t get_num_compartments() const
77+
{
78+
return m_count;
79+
}
80+
81+
/**
82+
* @brief Returns a reference to the internally stored flat array.
83+
* @return Const reference to the InternalArrayType instance.
84+
*/
85+
auto const& array() const
86+
{
87+
return m_y;
88+
}
89+
auto& array()
90+
{
91+
return m_y;
92+
}
93+
94+
/**
95+
* @brief Returns the entry of the array given a flat index.
96+
* @param index A flat index.
97+
* @return The value of the internal array at the index.
98+
*/
99+
Type& operator[](size_t index)
100+
{
101+
assert(index < m_count);
102+
return m_y[index];
103+
}
104+
105+
/**
106+
* @brief Gets the first index of a group in the flat array.
107+
* @tparam group The group for which the index should be returned.
108+
* @return The index of the first entry of group in the flat array.
109+
*/
110+
template <size_t Group>
111+
size_t get_first_index_of_group() const
112+
{
113+
static_assert((Group < num_groups) && (Group >= 0), "The template parameter Group should be valid.");
114+
if constexpr (Group == 0) {
115+
return 0;
116+
}
117+
else {
118+
return get_first_index_of_group<Group - 1>() + type_at_index_t<Group - 1, LctStatesGroups>::Count;
119+
}
120+
}
121+
/**
122+
* @brief Returns an Eigen copy of the vector of populations.
123+
* This can be used as initial conditions for the ODE solver.
124+
* @return Eigen::VectorXd of populations.
125+
*/
126+
inline Vector<FP> get_compartments() const
127+
{
128+
return m_y.array().template cast<FP>();
129+
}
130+
131+
/**
132+
* @brief Returns the total population of a group.
133+
* @tparam group The group for which the total population should be calculated.
134+
* @return Total population of the group.
135+
*/
136+
template <size_t Group>
137+
FP get_group_total() const
138+
{
139+
return m_y.array()
140+
.template cast<FP>()
141+
.segment(get_first_index_of_group<Group>(), type_at_index_t<Group, LctStatesGroups>::Count)
142+
.sum();
143+
}
144+
145+
/**
146+
* @brief Returns the total population of all compartments and groups.
147+
* @return Total population.
148+
*/
149+
FP get_total() const
150+
{
151+
return m_y.array().template cast<FP>().sum();
152+
}
153+
154+
/**
155+
* @brief Checks whether all compartments have non-negative values.
156+
* This function can be used to prevent slightly negative function values in compartment sizes that are produced
157+
* due to rounding errors if, e.g., population sizes were computed in a complex way.
158+
*
159+
* Attention: This function should be used with care. It can not and will not set model parameters and
160+
* compartments to meaningful values. In most cases it is preferable to use check_constraints,
161+
* and correct values manually before proceeding with the simulation.
162+
* The main usage for apply_constraints is in automated tests using random values for initialization.
163+
*
164+
* @return Returns true if one (or more) constraint(s) were corrected, otherwise false.
165+
*/
166+
bool apply_constraints()
167+
{
168+
bool corrected = false;
169+
for (int i = 0; i < m_y.array().size(); i++) {
170+
if (m_y.array()[i] < 0) {
171+
log_warning("Constraint check: Compartment size {:d} changed from {:.4f} to {:d}", i, m_y.array()[i],
172+
0);
173+
m_y.array()[i] = 0.;
174+
corrected = true;
175+
}
176+
}
177+
return corrected;
178+
}
179+
180+
/**
181+
* @brief Checks whether all compartments have non-negative values and logs an error if constraint is not satisfied.
182+
* @return Returns true if one or more constraints are not satisfied, false otherwise.
183+
*/
184+
bool check_constraints() const
185+
{
186+
if ((m_y.array() < 0.).any()) {
187+
log_error("Constraint check: At least one compartment size is smaller {}.", 0);
188+
return true;
189+
}
190+
return false;
191+
}
192+
193+
private:
194+
/**
195+
* @brief Sets recursively the total number of (sub-)compartments over all groups.
196+
* The number also corresponds to the size of the internal vector.
197+
*/
198+
template <size_t Group = 0>
199+
void set_count()
200+
{
201+
if constexpr (Group == 0) {
202+
m_count = 0;
203+
}
204+
if constexpr (Group < num_groups) {
205+
m_count += type_at_index_t<Group, LctStatesGroups>::Count;
206+
set_count<Group + 1>();
207+
}
208+
}
209+
210+
size_t m_count; //< Number of groups stored.
211+
InternalArrayType m_y{}; //< An array containing the number of people in the groups.
212+
};
213+
214+
} // namespace mio
215+
216+
#endif // MIO_EPI_LCT_POPULATIONS_H

cpp/models/lct_secir/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ target_include_directories(lct_secir PUBLIC
1111
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
1212
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
1313
)
14-
target_compile_options(lct_secir PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS})
14+
target_compile_options(lct_secir PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS})

0 commit comments

Comments
 (0)