Skip to content

Commit 0f3158c

Browse files
1087 adapt parameters_io of ide model (#1095)
- Shifted the deaths data to get data according to the day of death instead of the day of the positive test. - Corrected bug to allow correct initialization of flow InfectedNoSymptomsToRecovered. Co-authored-by: lenaploetzke <70579874+lenaploetzke@users.noreply.github.com>
1 parent f864b19 commit 0f3158c

File tree

4 files changed

+94
-20
lines changed

4 files changed

+94
-20
lines changed

cpp/models/ide_secir/parameters_io.cpp

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ IOResult<void> set_initial_flows(Model& model, ScalarType dt, std::string const&
5656
log_error("Specified date does not exist in RKI data.");
5757
return failure(StatusCode::OutOfRange, path + ", specified date does not exist in RKI data.");
5858
}
59+
auto min_date_entry = std::min_element(rki_data.begin(), rki_data.end(), [](auto&& a, auto&& b) {
60+
return a.date < b.date;
61+
});
62+
auto min_date = min_date_entry->date;
5963

6064
// Get (global) support_max to determine how many flows in the past we have to compute.
6165
ScalarType global_support_max = model.get_global_support_max(dt);
@@ -71,17 +75,33 @@ IOResult<void> set_initial_flows(Model& model, ScalarType dt, std::string const&
7175
0, TimeSeries<ScalarType>::Vector::Constant((int)InfectionState::Count, 0));
7276
}
7377

74-
// The first time we need is -4 * global_support_max.
75-
Eigen::Index start_shift = 4 * global_support_max_index;
76-
// The last time needed is dependent on the mean stay time in the Exposed compartment and
77-
// the mean stay time of asymptomatic individuals in InfectedNoSymptoms.
78+
// Set the Dead compartment to 0 so that RKI data can be added correctly.
79+
model.m_populations[0][Eigen::Index(InfectionState::Dead)] = 0;
80+
81+
// Define variables for the mean of transitions used.
7882
ScalarType mean_ExposedToInfectedNoSymptoms =
7983
model.parameters.get<TransitionDistributions>()[Eigen::Index(InfectionTransition::ExposedToInfectedNoSymptoms)]
8084
.get_mean(dt);
8185
ScalarType mean_InfectedNoSymptomsToInfectedSymptoms =
8286
model.parameters
83-
.get<TransitionDistributions>()[Eigen::Index(InfectionTransition::InfectedNoSymptomsToInfectedSymptoms)]
87+
.get<TransitionDistributions>()[(Eigen::Index)InfectionTransition::InfectedNoSymptomsToInfectedSymptoms]
8488
.get_mean(dt);
89+
ScalarType mean_InfectedSymptomsToInfectedSevere =
90+
model.parameters
91+
.get<TransitionDistributions>()[(Eigen::Index)InfectionTransition::InfectedSymptomsToInfectedSevere]
92+
.get_mean();
93+
ScalarType mean_InfectedSevereToInfectedCritical =
94+
model.parameters
95+
.get<TransitionDistributions>()[(Eigen::Index)InfectionTransition::InfectedSevereToInfectedCritical]
96+
.get_mean();
97+
ScalarType mean_InfectedCriticalToDead =
98+
model.parameters.get<TransitionDistributions>()[(Eigen::Index)InfectionTransition::InfectedCriticalToDead]
99+
.get_mean();
100+
101+
// The first time we need is -4 * global_support_max.
102+
Eigen::Index start_shift = 4 * global_support_max_index;
103+
// The last time needed is dependent on the mean stay time in the Exposed compartment and
104+
// the mean stay time of asymptomatic individuals in InfectedNoSymptoms.
85105
Eigen::Index last_time_index_needed =
86106
Eigen::Index(std::ceil((mean_ExposedToInfectedNoSymptoms + mean_InfectedNoSymptomsToInfectedSymptoms) / dt));
87107
// Create TimeSeries with zeros. The index of time zero is start_shift.
@@ -99,6 +119,7 @@ IOResult<void> set_initial_flows(Model& model, ScalarType dt, std::string const&
99119

100120
bool min_offset_needed_avail = false;
101121
bool max_offset_needed_avail = false;
122+
102123
// Go through the entries of rki_data and check if date is needed for calculation. Confirmed cases are scaled.
103124
// Define dummy variables to store the first and the last index of the TimeSeries where the considered entry of rki_data is potentially needed.
104125
Eigen::Index idx_needed_first = 0;
@@ -110,6 +131,9 @@ IOResult<void> set_initial_flows(Model& model, ScalarType dt, std::string const&
110131
if (offset == min_offset_needed) {
111132
min_offset_needed_avail = true;
112133
}
134+
if (offset == max_offset_needed) {
135+
max_offset_needed_avail = true;
136+
}
113137
// Smallest index for which the entry is needed.
114138
idx_needed_first =
115139
Eigen::Index(std::max(std::floor((offset - model.m_transitions.get_time(0) - 1) / dt), 0.));
@@ -137,21 +161,51 @@ IOResult<void> set_initial_flows(Model& model, ScalarType dt, std::string const&
137161
}
138162
}
139163

140-
if (offset == max_offset_needed) {
141-
max_offset_needed_avail = true;
164+
// Compute Dead by shifting RKI data according to mean stay times.
165+
// This is done because the RKI reports death with the date of positive test instead of the date of deaths.
166+
if (offset == std::floor(-mean_InfectedSymptomsToInfectedSevere - mean_InfectedSevereToInfectedCritical -
167+
mean_InfectedCriticalToDead)) {
168+
model.m_populations[0][Eigen::Index(InfectionState::Dead)] +=
169+
(1 - (-mean_InfectedSymptomsToInfectedSevere - mean_InfectedSevereToInfectedCritical -
170+
mean_InfectedCriticalToDead -
171+
std::floor(-mean_InfectedSymptomsToInfectedSevere - mean_InfectedSevereToInfectedCritical -
172+
mean_InfectedCriticalToDead))) *
173+
entry.num_deaths;
142174
}
175+
if (offset == std::ceil(-mean_InfectedSymptomsToInfectedSevere - mean_InfectedSevereToInfectedCritical -
176+
mean_InfectedCriticalToDead)) {
177+
model.m_populations[0][Eigen::Index(InfectionState::Dead)] +=
178+
(-mean_InfectedSymptomsToInfectedSevere - mean_InfectedSevereToInfectedCritical -
179+
mean_InfectedCriticalToDead -
180+
std::floor(-mean_InfectedSymptomsToInfectedSevere - mean_InfectedSevereToInfectedCritical -
181+
mean_InfectedCriticalToDead)) *
182+
entry.num_deaths;
183+
}
184+
143185
if (offset == 0) {
144-
model.m_populations[0][Eigen::Index(InfectionState::Dead)] = entry.num_deaths;
145186
model.m_total_confirmed_cases = scale_confirmed_cases * entry.num_confirmed;
146187
}
147188
}
148189
}
149190

150-
if (!max_offset_needed_avail || !min_offset_needed_avail) {
191+
if (!max_offset_needed_avail) {
151192
log_error("Necessary range of dates needed to compute initial values does not exist in RKI data.");
152193
return failure(StatusCode::OutOfRange, path + ", necessary range of dates does not exist in RKI data.");
153194
}
154195

196+
if (!min_offset_needed_avail) {
197+
std::string min_date_string =
198+
std::to_string(min_date.day) + "." + std::to_string(min_date.month) + "." + std::to_string(min_date.year);
199+
// Get first date that is needed.
200+
mio::Date min_offset_date = offset_date_by_days(date, int(min_offset_needed));
201+
std::string min_offset_date_string = std::to_string(min_offset_date.day) + "." +
202+
std::to_string(min_offset_date.month) + "." +
203+
std::to_string(min_offset_date.year);
204+
log_warning("RKI data is needed from " + min_offset_date_string +
205+
" to compute initial values. RKI data is only available from " + min_date_string +
206+
". Missing dates were set to 0.");
207+
}
208+
155209
//--- Calculate the flows "after" InfectedNoSymptomsToInfectedSymptoms. ---
156210
// Compute flow InfectedSymptomsToInfectedSevere for -3 * global_support_max, ..., 0.
157211
for (Eigen::Index i = -3 * global_support_max_index; i <= 0; i++) {
@@ -183,10 +237,10 @@ IOResult<void> set_initial_flows(Model& model, ScalarType dt, std::string const&
183237
}
184238

185239
//--- Calculate the remaining flows. ---
186-
// Compute flow ExposedToInfectedNoSymptoms for -global_support_max, ..., 0.
240+
// Compute flow ExposedToInfectedNoSymptoms for -2 * global_support_max, ..., 0.
187241
// Use mean value of the TransitionDistribution InfectedNoSymptomsToInfectedSymptoms for the calculation.
188242
Eigen::Index index_shift_mean = Eigen::Index(std::round(mean_InfectedNoSymptomsToInfectedSymptoms / dt));
189-
for (Eigen::Index i = -global_support_max_index; i <= 0; i++) {
243+
for (Eigen::Index i = -2 * global_support_max_index; i <= 0; i++) {
190244
model.m_transitions[i + start_shift][Eigen::Index(InfectionTransition::ExposedToInfectedNoSymptoms)] =
191245
(1 / model.parameters.get<TransitionProbabilities>()[Eigen::Index(
192246
InfectionTransition::InfectedNoSymptomsToInfectedSymptoms)]) *
@@ -226,4 +280,4 @@ IOResult<void> set_initial_flows(Model& model, ScalarType dt, std::string const&
226280
} // namespace isecir
227281
} // namespace mio
228282

229-
#endif // MEMILIO_HAS_JSONCPP
283+
#endif // MEMILIO_HAS_JSONCPP

cpp/models/ide_secir/parameters_io.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ namespace isecir
5252
* The flow InfectedNoSymptomsToInfectedSymptoms is calculated with the standard formula from the IDE model
5353
* using the results for ExposedToInfectedNoSymptoms.
5454
*
55-
* The number of deaths used in the model is set to the number given in the RKI data.
55+
* The number of deaths used in the model is computed by shifting the reported RKI data according to the mean values of the transitions
56+
* InfectedSymptomsToInfectedSevere, InfectedSevereToInfectedCritical and InfectedCriticalToDead.
5657
* We also set the number of total confirmed cases in the model.
5758
* Therefore the initialization method using the total confirmed cases is used in the model. See also the documentation of the model class.
5859
*
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Time S->E E->C C->I C->R I->H I->R H->U H->R U->D U->R
2-
-2.00000000 25.50000000 0.50000000 4.50000000 0.03661165 1.83235622 1.67500000 0.50414276 0.50414276 0.13557638 0.13557638
3-
-1.50000000 30.00000000 25.50000000 0.25000000 1.95558262 1.78897083 1.77038735 0.73448590 0.73448590 0.20627078 0.20627078
4-
-1.00000000 30.00000000 25.50000000 0.25000000 6.46338835 0.89673307 1.18750000 0.80038452 0.80038452 0.29817594 0.29817594
2+
-2.00000000 25.50000000 0.50000000 4.50000000 2.37500000 1.83235622 1.67500000 0.50414276 0.50414276 0.13557638 0.13557638
3+
-1.50000000 30.00000000 25.50000000 0.25000000 2.70298071 1.78897083 1.77038735 0.73448590 0.73448590 0.20627078 0.20627078
4+
-1.00000000 30.00000000 25.50000000 0.25000000 6.50000000 0.89673307 1.18750000 0.80038452 0.80038452 0.29817594 0.29817594
55
-0.50000000 0.30000000 30.00000000 12.75000000 11.24892225 1.43851502 1.35149035 0.71427386 0.71427386 0.36054581 0.36054581
66
0.00000000 0.30000000 30.00000000 12.75000000 13.87500000 4.10519684 3.25000000 0.84440788 0.84440788 0.38336812 0.38336812

cpp/tests/test_ide_parameters_io.cpp

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ TEST(TestIDEParametersIo, RKIcompareWithPreviousRun)
7878
// Compare with previous run.
7979
auto compare = load_test_data_csv<ScalarType>("ide-parameters-io-compare.csv");
8080

81+
EXPECT_NEAR(model.m_populations.get_value(0)[(Eigen::Index)mio::isecir::InfectionState::Dead], 6.3, 1e-4);
82+
EXPECT_NEAR(model.m_total_confirmed_cases, 70, 1e-4);
83+
8184
ASSERT_EQ(compare.size(), static_cast<size_t>(model.m_transitions.get_num_time_points()));
8285
for (size_t i = 0; i < compare.size(); i++) {
8386
ASSERT_EQ(compare[i].size(), static_cast<size_t>(model.m_transitions.get_num_elements()) + 1) << "at row " << i;
@@ -91,12 +94,13 @@ TEST(TestIDEParametersIo, RKIcompareWithPreviousRun)
9194
// Check some cases where computation of initial values for an IDE model based on RKI data should fail.
9295
TEST(TestIDEParametersIo, ParametersIoRKIFailure)
9396
{
94-
// Define start date and the total population used for the initialization.
97+
// Set up.
9598
ScalarType total_population = 80 * 1e6;
9699
ScalarType dt = 0.5;
97100

98101
// Initialize model.
99-
// The number of deaths will be overwritten if real data is used for initialization. Therefore, an arbitrary number is used for the number of deaths.
102+
// The number of deaths will be overwritten if real data is used for initialization.
103+
// Therefore, an arbitrary number is used for the number of deaths.
100104
mio::isecir::Model model(mio::TimeSeries<ScalarType>((int)mio::isecir::InfectionTransition::Count),
101105
total_population, -1);
102106

@@ -110,13 +114,28 @@ TEST(TestIDEParametersIo, ParametersIoRKIFailure)
110114

111115
ASSERT_THAT(print_wrap(status), IsFailure(mio::StatusCode::OutOfRange));
112116

113-
// --- Case where not all needed dates are provided.
117+
// --- Case where not all needed dates from the future are provided.
114118
start_date = mio::Date(2020, 6, 7);
115119
status =
116120
mio::isecir::set_initial_flows(model, dt, mio::path_join(TEST_DATA_DIR, "cases_all_germany.json"), start_date);
117121

118122
ASSERT_THAT(print_wrap(status), IsFailure(mio::StatusCode::OutOfRange));
119123

124+
// --- Case where not all needed dates from the past are provided.
125+
start_date = mio::Date(2020, 5, 24);
126+
status =
127+
mio::isecir::set_initial_flows(model, dt, mio::path_join(TEST_DATA_DIR, "cases_all_germany.json"), start_date);
128+
// Check that status is Success as just a warning is logged.
129+
ASSERT_THAT(print_wrap(status), IsSuccess());
130+
// Check that the flow InfectedNoSymptomsToInfectedSymptoms has actually been set to 0.
131+
// As start_date is the first date in the data file, there is some infection data for all times >-1
132+
// (as we interpolate linearly in between -1 and 0). Therefore, we only expect the flow InfectedNoSymptomsToInfectedSymptoms
133+
// to be zero for times <=-1.
134+
for (Eigen::Index i = 0; i < model.m_transitions.get_num_time_points() - 2; i++) {
135+
EXPECT_EQ(0., model.m_transitions.get_value(
136+
i)[(Eigen::Index)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms]);
137+
}
138+
120139
// --- Case with empty RKI data file.
121140
status =
122141
mio::isecir::set_initial_flows(model, dt, mio::path_join(TEST_DATA_DIR, "test_empty_file.json"), start_date);
@@ -125,4 +144,4 @@ TEST(TestIDEParametersIo, ParametersIoRKIFailure)
125144

126145
// Reactive log output.
127146
mio::set_log_level(mio::LogLevel::warn);
128-
}
147+
}

0 commit comments

Comments
 (0)