diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 2da730d298..d3625ed7b8 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -120,6 +120,7 @@ if(MEMILIO_BUILD_MODELS) add_subdirectory(models/ode_secir) add_subdirectory(models/ode_secirvvs) add_subdirectory(models/ide_secir) + add_subdirectory(models/lct_secir) add_subdirectory(models/ide_seir) add_subdirectory(models/ode_seir) endif() diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 0184b90246..11c622e276 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -58,6 +58,13 @@ add_executable(ide_secir_example ide_secir.cpp) target_link_libraries(ide_secir_example PRIVATE memilio ide_secir) target_compile_options(ide_secir_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +add_executable(compute_parameters compute_parameters.cpp) +target_link_libraries(compute_parameters PRIVATE memilio) + +add_executable(lct_secir_example lct_secir.cpp) +target_link_libraries(lct_secir_example PRIVATE memilio lct_secir) + + if(MEMILIO_HAS_JSONCPP) add_executable(ode_secir_read_graph_example ode_secir_read_graph.cpp) target_link_libraries(ode_secir_read_graph_example PRIVATE memilio ode_secir) @@ -88,3 +95,18 @@ if(MEMILIO_HAS_HDF5) target_link_libraries(ode_secir_save_results_example PRIVATE memilio ode_secir) target_compile_options(ode_secir_save_results_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) endif() + +if(MEMILIO_HAS_HDF5) + add_executable(lct_secir_fictional_scenario lct_secir_fictional_scenario.cpp) + target_link_libraries(lct_secir_fictional_scenario PRIVATE memilio lct_secir ide_secir) +endif() + +if(MEMILIO_HAS_HDF5) + add_executable(lct_secir_real_scenario lct_secir_real_scenario.cpp) + target_link_libraries(lct_secir_real_scenario PRIVATE memilio lct_secir) +endif() + +if(MEMILIO_HAS_HDF5) + add_executable(lct_secir_initializations lct_secir_initializations.cpp) + target_link_libraries(lct_secir_initializations PRIVATE memilio lct_secir ide_secir) +endif() \ No newline at end of file diff --git a/cpp/examples/compute_parameters.cpp b/cpp/examples/compute_parameters.cpp new file mode 100644 index 0000000000..31699b524a --- /dev/null +++ b/cpp/examples/compute_parameters.cpp @@ -0,0 +1,209 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include + +int main() +{ + /** With this file parameters without age distribution can be calculated to match those specified in the first published paper + (https://doi.org/10.1016/j.mbs.2021.108648). + + For ODE or LCT models, the parameters from the paper are used first to derive just one transitiontime if 2 are specified, eg T_C=\mu_C^R*T_C^R+(1-\mu_C^R)*T_C^I. + For an IDE mdel, this step is omitted. + For each age group, average values are calculated if lower and upper bounds are given. + For this we assume a uniform distribution so that T=(T_min+T_max)/2. + Finally we calculate a weighted average time across the age groups. + */ + bool printResult = true; + + // Age group sizes are calculated using table number 12411-04-02-4-B from www.regionalstatistik.de for the date 31.12.2020. + const double age_group_sizes[] = {3969138.0, 7508662, 18921292, 28666166, 18153339, 5936434}; + const int total = 83155031.0; + const int numagegroups = 6; + + // Transmission parameters. + const double transmissionProbabilityOnContactMin[] = {0.02, 0.05, 0.05, 0.05, 0.08, 0.15}; + const double transmissionProbabilityOnContactMax[] = {0.04, 0.07, 0.07, 0.07, 0.10, 0.20}; + + double transmissionProbabilityOnContact = 0; + for (int i = 0; i < numagegroups; i++) { + transmissionProbabilityOnContact += + (age_group_sizes[i] / total) * 0.5 * + (transmissionProbabilityOnContactMin[i] + transmissionProbabilityOnContactMax[i]); + } + + if (printResult) { + std::cout << "transmissionProbabilityOnContact: " << transmissionProbabilityOnContact << std::endl; + } + + const double relativeTransmissionNoSymptoms = 1; + const double riskOfInfectionFromSymptomatic = 0.3; + + if (printResult) { + std::cout << "relativeTransmissionNoSymptoms: " << relativeTransmissionNoSymptoms << std::endl; + std::cout << "riskOfInfectionFromSymptomatic: " << riskOfInfectionFromSymptomatic << std::endl; + } + + // E + const double timeExposedMin = 2.67; + const double timeExposedMax = 4.00; + + double timeExposed = 0.5 * (timeExposedMin + timeExposedMax); + + if (printResult) { + std::cout << "timeExposed: " << timeExposed << std::endl; + } + + // Calculate parameters for I first because a value of I is needed for C. + // I + const double timeInfectedSymptomstoRecoveredMin = 5.6; + const double timeInfectedSymptomstoRecoveredMax = 8.4; + const double timeInfectedSymptomstoInfectedSevereMin[] = {9, 9, 9, 5, 5, 5}; + const double timeInfectedSymptomstoInfectedSevereMax[] = {12, 12, 12, 7, 7, 7}; + const double severePerInfectedSymptomsMin[] = {0.006, 0.006, 0.015, 0.049, 0.15, 0.20}; + const double severePerInfectedSymptomsMax[] = {0.009, 0.009, 0.023, 0.074, 0.18, 0.25}; + + double timeInfectedSymptomsMindummy; + double timeInfectedSymptomsMaxdummy; + double severePerInfectedSymptomsdummy; + double timeInfectedSymptoms = 0; + double severePerInfectedSymptoms = 0; + + for (int i = 0; i < numagegroups; i++) { + severePerInfectedSymptomsdummy = 0.5 * (severePerInfectedSymptomsMin[i] + severePerInfectedSymptomsMax[i]); + timeInfectedSymptomsMindummy = (1 - severePerInfectedSymptomsdummy) * timeInfectedSymptomstoRecoveredMin + + severePerInfectedSymptomsdummy * timeInfectedSymptomstoInfectedSevereMin[i]; + timeInfectedSymptomsMaxdummy = (1 - severePerInfectedSymptomsdummy) * timeInfectedSymptomstoRecoveredMax + + severePerInfectedSymptomsdummy * timeInfectedSymptomstoInfectedSevereMax[i]; + + timeInfectedSymptoms += + (age_group_sizes[i] / total) * 0.5 * (timeInfectedSymptomsMindummy + timeInfectedSymptomsMaxdummy); + severePerInfectedSymptoms += (age_group_sizes[i] / total) * severePerInfectedSymptomsdummy; + } + + // Calculation for an IDE model. + const double timeInfectedSymptomstoRecovered = + 0.5 * (timeInfectedSymptomstoRecoveredMin + timeInfectedSymptomstoRecoveredMax); + double timeInfectedSymptomstoInfectedSevere = 0; + + for (int i = 0; i < numagegroups; i++) { + severePerInfectedSymptomsdummy = 0.5 * (severePerInfectedSymptomsMin[i] + severePerInfectedSymptomsMax[i]); + timeInfectedSymptomstoInfectedSevere += + severePerInfectedSymptomsdummy * (age_group_sizes[i] / total) * 0.5 * + (timeInfectedSymptomstoInfectedSevereMin[i] + timeInfectedSymptomstoInfectedSevereMax[i]); + } + timeInfectedSymptomstoInfectedSevere = timeInfectedSymptomstoInfectedSevere / severePerInfectedSymptoms; + // C + const double timeInfectedNoSymptomstoInfectedSymptoms = 5.2 - timeExposed; + const double timeInfectedNoSymptomstoRecovered = + timeInfectedNoSymptomstoInfectedSymptoms + + 0.5 * (timeInfectedSymptomstoRecoveredMin + timeInfectedSymptomstoRecoveredMax); + const double recoveredPerInfectedNoSymptomsMin[] = {0.2, 0.2, 0.15, 0.15, 0.15, 0.15}; + const double recoveredPerInfectedNoSymptomsMax[] = {0.3, 0.3, 0.25, 0.25, 0.25, 0.25}; + + double timeInfectedNoSymptoms = 0; + double recoveredPerInfectedNoSymptoms = 0; + + for (int i = 0; i < numagegroups; i++) { + recoveredPerInfectedNoSymptoms += (age_group_sizes[i] / total) * 0.5 * + (recoveredPerInfectedNoSymptomsMin[i] + recoveredPerInfectedNoSymptomsMax[i]); + } + + timeInfectedNoSymptoms = recoveredPerInfectedNoSymptoms * timeInfectedNoSymptomstoRecovered + + (1 - recoveredPerInfectedNoSymptoms) * timeInfectedNoSymptomstoInfectedSymptoms; + + if (printResult) { + std::cout << "timeInfectedNoSymptoms: " << timeInfectedNoSymptoms << std::endl; + std::cout << "recoveredPerInfectedNoSymptoms: " << recoveredPerInfectedNoSymptoms << std::endl; + std::cout << "timeInfectedSymptoms: " << timeInfectedSymptoms << std::endl; + std::cout << "severePerInfectedSymptoms: " << severePerInfectedSymptoms << std::endl; + } + + // H + const double timeInfectedSeveretoRecoveredMin[] = {4, 4, 5, 7, 9, 13}; + const double timeInfectedSeveretoRecoveredMax[] = {6, 6, 7, 9, 11, 17}; + const double timeInfectedSeveretoInfectedCriticalMin = 3; + const double timeInfectedSeveretoInfectedCriticalMax = 7; + const double criticalPerSevereMin[] = {0.05, 0.05, 0.05, 0.10, 0.25, 0.35}; + const double criticalPerSevereMax[] = {0.10, 0.10, 0.10, 0.20, 0.35, 0.45}; + + double timeInfectedSevereMindummy; + double timeInfectedSevereMaxdummy; + double criticalPerSeveredummy; + double timeInfectedSevere = 0; + double criticalPerSevere = 0; + + for (int i = 0; i < numagegroups; i++) { + criticalPerSeveredummy = 0.5 * (criticalPerSevereMin[i] + criticalPerSevereMax[i]); + timeInfectedSevereMindummy = (1 - criticalPerSeveredummy) * timeInfectedSeveretoRecoveredMin[i] + + criticalPerSeveredummy * timeInfectedSeveretoInfectedCriticalMin; + timeInfectedSevereMaxdummy = (1 - criticalPerSeveredummy) * timeInfectedSeveretoRecoveredMax[i] + + criticalPerSeveredummy * timeInfectedSeveretoInfectedCriticalMax; + + timeInfectedSevere += + (age_group_sizes[i] / total) * 0.5 * (timeInfectedSevereMindummy + timeInfectedSevereMaxdummy); + criticalPerSevere += (age_group_sizes[i] / total) * criticalPerSeveredummy; + } + + if (printResult) { + std::cout << "timeInfectedSevere: " << timeInfectedSevere << std::endl; + std::cout << "criticalPerSevere: " << criticalPerSevere << std::endl; + } + + // U + const double timeInfectedCriticaltoRecoveredMin[] = {5, 5, 5, 14, 14, 10}; + const double timeInfectedCriticaltoRecoveredMax[] = {9, 9, 9, 21, 21, 15}; + const double timeInfectedCriticaltoDeadMin[] = {4, 4, 4, 15, 15, 10}; + const double timeInfectedCriticaltoDeadMax[] = {8, 8, 8, 18, 18, 12}; + const double deathsPerCriticalMin[] = {0.00, 0.00, 0.10, 0.10, 0.30, 0.5}; + const double deathsPerCriticalMax[] = {0.10, 0.10, 0.18, 0.18, 0.50, 0.7}; + + double timeInfectedCriticalMindummy; + double timeInfectedCriticalMaxdummy; + double deathsPerCriticaldummy; + double timeInfectedCritical = 0; + double deathsPerCritical = 0; + + for (int i = 0; i < numagegroups; i++) { + deathsPerCriticaldummy = 0.5 * (deathsPerCriticalMin[i] + deathsPerCriticalMax[i]); + timeInfectedCriticalMindummy = (1 - deathsPerCriticaldummy) * timeInfectedCriticaltoRecoveredMin[i] + + deathsPerCriticaldummy * timeInfectedCriticaltoDeadMin[i]; + timeInfectedCriticalMaxdummy = (1 - deathsPerCriticaldummy) * timeInfectedCriticaltoRecoveredMax[i] + + deathsPerCriticaldummy * timeInfectedCriticaltoDeadMax[i]; + + timeInfectedCritical += + (age_group_sizes[i] / total) * 0.5 * (timeInfectedCriticalMindummy + timeInfectedCriticalMaxdummy); + deathsPerCritical += (age_group_sizes[i] / total) * deathsPerCriticaldummy; + } + + if (printResult) { + std::cout << "timeInfectedCritical: " << timeInfectedCritical << std::endl; + std::cout << "deathsPerCritical: " << deathsPerCritical << std::endl; + } + + if (printResult) { + std::cout << "\nFor IDE model: " << std::endl; + std::cout << "timeInfectedNoSymptomstoInfectedSymptoms: " << timeInfectedNoSymptomstoInfectedSymptoms + << std::endl; + std::cout << "timeInfectedNoSymptomstoRecovered: " << timeInfectedNoSymptomstoRecovered << std::endl; + std::cout << "timeInfectedSymptomstoInfectedSevere: " << timeInfectedSymptomstoInfectedSevere << std::endl; + std::cout << "timeInfectedSymptomstoRecovered: " << timeInfectedSymptomstoRecovered << std::endl; + } +} \ No newline at end of file diff --git a/cpp/examples/ide_secir.cpp b/cpp/examples/ide_secir.cpp index 9008d7ddbf..b263a7108c 100644 --- a/cpp/examples/ide_secir.cpp +++ b/cpp/examples/ide_secir.cpp @@ -32,10 +32,10 @@ int main() { using Vec = mio::TimeSeries::Vector; - ScalarType tmax = 10; - ScalarType N = 10000; - ScalarType Dead_before = 12; - ScalarType dt = 1; + ScalarType tmax = 10; + ScalarType N = 10000; + ScalarType deaths = 13.10462213; + ScalarType dt = 1; int num_transitions = (int)mio::isecir::InfectionTransition::Count; @@ -63,7 +63,7 @@ int main() } // Initialize model. - mio::isecir::Model model(std::move(init), N, Dead_before); + mio::isecir::Model model(std::move(init), N, deaths); // model.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Susceptible] = 1000; // model.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Recovered] = 0; @@ -100,4 +100,4 @@ int main() sim.print_transitions(); sim.print_compartments(); -} +} \ No newline at end of file diff --git a/cpp/examples/lct_secir.cpp b/cpp/examples/lct_secir.cpp new file mode 100644 index 0000000000..24064bf28b --- /dev/null +++ b/cpp/examples/lct_secir.cpp @@ -0,0 +1,91 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "lct_secir/model.h" +#include "lct_secir/infection_state.h" +#include "lct_secir/simulation.h" +#include "memilio/config.h" +#include "memilio/utils/time_series.h" +#include "memilio/epidemiology/uncertain_matrix.h" +#include "memilio/math/eigen.h" +#include + +int main() +{ + /** Simple example to demonstrate how to simulate using an LCT SECIR model. + Parameters, initial values and subcompartments are not realistic. */ + + // Set vector that specifies the number of subcompartments. + std::vector num_subcompartments((int)mio::lsecir::InfectionStateBase::Count, 1); + num_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = 2; + num_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = 3; + num_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 5; + mio::lsecir::InfectionState infectionState(num_subcompartments); + + ScalarType tmax = 20; + + // Define initial distribution of the population in the subcompartments. + Eigen::VectorXd init(infectionState.get_count()); + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::Susceptible)] = 750; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::Exposed)] = 30; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::Exposed) + 1] = 20; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms)] = 20; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms) + 1] = 10; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms) + 2] = 10; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSymptoms)] = 50; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSevere)] = 50; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical)] = 10; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 1] = 10; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 2] = 5; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 3] = 3; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 4] = 2; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::Recovered)] = 20; + init[infectionState.get_firstindex(mio::lsecir::InfectionStateBase::Dead)] = 10; + + // Initialize model. + mio::lsecir::Model model(std::move(init), infectionState); + + // Set Parameters. + model.parameters.get() = 2 * 4.2 - 5.2; + model.parameters.get() = 2 * (5.2 - 4.2); + model.parameters.get() = 5.8; + model.parameters.get() = 9.5; + // Also possible to change values with setter. + model.parameters.set(7.1); + + model.parameters.get() = 0.05; + + mio::ContactMatrixGroup& contact_matrix = model.parameters.get(); + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10)); + contact_matrix[0].add_damping(0.7, mio::SimulationTime(5.)); + + model.parameters.get() = 0.7; + model.parameters.get() = 0.25; + model.parameters.get() = 0.09; + model.parameters.get() = 0.2; + model.parameters.get() = 0.25; + model.parameters.set(0.3); + + // Perform a simulation. + mio::TimeSeries result = mio::lsecir::simulate(0, tmax, 0.5, model); + // Calculate the distribution in infectionState without subcompartments of the result and print it. + mio::TimeSeries populations = model.calculate_populations(result); + mio::lsecir::print_TimeSeries(populations, model.get_heading_CompartmentsBase()); +} \ No newline at end of file diff --git a/cpp/examples/lct_secir_fictional_scenario.cpp b/cpp/examples/lct_secir_fictional_scenario.cpp new file mode 100644 index 0000000000..83aa0b648e --- /dev/null +++ b/cpp/examples/lct_secir_fictional_scenario.cpp @@ -0,0 +1,391 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "lct_secir/model.h" +#include "lct_secir/infection_state.h" +#include "lct_secir/initialization.h" +#include "lct_secir/parameters.h" +#include "lct_secir/simulation.h" + +#include "ide_secir/model.h" +#include "ide_secir/infection_state.h" +#include "ide_secir/parameters.h" +#include "ide_secir/simulation.h" + +#include "memilio/config.h" +#include "memilio/epidemiology/state_age_function.h" +#include "memilio/io/result_io.h" +#include "memilio/io/io.h" +#include "memilio/utils/time_series.h" +#include "memilio/math/eigen.h" +#include "boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp" +#include +#include + +/** +* @brief Perform a fictive simulation with realistic parameters and contacts, such that the reproduction number +* is approximately 1 at the beginning and rising or dropping at simulationtime 2. +* +* This scenario should enable a comparison of the qualitative behavior of different models. +* +* @param[in] R0 Define R0 from simulationtime 2 on. Please use a number > 0. +* @param[in] num_subcompartments Number of subcompartments for each compartment where subcompartments make sense. +* Number is also used for some distributions of the IDE model. +* Set num_subcompartments = 0 to use subcompartments with an expected sojourn time of approximately 1. +* @param[in] simulate_lct Defines if a simulation with an LCT model is done. +* @param[in] simulate_ide Defines if a simulation with an IDE model is done. +* @param[in] save_dir Specifies the directory where the results should be stored. Provide an empty string if results should not be saved. +* @param[in] tmax End time of the simulation. +* @param[in] print_result Specifies if the results should be printed. +* @returns Any io errors that happen during saving the results. +*/ +mio::IOResult simulate(ScalarType R0, int num_subcompartments = 3, bool simulate_lct = true, + bool simulate_ide = true, std::string save_dir = "", ScalarType tmax = 10, + bool print_result = false) +{ + + ScalarType dt_flows = 0.1; + ScalarType total_population = 83155031.0; + + // Define parameters used for simulation and initialization. + // Parameters are calculated via examples/compute_parameters.cpp. + mio::lsecir::Parameters parameters_lct; + parameters_lct.get() = 3.335; + parameters_lct.get() = 3.31331; + parameters_lct.get() = 6.94547; + parameters_lct.get() = 11.634346; + parameters_lct.get() = 17.476959; + parameters_lct.get() = 0.0733271; + + mio::ContactMatrixGroup contact_matrix = mio::ContactMatrixGroup(1, 1); + if (R0 <= 1.) { + // Perform simulation with dropping R0. + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 2.7463)); + contact_matrix[0].add_damping(0., mio::SimulationTime(1.9)); + contact_matrix[0].add_damping(R0, mio::SimulationTime(2.)); + } + else { + // Perform simulation with rising R0. + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, R0 * 2.7463)); + contact_matrix[0].add_damping(1 - 1. / R0, mio::SimulationTime(-1.)); + contact_matrix[0].add_damping(1 - 1. / R0, mio::SimulationTime(1.9)); + contact_matrix[0].add_damping(0., mio::SimulationTime(2.)); + } + + parameters_lct.get() = mio::UncertainContactMatrix(contact_matrix); + + parameters_lct.get() = 1; + parameters_lct.get() = 0.3; + parameters_lct.get() = 0.; + parameters_lct.get() = 0.206901; + parameters_lct.get() = 0.0786429; + parameters_lct.get() = 0.173176; + parameters_lct.get() = 0.217177; + + // The initialization vector for the LCT model is calculated by defining transitions. + // Create TimeSeries with num_transitions elements. + int num_transitions = (int)mio::lsecir::InfectionTransition::Count; + mio::TimeSeries init(num_transitions); + + // Add time points for initialization of transitions. + /* For this example, the intention is to create nearly constant values for SusceptiblesToExposed flow + at the beginning of the simulation. Therefore we initalize the flows accordingly constant for + SusceptiblesToExposed and derive matching values for the other flows.*/ + // 7-Tage-Inzidenz at 15.10.2020 was 34.1, see https://www.rki.de/DE/Content/InfAZ/N/Neuartiges_Coronavirus/Situationsberichte/Okt_2020/2020-10-15-de.pdf?__blob=publicationFile. + ScalarType SusceptibleToExposed_const = (34.1 / 7) * total_population / 100000; + ScalarType total_confirmed_cases = 341223; + ScalarType deaths = 9710; + Eigen::VectorXd init_transitions(num_transitions); + init_transitions[(int)mio::isecir::InfectionTransition::SusceptibleToExposed] = SusceptibleToExposed_const; + init_transitions[(int)mio::isecir::InfectionTransition::ExposedToInfectedNoSymptoms] = SusceptibleToExposed_const; + init_transitions[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] = + SusceptibleToExposed_const * (1 - parameters_lct.get()); + init_transitions[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToRecovered] = + SusceptibleToExposed_const * parameters_lct.get(); + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSymptomsToInfectedSevere] = + init_transitions[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] * + parameters_lct.get(); + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSymptomsToRecovered] = + init_transitions[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] * + (1 - parameters_lct.get()); + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSevereToInfectedCritical] = + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSymptomsToInfectedSevere] * + parameters_lct.get(); + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSevereToRecovered] = + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSymptomsToInfectedSevere] * + (1 - parameters_lct.get()); + init_transitions[(int)mio::isecir::InfectionTransition::InfectedCriticalToDead] = + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSevereToInfectedCritical] * + parameters_lct.get(); + init_transitions[(int)mio::isecir::InfectionTransition::InfectedCriticalToRecovered] = + init_transitions[(int)mio::isecir::InfectionTransition::InfectedSevereToInfectedCritical] * + (1 - parameters_lct.get()); + init_transitions = init_transitions * dt_flows; + + // Add initial time point to time series. + init.add_time_point(-350, init_transitions); + // Add further time points until time 0 with constant values. + while (init.get_last_time() < -1e-10) { + init.add_time_point(init.get_last_time() + dt_flows, init_transitions); + } + + // Set vector that specifies the number of subcompartments. + std::vector vec_subcompartments((int)mio::lsecir::InfectionStateBase::Count, 1); + if (!(num_subcompartments == 0)) { + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = num_subcompartments; + } + else { + // For approximately soujourn time of one day in each Subcompartment + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = + (int)round(parameters_lct.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = + (int)round(parameters_lct.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = + (int)round(parameters_lct.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = + (int)round(parameters_lct.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = + (int)round(parameters_lct.get()); + } + mio::lsecir::InfectionState infectionState(vec_subcompartments); + + if (simulate_lct) { + // Get initialization vector for LCT model with num_subcompartments subcompartments. + mio::TimeSeries init_copy(init); + mio::lsecir::Initializer initializer(std::move(init_copy), infectionState, std::move(parameters_lct)); + Eigen::VectorXd init_compartments = + initializer.compute_initializationvector(total_population, deaths, total_confirmed_cases); + + // Initialize model and perform simulation. + mio::lsecir::Model model(std::move(init_compartments), infectionState, std::move(parameters_lct)); + mio::TimeSeries result = mio::lsecir::simulate( + 0, tmax, 0.5, model, + std::make_shared>( + 1e-10, 1e-5, 0, 0.1)); + // Calculate result without division in subcompartments. + mio::TimeSeries populations = model.calculate_populations(result); + + if (print_result) { + std::cout << "Result LCT model:" << std::endl; + mio::lsecir::print_TimeSeries(populations, model.get_heading_CompartmentsBase()); + } + if (!save_dir.empty()) { + std::string R0string = std::to_string(R0); + std::string filename = save_dir + "fictional_lct_" + R0string.substr(0, R0string.find(".") + 2) + "_" + + std::to_string(num_subcompartments); + if (tmax > 50) { + filename = filename + "_long"; + } + filename = filename + ".h5"; + mio::IOResult save_result_status = mio::save_result({populations}, {0}, 1, filename); + } + } + + //--------- IDE model ------------ + if (simulate_ide) { + // Initialize model. + mio::isecir::Model model_ide(std::move(init), total_population, deaths, total_confirmed_cases); + + // Set working parameters. + // Set TransitionDistributions. + mio::ConstantFunction initialfunc(0); + mio::StateAgeFunctionWrapper delaydistributioninit(initialfunc); + std::vector vec_delaydistrib(num_transitions, delaydistributioninit); + + mio::ExponentialDecay expdecayInfectedSevereToInfectedCritical(1. / 9.36); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedSevereToInfectedCritical] + .set_state_age_function(expdecayInfectedSevereToInfectedCritical); + + mio::LognormSurvivalFunction lognInfectedSevereToRecovered(0.76, -0.45, 9.41); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedSevereToRecovered].set_state_age_function( + lognInfectedSevereToRecovered); + + mio::ExponentialDecay expdecayInfectedCriticalToDeath(1. / 14.88, 1); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedCriticalToDead].set_state_age_function( + expdecayInfectedCriticalToDeath); + expdecayInfectedCriticalToDeath.set_parameter(1 / 16.92); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedCriticalToRecovered].set_state_age_function( + expdecayInfectedCriticalToDeath); + + mio::GammaSurvivalFunction erlangExposedToInfectedNoSymptoms( + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed], 0, + 3.335 / vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed]); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::ExposedToInfectedNoSymptoms].set_state_age_function( + erlangExposedToInfectedNoSymptoms); + + mio::GammaSurvivalFunction erlangInfectedNoSymptomsToInfectedSymptoms( + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms], 0, + 1.865 / vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms]); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] + .set_state_age_function(erlangInfectedNoSymptomsToInfectedSymptoms); + erlangInfectedNoSymptomsToInfectedSymptoms.set_scale( + 8.865 / vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms]); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToRecovered].set_state_age_function( + erlangInfectedNoSymptomsToInfectedSymptoms); + + mio::GammaSurvivalFunction erlangInfectedSymptomsToInfectedSevere( + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms], 0, + 6.30662 / vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms]); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedSymptomsToInfectedSevere] + .set_state_age_function(erlangInfectedSymptomsToInfectedSevere); + erlangInfectedSymptomsToInfectedSevere.set_scale( + 7. / vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms]); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedSymptomsToRecovered].set_state_age_function( + erlangInfectedSymptomsToInfectedSevere); + + model_ide.parameters.set(vec_delaydistrib); + + // Set other parameters. + std::vector vec_prob((int)mio::isecir::InfectionTransition::Count, 0.5); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::SusceptibleToExposed)] = 1; + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::ExposedToInfectedNoSymptoms)] = 1; + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms)] = + 1 - parameters_lct.get(); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedNoSymptomsToRecovered)] = + parameters_lct.get(); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedSymptomsToInfectedSevere)] = + parameters_lct.get(); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedSymptomsToRecovered)] = + 1 - parameters_lct.get(); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedSevereToInfectedCritical)] = + parameters_lct.get(); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedSevereToRecovered)] = + 1 - parameters_lct.get(); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedCriticalToDead)] = + parameters_lct.get(); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::InfectedCriticalToRecovered)] = + 1 - parameters_lct.get(); + + model_ide.parameters.set(vec_prob); + + model_ide.parameters.get() = mio::UncertainContactMatrix(contact_matrix); + + mio::ConstantFunction constfunc(parameters_lct.get()); + mio::StateAgeFunctionWrapper StateAgeFunctionWrapperide(constfunc); + model_ide.parameters.set(StateAgeFunctionWrapperide); + StateAgeFunctionWrapperide.set_parameter(parameters_lct.get()); + model_ide.parameters.set(StateAgeFunctionWrapperide); + StateAgeFunctionWrapperide.set_parameter(parameters_lct.get()); + model_ide.parameters.set(StateAgeFunctionWrapperide); + + model_ide.set_tol_for_support_max(1e-6); + model_ide.check_constraints(dt_flows); + + // Simulate. + mio::isecir::Simulation sim(model_ide, 0, dt_flows); + sim.advance(tmax); + + if (print_result) { + std::cout << "\nResult IDE model:" << std::endl; + sim.print_compartments(); + } + if (!save_dir.empty()) { + std::string R0string = std::to_string(R0); + std::string filename_ide = save_dir + "fictional_ide_" + R0string.substr(0, R0string.find(".") + 2) + "_" + + std::to_string(num_subcompartments); + if (tmax > 50) { + filename_ide = filename_ide + "_long"; + } + filename_ide = filename_ide + ".h5"; + mio::IOResult save_result_status = mio::save_result({sim.get_result()}, {0}, 1, filename_ide); + } + } + return mio::success(); +} + +int main() +{ + // First case: drop R0 to 0.5. + // Paths are valid if file is executed eg in memilio/build/bin + std::string save_dir = "../../data/simulation_lct/dropR0/"; + auto result = simulate(0.5, 3, true, true, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + result = simulate(0.5, 10, true, true, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + for (int i : {0, 1, 20}) { + result = simulate(0.5, i, true, false, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + + // Second case: Rise R0 to 2. + save_dir = "../../data/simulation_lct/riseR0short/"; + for (int i : {3, 10}) { + result = simulate(2, i, true, true, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + + for (int i : {0, 1, 20}) { + result = simulate(2, i, true, false, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + + // Third case: Rise R0 to 2 or 4 long term. + save_dir = "../../data/simulation_lct/riseR0long/"; + + for (int i : {3, 10}) { + result = simulate(2, i, true, true, save_dir, 150); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + for (int i : {0, 1, 20}) { + result = simulate(2, i, true, false, save_dir, 150); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + + for (int i : {3, 10}) { + result = simulate(4, i, true, true, save_dir, 75); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + for (int i : {0, 1, 20}) { + result = simulate(4, i, true, false, save_dir, 75); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + return 0; +} \ No newline at end of file diff --git a/cpp/examples/lct_secir_initializations.cpp b/cpp/examples/lct_secir_initializations.cpp new file mode 100644 index 0000000000..05e504ded1 --- /dev/null +++ b/cpp/examples/lct_secir_initializations.cpp @@ -0,0 +1,260 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "lct_secir/model.h" +#include "lct_secir/infection_state.h" +#include "lct_secir/initialization.h" +#include "lct_secir/parameters.h" +#include "lct_secir/simulation.h" +#include "memilio/config.h" +#include "memilio/io/result_io.h" +#include "memilio/io/io.h" +#include "memilio/utils/time_series.h" +#include "memilio/math/eigen.h" +#include "boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp" +#include + +/** +* @brief Perform simulation for an LCT model with realistic parameters with different initialization methods to compare those. +* +* @param[in] num_subcompartments Number of subcompartments for each compartment where subcompartments make sense. +* @param[in] save_dir Specifies the directory where the results should be stored. Provide an empty string if results should not be saved. +* @param[in] tmax End time of the simulation. +* @param[in] print_result Specifies if the results should be printed. +* @returns Any io errors that happen during saving the results. +*/ +mio::IOResult simulate(int num_subcompartments = 3, std::string save_dir = "", ScalarType tmax = 20, + bool print_result = false) +{ + ScalarType dt_flows = 0.1; + ScalarType total_population = 83155031.0; + + // Define parameters used for simulation and initialization. + // Parameters are calculated via examples/compute_parameters.cpp. + mio::lsecir::Parameters parameters; + parameters.get() = 3.335; + parameters.get() = 3.31331; + parameters.get() = 6.94547; + parameters.get() = 11.634346; + parameters.get() = 17.476959; + parameters.get() = 0.0733271; + + mio::ContactMatrixGroup contact_matrix = mio::ContactMatrixGroup(1, 1); + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 2.7463)); + parameters.get() = mio::UncertainContactMatrix(contact_matrix); + + parameters.get() = 1; + parameters.get() = 0.3; + parameters.get() = 0; + parameters.get() = 0.206901; + parameters.get() = 0.0786429; + parameters.get() = 0.173176; + parameters.get() = 0.217177; + + // --- First initialization method: The initialization vector for the LCT model is calculated by defining transitions. + // Create TimeSeries with num_transitions elements. + int num_transitions = (int)mio::lsecir::InfectionTransition::Count; + mio::TimeSeries init(num_transitions); + + // Add time points for initialization of transitions. + /* For this example, the intention is to create nearly constant values for SusceptiblesToExposed flow + at the beginning of the simulation. Therefore we initalize the flows accordingly constant for + SusceptiblesToExposed and derive matching values for the other flows.*/ + // 7-Tage-Inzidenz at 15.10.2020 was 34.1, see https://www.rki.de/DE/Content/InfAZ/N/Neuartiges_Coronavirus/Situationsberichte/Okt_2020/2020-10-15-de.pdf?__blob=publicationFile. + ScalarType SusceptibleToExposed_const = (34.1 / 7) * total_population / 100000; + ScalarType total_confirmed_cases = 341223; + ScalarType deaths = 9710; + Eigen::VectorXd init_transitions(num_transitions); + init_transitions[(int)mio::lsecir::InfectionTransition::SusceptibleToExposed] = SusceptibleToExposed_const; + init_transitions[(int)mio::lsecir::InfectionTransition::ExposedToInfectedNoSymptoms] = SusceptibleToExposed_const; + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] = + SusceptibleToExposed_const * (1 - parameters.get()); + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedNoSymptomsToRecovered] = + SusceptibleToExposed_const * parameters.get(); + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSymptomsToInfectedSevere] = + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] * + parameters.get(); + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSymptomsToRecovered] = + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] * + (1 - parameters.get()); + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSevereToInfectedCritical] = + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSymptomsToInfectedSevere] * + parameters.get(); + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSevereToRecovered] = + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSymptomsToInfectedSevere] * + (1 - parameters.get()); + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedCriticalToDead] = + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSevereToInfectedCritical] * + parameters.get(); + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedCriticalToRecovered] = + init_transitions[(int)mio::lsecir::InfectionTransition::InfectedSevereToInfectedCritical] * + (1 - parameters.get()); + init_transitions = init_transitions * dt_flows; + + // Add initial time point to time series. + init.add_time_point(-200, init_transitions); + // Add further time points until time 0. + while (init.get_last_time() < 0) { + init.add_time_point(init.get_last_time() + dt_flows, init_transitions); + } + + // Set vector that specifies the number of subcompartments. + std::vector vec_subcompartments((int)mio::lsecir::InfectionStateBase::Count, 1); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = num_subcompartments; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = num_subcompartments; + mio::lsecir::InfectionState infectionState(vec_subcompartments); + + mio::TimeSeries init_copy(init); + + // Get initialization vector for LCT model with num_subcompartments subcompartments. + mio::lsecir::Initializer initializer(std::move(init_copy), infectionState, std::move(parameters)); + auto init_compartments = initializer.compute_initializationvector(total_population, deaths, total_confirmed_cases); + + // Initialize model and perform simulation. + mio::lsecir::Model model(std::move(init_compartments), infectionState, std::move(parameters)); + mio::TimeSeries result_transitions = mio::lsecir::simulate( + 0, tmax, 0.5, model, + std::make_shared>(1e-10, 1e-5, 0, + 0.1)); + // Calculate result without division in subcompartments. + mio::TimeSeries populations_transitions = model.calculate_populations(result_transitions); + + if (print_result) { + mio::lsecir::print_TimeSeries(populations_transitions, model.get_heading_CompartmentsBase()); + } + if (!save_dir.empty()) { + std::string filename = save_dir + "lct_init_transitions_" + std::to_string(num_subcompartments); + if (tmax > 50) { + filename = filename + "_long"; + } + filename = filename + ".h5"; + mio::IOResult save_result_status = mio::save_result({populations_transitions}, {0}, 1, filename); + } + + // --- Second initialization method: The initialization vector for the LCT model is calculated by taking expected sojourn times. + // Because of the constant initialization, the constant value of SusceptibleToExposed is scaled by expected sojourn time. + Eigen::VectorXd init_vec((int)mio::lsecir::InfectionStateBase::Count); + init_vec[(int)mio::lsecir::InfectionStateBase::Exposed] = + SusceptibleToExposed_const * parameters.get(); + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = + SusceptibleToExposed_const * parameters.get(); + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = + SusceptibleToExposed_const * (1 - parameters.get()) * + parameters.get(); + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = + SusceptibleToExposed_const * (1 - parameters.get()) * + parameters.get() * parameters.get(); + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = + SusceptibleToExposed_const * (1 - parameters.get()) * + parameters.get() * parameters.get() * + parameters.get(); + init_vec[(int)mio::lsecir::InfectionStateBase::Recovered] = + total_confirmed_cases - deaths - init_vec[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] - + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedSevere] - + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedCritical]; + init_vec[(int)mio::lsecir::InfectionStateBase::Dead] = deaths; + init_vec[(int)mio::lsecir::InfectionStateBase::Susceptible] = + total_population - init_vec[(int)mio::lsecir::InfectionStateBase::Exposed] - + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] - + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] - + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedSevere] - + init_vec[(int)mio::lsecir::InfectionStateBase::InfectedCritical] - + init_vec[(int)mio::lsecir::InfectionStateBase::Recovered] - + init_vec[(int)mio::lsecir::InfectionStateBase::Dead]; + + // Constant initialization leads to equally distributed values accross substates. + Eigen::VectorXd init_mean(infectionState.get_count()); + for (int i = 0; i < (int)mio::lsecir::InfectionStateBase::Count; i++) { + for (int j = infectionState.get_firstindex(i); + j < infectionState.get_firstindex(i) + infectionState.get_number(i); j++) { + init_mean[j] = init_vec[i] / infectionState.get_number(i); + } + } + + // Initialize model and perform simulation. + mio::lsecir::Model model2(std::move(init_mean), infectionState, std::move(parameters)); + mio::TimeSeries result2 = mio::lsecir::simulate( + 0, tmax, 0.5, model2, + std::make_shared>(1e-10, 1e-5, 0, + 0.1)); + // Calculate result without division in subcompartments. + mio::TimeSeries populations2 = model2.calculate_populations(result2); + + if (print_result) { + mio::lsecir::print_TimeSeries(populations2, model2.get_heading_CompartmentsBase()); + } + if (!save_dir.empty()) { + std::string filename_mean = save_dir + "lct_init_mean_" + std::to_string(num_subcompartments); + if (tmax > 50) { + filename_mean = filename_mean + "_long"; + } + filename_mean = filename_mean + ".h5"; + mio::IOResult save_result_status2 = mio::save_result({populations2}, {0}, 1, filename_mean); + } + + // --- Third initialization method: Initial values are distributed only in the first substates. + Eigen::VectorXd init_first = Eigen::VectorXd::Zero(infectionState.get_count()); + for (int i = 0; i < (int)mio::lsecir::InfectionStateBase::Count; i++) { + init_first[infectionState.get_firstindex(i)] = init_vec[i]; + } + + // Initialize model and perform simulation. + mio::lsecir::Model model3(std::move(init_first), infectionState, std::move(parameters)); + mio::TimeSeries result3 = mio::lsecir::simulate( + 0, tmax, 0.5, model3, + std::make_shared>(1e-10, 1e-5, 0, + 0.1)); + // Calculate result without division in subcompartments. + mio::TimeSeries populations3 = model3.calculate_populations(result3); + + if (print_result) { + mio::lsecir::print_TimeSeries(populations3, model3.get_heading_CompartmentsBase()); + } + if (!save_dir.empty()) { + std::string filename_first = save_dir + "lct_init_first_" + std::to_string(num_subcompartments); + if (tmax > 50) { + filename_first = filename_first + "_long"; + } + filename_first = filename_first + ".h5"; + mio::IOResult save_result_status3 = mio::save_result({populations3}, {0}, 1, filename_first); + } + return mio::success(); +} + +int main() +{ // Path is valid if file is executed eg in memilio/build/bin + std::string save_dir = "../../data/simulation_lct/init/"; + auto result = simulate(20, save_dir, 100); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + for (int i : {3, 10, 20}) { + result = simulate(i, save_dir, 20); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + } + return 0; +} \ No newline at end of file diff --git a/cpp/examples/lct_secir_real_scenario.cpp b/cpp/examples/lct_secir_real_scenario.cpp new file mode 100644 index 0000000000..6c2c6e9eb3 --- /dev/null +++ b/cpp/examples/lct_secir_real_scenario.cpp @@ -0,0 +1,591 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "boost/outcome/try.hpp" +#include "lct_secir/model.h" +#include "lct_secir/infection_state.h" +#include "lct_secir/parameters.h" +#include "lct_secir/simulation.h" +#include "lct_secir/parameters_io.h" + +#include "memilio/config.h" +#include "memilio/io/result_io.h" +#include "memilio/io/io.h" +#include "memilio/utils/time_series.h" +#include "memilio/math/eigen.h" +#include "boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp" +#include "boost/filesystem.hpp" +#include + +/** + * @brief Indices of contact matrix corresponding to locations where contacts occur. + */ +enum class ContactLocation +{ + Home = 0, + School, + Work, + Other, + Count +}; + +/** + * @brief Different types of NPI, used as DampingType. + */ +enum class Intervention +{ + Home, + SchoolClosure, + HomeOffice, + GatheringBanFacilitiesClosure, + PhysicalDistanceAndMasks, + Count +}; + +/** + * @brief Different level of NPI, used as DampingLevel. + */ +enum class InterventionLevel +{ + Main, + PhysicalDistanceAndMasks, + Count +}; + +// Map the ContactLocation%s to file names. +static const std::map contact_locations = {{ContactLocation::Home, "home"}, + {ContactLocation::School, "school_pf_eig"}, + {ContactLocation::Work, "work"}, + {ContactLocation::Other, "other"}}; + +/** + * @brief Add NPIs to a given contact matrix from 01/06/2020 on. + * + * NPIs have been adjusted so that they approximate the trend of the RKI data for a period of 45 days from 01/06/2020 on. + * + * @param[in] contact_matrices The contact matrices where the NPIs shpuld be added to. + * @param[in] start_date Start date of the simulation used for setting the NPIs. + */ +void set_npi_june(mio::ContactMatrixGroup& contact_matrices, mio::Date start_date) +{ + // ---------------------01/06/2020-------------------------------- + /* NPIs from Paper "Assessment of effective mitigation ..." (doi: 10.1016/j.mbs.2021.108648) with slightly higher value for + GatheringBanFacilitiesClosure. */ + auto offset_npi = mio::SimulationTime(mio::get_offset_in_days(mio::Date(2020, 6, 1), start_date)); + // Contact reduction at home. + ScalarType v = 0.1; + contact_matrices[size_t(ContactLocation::Home)].add_damping(Eigen::MatrixXd::Constant(1, 1, v), + mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::Home)), offset_npi); + // Home-Office + people stopped working. + v = (0.25 + 0.025); + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::HomeOffice)), offset_npi); + // GatheringBanFacilitiesClosure affects ContactLocation Other. + v = 0.2; + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::GatheringBanFacilitiesClosure)), offset_npi); + // PhysicalDistanceAndMasks in all locations. + v = 0.1; + for (auto&& contact_location : contact_locations) { + contact_matrices[size_t(contact_location.first)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + } + // Remote schooling. + v = 0.5; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::SchoolClosure)), offset_npi); + + // ---------------------02/06/2020-------------------------------- + /* Values of new infections per day of the RKI data are rising from 02/06/2020 on. + This could be partly explained by the relatively low number of cases and psychological effects. + Most NPIs need to be lifted in order to reproduce the trend of the number of cases.*/ + offset_npi = mio::SimulationTime(mio::get_offset_in_days(mio::Date(2020, 6, 2), start_date)); + // Lifted contact reduction at home. + v = 0.; + contact_matrices[size_t(ContactLocation::Home)].add_damping(Eigen::MatrixXd::Constant(1, 1, v), + mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::Home)), offset_npi); + // Lifted home-Office, people continued to stop working. + v = (0. + 0.025); + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::HomeOffice)), offset_npi); + // Lifted gatheringBanFacilitiesClosure affects ContactLocation Other. + v = 0.; + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::GatheringBanFacilitiesClosure)), offset_npi); + // Lifted physicalDistanceAndMasks in all locations. + v = 0.; + for (auto&& contact_location : contact_locations) { + contact_matrices[size_t(contact_location.first)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + } + // Lifted part of remote schooling. + v = 0.25; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::SchoolClosure)), offset_npi); + + // ---------------------14/06/2020-------------------------------- + // Number of cases are rising again so that further NPIs had to be implemented. People became more cautious again. + offset_npi = mio::SimulationTime(mio::get_offset_in_days(mio::Date(2020, 6, 14), start_date)); + // Contact reduction at home. + v = 0.1; + contact_matrices[size_t(ContactLocation::Home)].add_damping(Eigen::MatrixXd::Constant(1, 1, v), + mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::Home)), offset_npi); + // Home-Office + people stopped working. + v = (0.25 + 0.025); + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::HomeOffice)), offset_npi); + // GatheringBanFacilitiesClosure affects ContactLocation Other. + v = 0.25; + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::GatheringBanFacilitiesClosure)), offset_npi); + // PhysicalDistanceAndMasks in all locations. + v = 0.25; + for (auto&& contact_location : contact_locations) { + contact_matrices[size_t(contact_location.first)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + } + // Remote schooling. + v = 0.35; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::SchoolClosure)), offset_npi); + + // ---------------------03/07/2020-------------------------------- + // Number of cases are at a low level. People begin to meet again but are more cautious than on 02/06/2020. + offset_npi = mio::SimulationTime(mio::get_offset_in_days(mio::Date(2020, 7, 3), start_date)); + // Contact reduction at home. + v = 0.; + contact_matrices[size_t(ContactLocation::Home)].add_damping(Eigen::MatrixXd::Constant(1, 1, v), + mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::Home)), offset_npi); + // Home-Office + people stopped working. + v = (0. + 0.025); + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::HomeOffice)), offset_npi); + // GatheringBanFacilitiesClosure affects ContactLocation Other. + v = 0.1; + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::GatheringBanFacilitiesClosure)), offset_npi); + // PhysicalDistanceAndMasks in ContactLocation%s Home. + v = 0.; + contact_matrices[size_t(ContactLocation::Home)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + // PhysicalDistanceAndMasks in ContactLocation%s School. + v = 0.1; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + // PhysicalDistanceAndMasks in ContactLocation%s Work and Other. + v = 0.1; + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + v = 0.; + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + // Remote schooling. + v = 0.2; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::SchoolClosure)), offset_npi); +} + +/** + * @brief Add NPIs to a given contact matrix from 01/10/2020 on. + * + * NPIs from the Paper "Assessment of effective mitigation ..." (doi: 10.1016/j.mbs.2021.108648) are used with slight + * modifications for a period of 45 days from 01/10/2020 on. + * + * @param[in] contact_matrices The contact matrices where the NPIs shpuld be added to. + * @param[in] start_date Start date of the simulation used for setting the NPIs. + * @param[in] lockdown_hard Proportion of counties for which a hard lockdown is implemented. + */ +void set_npi_october(mio::ContactMatrixGroup& contact_matrices, mio::Date start_date, ScalarType lockdown_hard) +{ + // ---------------------01/10/2020-------------------------------- + // NPIs from Paper for october. + auto offset_npi = mio::SimulationTime(mio::get_offset_in_days(mio::Date(2020, 10, 1), start_date)); + // For the beginning of the time period, we assume only half of the defined proportion of counties is in a hard lockdown. + lockdown_hard = lockdown_hard / 2; + // Contact reduction at home. + ScalarType v = 0.3 * (1 - lockdown_hard) + lockdown_hard * 0.5; + contact_matrices[size_t(ContactLocation::Home)].add_damping(Eigen::MatrixXd::Constant(1, 1, v), + mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::Home)), offset_npi); + // Home-Office + people stopped working. + v = (0.25 + 0.025) * (1 - lockdown_hard) + lockdown_hard * (0.25 + 0.15); + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::HomeOffice)), offset_npi); + // GatheringBanFacilitiesClosure affects ContactLocation Other. + v = 0.1 * (1 - lockdown_hard) + lockdown_hard * 0.7; + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::GatheringBanFacilitiesClosure)), offset_npi); + // PhysicalDistanceAndMasks in all locations. + v = 0.3 * (1 - lockdown_hard) + lockdown_hard * 0.7; + for (auto&& contact_location : contact_locations) { + contact_matrices[size_t(contact_location.first)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + } + // Remote schooling. + v = lockdown_hard * 0.25; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::SchoolClosure)), offset_npi); + + // ---------------------24/10/2020-------------------------------- + /* We assume that the stricter NPIs of november defined in the paper are beginning about a week earlier, + which can be seen from the RKI data. + Moreover the lockdown value of PhysicalDistanceAndMasks in the location school is assumed to apply for all counties.*/ + offset_npi = mio::SimulationTime(mio::get_offset_in_days(mio::Date(2020, 10, 24), start_date)); + // For the second half of the simulation, the proportion of counties in hard lockdown is increased to compensate for the lower proportion before. + lockdown_hard = lockdown_hard * 3; + // Contact reduction at home. + v = 0.5; + contact_matrices[size_t(ContactLocation::Home)].add_damping(Eigen::MatrixXd::Constant(1, 1, v), + mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::Home)), offset_npi); + // Home-Office + people stopped working. + v = (0.25 + 0.05) * (1 - lockdown_hard) + lockdown_hard * (0.25 + 0.15); + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::HomeOffice)), offset_npi); + // GatheringBanFacilitiesClosure affects ContactLocation Other. + v = 0.7; + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::GatheringBanFacilitiesClosure)), offset_npi); + // PhysicalDistanceAndMasks in ContactLocation%s Home. + v = 0.3 * (1 - lockdown_hard) + lockdown_hard * 0.7; + contact_matrices[size_t(ContactLocation::Home)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + // PhysicalDistanceAndMasks in ContactLocation%s School. + v = 0.7; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + // PhysicalDistanceAndMasks in ContactLocation%s Work and Other. + v = 0.5 * (1 - lockdown_hard) + lockdown_hard * 0.7; + contact_matrices[size_t(ContactLocation::Work)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + contact_matrices[size_t(ContactLocation::Other)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::PhysicalDistanceAndMasks)), + mio::DampingType(int(Intervention::PhysicalDistanceAndMasks)), offset_npi); + // Remote schooling. + v = lockdown_hard * 0.25; + contact_matrices[size_t(ContactLocation::School)].add_damping( + Eigen::MatrixXd::Constant(1, 1, v), mio::DampingLevel(int(InterventionLevel::Main)), + mio::DampingType(int(Intervention::SchoolClosure)), offset_npi); +} + +/** + * @brief Set the contact pattern of parameters for a Model without division in age groups. + * + * The contacts are calculated using contact matrices from files in the data directory for different locations. + * Also set Nonpharmaceutical Interventions influencing the ContactPatterns used for simulation in the timeframe from start_date to end_date. + * + * @param[in] data_dir Directory to files with minimum and baseline contact matrices. + * @param[in] parameters Object that the contact pattern will be added to. + * @param[in] simulation_parameters Map with parameters necessary for the calculation of contacts an NPIs which can be different for diffferent start dates. + * Function uses the values to define start and end date, lockdown_hard and scale_contacts. + * @returns Any io errors that happen during reading of the input files. + */ +mio::IOResult set_contact_matrices(const fs::path& data_dir, mio::lsecir::Parameters& parameters, + std::map simulation_parameters) +{ + // Files in data_dir are containing contact matrices with 6 agegroups. We use this to compute a contact pattern without division of age groups. + // Age group sizes are calculated using table number 12411-04-02-4-B from www.regionalstatistik.de for the date 31.12.2020. + const ScalarType age_group_sizes[] = {3969138.0, 7508662, 18921292, 28666166, 18153339, 5936434}; + const ScalarType total = 83155031.0; + const int numagegroups = 6; + mio::Date start_date = + mio::Date(2020, (int)simulation_parameters["start_month"], (int)simulation_parameters["start_day"]); + mio::Date end_date = mio::offset_date_by_days(start_date, 45); + + auto contact_matrices = mio::ContactMatrixGroup(contact_locations.size(), 1); + // Load and set minimum and baseline contacts for each contact location. + for (auto&& contact_location : contact_locations) { + BOOST_OUTCOME_TRY(baseline, + mio::read_mobility_plain( + (data_dir / "contacts" / ("baseline_" + contact_location.second + ".txt")).string())); + BOOST_OUTCOME_TRY(minimum, + mio::read_mobility_plain( + (data_dir / "contacts" / ("minimum_" + contact_location.second + ".txt")).string())); + ScalarType base = 0; + ScalarType min = 0; + for (int i = 0; i < numagegroups; i++) { + for (int j = 0; j < numagegroups; j++) { + // Calculate a weighted average according to the age group sizes of the total contacts. + base += age_group_sizes[i] / total * baseline(i, j); + min += age_group_sizes[i] / total * minimum(i, j); + } + } + contact_matrices[size_t(contact_location.first)].get_baseline() = + simulation_parameters["scale_contacts"] * Eigen::MatrixXd::Constant(1, 1, base); + contact_matrices[size_t(contact_location.first)].get_minimum() = + simulation_parameters["scale_contacts"] * Eigen::MatrixXd::Constant(1, 1, min); + } + + // ----- Add NPIs to the contact matrices. ----- + // Set of NPIs for June. + if (mio::Date(2020, 6, 1) < end_date) { + set_npi_june(contact_matrices, start_date); + } + + // Set of NPIs for October. + auto start_npi_october = mio::Date(2020, 10, 1); + if (start_npi_october < end_date) { + set_npi_october(contact_matrices, start_date, simulation_parameters["lockdown_hard"]); + } + + // Set ContactPatterns in parameters. + parameters.get() = mio::UncertainContactMatrix(contact_matrices); + + return mio::success(); +} + +/** + * @brief Performs a simulation of a real scenario with an LCT and an ODE model. + * + * @param[in] path Path of the RKI file that should be used to compute initial values for simulations. + * @param[in] simulation_parameters Map with parameters necessary for the simulation which can be different for diffferent start dates. + * Provide the parameters "start_month", "start_day","seasonality" (parameter k for the seasonality of the models), + * "RelativeTransmissionNoSymptoms", "RiskOfInfectionFromSymptomatic", "scale_confirmed_cases" (to scale the RKI data while computing an initialization vector), + * "lockdown_hard" (Proportion of counties for which a hard lockdown is implemented) and + * "scale_contacts" (scales contacts per hand to match the new infections in the RKI data). + * The assumption regarding the number of subcompartments of the LCT model can be controlled via the parameter "num_subcompartments". + * @param[in] save_dir Specifies the directory where the results should be stored. Provide an empty string if results should not be saved. + * @param[in] print_result Specifies if the results should be printed. + * @returns Any io errors that happen during reading of the RKI file or files for contact matrices or saving the results. + */ +mio::IOResult simulate(std::string const& path, std::map simulation_parameters, + std::string save_dir = "", bool print_result = false) +{ + // Set values needed for initialization. + ScalarType total_population = 83155031.0; + mio::Date start_date = + mio::Date(2020, (int)simulation_parameters["start_month"], (int)simulation_parameters["start_day"]); + mio::Date end_date = mio::offset_date_by_days(start_date, 45); + + // Define parameters used for simulation and initialization. + // Parameters are calculated via examples/compute_parameters.cpp. + mio::lsecir::Parameters parameters; + parameters.get() = 3.335; + parameters.get() = 3.31331; + parameters.get() = 6.94547; + parameters.get() = 11.634346; + parameters.get() = 17.476959; + parameters.get() = 0.0733271; + parameters.get() = + simulation_parameters["RelativeTransmissionNoSymptoms"]; + parameters.get() = + simulation_parameters["RiskOfInfectionFromSymptomatic"]; + parameters.get() = simulation_parameters["seasonality"]; + parameters.get() = mio::get_day_in_year(start_date); + parameters.get() = 0.206901; + parameters.get() = 0.0786429; + parameters.get() = 0.173176; + parameters.get() = 0.217177; + + auto status = set_contact_matrices("../../data", parameters, simulation_parameters); + + // Define number of subcompartments. + std::vector vec_subcompartments((int)mio::lsecir::InfectionStateBase::Count, 1); + if (!(simulation_parameters["num_subcompartments"] == 0)) { + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = + simulation_parameters["num_subcompartments"]; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = + simulation_parameters["num_subcompartments"]; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = + simulation_parameters["num_subcompartments"]; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = + simulation_parameters["num_subcompartments"]; + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = + simulation_parameters["num_subcompartments"]; + } + else { + // Use subcompartments with a soujourn time of approximately one day in each subcompartment. + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = + (int)round(parameters.get()); + } + mio::lsecir::InfectionState infectionState(vec_subcompartments); + + // Calculate initial value vector for subcompartments with RKI data. + BOOST_OUTCOME_TRY(init_subcompartments, mio::lsecir::get_initial_data_from_file( + path, start_date, infectionState, std::move(parameters), + total_population, simulation_parameters["scale_confirmed_cases"])); + // Sum subcompartments to get initial values ​​for the ODE model. + Eigen::VectorXd init_base((int)mio::lsecir::InfectionStateBase::Count); + for (int i = 0; i < (int)mio::lsecir::InfectionStateBase::Count; i++) { + init_base[i] = + init_subcompartments + .segment(Eigen::Index(infectionState.get_firstindex(i)), Eigen::Index(infectionState.get_number(i))) + .sum(); + } + + // Initialize LCT model and perform simulation. + mio::lsecir::Model model_lct(std::move(init_subcompartments), infectionState, std::move(parameters)); + mio::TimeSeries result_lct = mio::lsecir::simulate( + 0, mio::get_offset_in_days(end_date, start_date), 0.1, model_lct, + std::make_shared>(1e-10, 1e-5, 0, + 0.1)); + // Calculate result without division in subcompartments. + mio::TimeSeries populations_lct = model_lct.calculate_populations(result_lct); + + if (print_result) { + // Print results. + std::cout << "Result of LCT " << (int)simulation_parameters["num_subcompartments"] << " model:" << std::endl; + mio::lsecir::print_TimeSeries(populations_lct, model_lct.get_heading_CompartmentsBase()); + } + if (!save_dir.empty()) { + // Save results. + std::string filename = save_dir + "real_lct" + + std::to_string((int)simulation_parameters["num_subcompartments"]) + "_2020_" + + std::to_string((int)simulation_parameters["start_month"]) + "_" + + std::to_string((int)simulation_parameters["start_day"]) + ".h5"; + auto save_result_status = mio::save_result({populations_lct}, {0}, 1, filename); + } + // Print commands to get the number of new infections on the first day of simulation. Could be used to scale the contacts. + std::cout << "Number of new infections on the first day of simulation for LCT model with " + << (int)simulation_parameters["num_subcompartments"] << " subcompartments: " << std::endl; + std::cout << std::fixed << std::setprecision(1) + << (populations_lct[0][0] - populations_lct[1][0]) / + (populations_lct.get_time(1) - populations_lct.get_time(0)) + << std::endl; + + return mio::success(); +} + +int main() +{ + std::string save_dir = "../../data/simulation_lct/real/"; + std::map simulation_parameters_2020_06_01 = {{"num_subcompartments", 1}, + {"start_month", 6}, + {"start_day", 1}, + {"seasonality", 0.2}, + {"RelativeTransmissionNoSymptoms", 0.7}, + {"RiskOfInfectionFromSymptomatic", 0.2}, + {"scale_confirmed_cases", 1.}, + {"lockdown_hard", 0.03 * 14 / (45 * 401.)}, + {"scale_contacts", 445.7694 / 516.8929}}; + + std::map simulation_parameters_2020_10_01 = {{"num_subcompartments", 1}, + {"start_month", 10}, + {"start_day", 1}, + {"seasonality", 0.2}, + {"RelativeTransmissionNoSymptoms", 1}, + {"RiskOfInfectionFromSymptomatic", 0.3}, + {"scale_confirmed_cases", 2.}, + {"lockdown_hard", 371 * 14 / (45 * 401.)}, + {"scale_contacts", 11154.2091 / 13106.6115}}; + /* Values for "RelativeTransmissionNoSymptoms", "RelativeTransmissionNoSymptoms" and "seasonality" are suitable values based on doi: 10.1016/j.mbs.2021.108648. + "scale_confirmed_cases" are values directly from this paper. + "lockdown_hard" is based on the number of beginnings of a strict lockdown for 14 days of the 45 days simulation period in the lockdown. Value is not used for 01/06/2020 and + scaled in octiber to assume that in the beginning of the period are less counties in lockdown and more in the second half. + "lockdown_hard" should give the average percentage of counties that are in a hard lockdown on a simulation day. + "scale_contacts" is used to match the predicted number of new infections on the first simulation day to the RKI data. */ + + // Paths are valid if file is executed eg in memilio/build/bin + // Simulation with start date 01.06.2020 with 1 subcompartment. + auto result = + simulate("../../data/pydata/Germany/cases_all_germany_ma7.json", simulation_parameters_2020_06_01, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + // Simulation with start date 01.06.2020 with 3 subcompartments. + simulation_parameters_2020_06_01["num_subcompartments"] = 3; + simulation_parameters_2020_06_01["scale_contacts"] = 445.7694 / 516.6578; + result = + simulate("../../data/pydata/Germany/cases_all_germany_ma7.json", simulation_parameters_2020_06_01, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + // Simulation with start date 01.06.2020 with 10 subcompartments. + simulation_parameters_2020_06_01["num_subcompartments"] = 10; + simulation_parameters_2020_06_01["scale_contacts"] = 445.7694 / 516.5778; + result = + simulate("../../data/pydata/Germany/cases_all_germany_ma7.json", simulation_parameters_2020_06_01, save_dir); + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + + // Simulation with start date 01.10.2020 with 1 subcompartment. + result = + simulate("../../data/pydata/Germany/cases_all_germany_ma7.json", simulation_parameters_2020_10_01, save_dir); + + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + // Simulation with start date 01.10.2020 with 3 subcompartments. + simulation_parameters_2020_10_01["num_subcompartments"] = 3; + simulation_parameters_2020_10_01["scale_contacts"] = 11154.2091 / 13105.7208; + result = + simulate("../../data/pydata/Germany/cases_all_germany_ma7.json", simulation_parameters_2020_10_01, save_dir); + + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + // Simulation with start date 01.10.2020 with 10 subcompartments. + simulation_parameters_2020_10_01["num_subcompartments"] = 10; + simulation_parameters_2020_10_01["scale_contacts"] = 11154.2091 / 13102.1831; + result = + simulate("../../data/pydata/Germany/cases_all_germany_ma7.json", simulation_parameters_2020_10_01, save_dir); + + if (!result) { + printf("%s\n", result.error().formatted_message().c_str()); + return -1; + } + return 0; +} \ No newline at end of file diff --git a/cpp/memilio/epidemiology/state_age_function.h b/cpp/memilio/epidemiology/state_age_function.h index e865948717..702f5c89f0 100644 --- a/cpp/memilio/epidemiology/state_age_function.h +++ b/cpp/memilio/epidemiology/state_age_function.h @@ -22,12 +22,15 @@ #include "memilio/config.h" #include "memilio/utils/parameter_set.h" -#include "ide_secir/infection_state.h" -#include "memilio/math/eigen.h" #include "memilio/math/smoother.h" #include "memilio/math/floating_point.h" #include "memilio/epidemiology/uncertain_matrix.h" +MSVC_WARNING_DISABLE_PUSH(4702) +#include +MSVC_WARNING_POP() +#include "boost/math/distributions/lognormal.hpp" + namespace mio { /************************** @@ -48,7 +51,11 @@ namespace mio * b) Arbitrary non-negative functions used for parameters such as TransmissionProbabilityOnContact. * * Derived classes must implement the eval method which implements the actual function that is evaluated at some state age. - * This function can depend on one parameter. + * This function can depend on the parameter scale to scale the function and on the parameter location to shift the function. + * Location should be a positive number to fulfill the characteristics of a TransitionDistribution and shift has to be positive. + * For a Function F we normally use these parameters at state age x as F(x,location,scale)=F((x-location)/scale). + * A derived class does not have to use these two parameters. + * Additionally there is one parameter which specifies the distribution. * * The derived classes must also implement the clone_impl method which allows to deepcopy the derived class. * @@ -66,12 +73,20 @@ struct StateAgeFunction { * @brief Constructs a new StateAgeFunction object * * @param[in] init_parameter Specifies the initial function parameter of the function. + * @param[in] init_location Location paramter to shift the StateAgeFunction. + * @param[in] init_scale Scale paramter to scale the StateAgeFunction. */ - StateAgeFunction(ScalarType init_parameter) + StateAgeFunction(ScalarType init_parameter, ScalarType init_location = 0, ScalarType init_scale = 1) : m_parameter{init_parameter} + , m_location{init_location} + , m_scale{init_scale} , m_support_max{-1.} // initialize support maximum as not set , m_support_tol{-1.} // initialize support tolerance as not set { + if (m_scale <= 0) { + log_error("The scale Parameter of a StateAgeFunction has to be positive. Set scale to 1."); + m_scale = 1; + } } /** @@ -104,7 +119,8 @@ struct StateAgeFunction { */ bool operator==(const StateAgeFunction& other) const { - return (typeid(*this).name() == typeid(other).name() && m_parameter == other.get_parameter()); + return (typeid(*this).name() == typeid(other).name() && m_parameter == other.get_parameter() && + m_location == other.get_location() && m_scale == other.get_scale()); } /** @@ -117,7 +133,7 @@ struct StateAgeFunction { virtual ScalarType eval(ScalarType state_age) = 0; /** - * @brief Get the m_parameter object + * @brief Get the m_parameter object. * * Can be used to access the m_parameter object, which specifies the used function. * @@ -141,9 +157,73 @@ struct StateAgeFunction { */ void set_parameter(ScalarType new_parameter) { - m_parameter = new_parameter; + m_parameter = new_parameter; + m_support_max = -1.; + m_support_tol = -1.; + } + + /** + * @brief Get the m_location object. + * + * Can be used to access the m_location object, which specifies the shift of the function. + * + * @return ScalarType + */ + ScalarType get_location() const + { + return m_location; + } + /** + * @brief Set the m_location object. + * + * Can be used to set the m_location object, which specifies the shift of the function. + * The maximum support of a function may be costly to evaluate. In order to not always reevaluate or recompute the + * support when the user asks for it, a cached value is used. If m_support_max is set to -1, the cached value is + * deleted and a recomputation is done the next time the user asks for the support. As the support (potentially) + * depends on the m_location object, the cached value has to be deleted. For details see get_support_max(). + * + *@param[in] new_location New location for StateAgeFunction. + */ + void set_location(ScalarType new_location) + { + m_location = new_location; m_support_max = -1.; + m_support_tol = -1.; + } + + /** + * @brief Get the m_scale object. + * + * Can be used to access the m_scale object, which is used to scale the function. + * + * @return ScalarType + */ + ScalarType get_scale() const + { + return m_scale; + } + + /** + * @brief Set the m_scale object. + * + * Can be used to access the m_scale object, which is used to scale the function. + * The maximum support of a function may be costly to evaluate. In order to not always reevaluate or recompute the + * support when the user asks for it, a cached value is used. If m_support_max is set to -1, the cached value is + * deleted and a recomputation is done the next time the user asks for the support. As the support (potentially) + * depends on the m_scale object, the cached value has to be deleted. For details see get_support_max(). + * + *@param[in] new_scale New Scale for StateAgeFunction. + */ + void set_scale(ScalarType new_scale) + { + if (new_scale <= 0) { + log_error("The scale Parameter of a StateAgeFunction has to be positive. Set scale to 1."); + new_scale = 1; + } + m_scale = new_scale; + m_support_max = -1.; + m_support_tol = -1.; } /** @@ -205,6 +285,8 @@ struct StateAgeFunction { virtual StateAgeFunction* clone_impl() const = 0; ScalarType m_parameter; ///< Parameter for function in derived class. + ScalarType m_location; ///< Location parameter for function in derived class. + ScalarType m_scale; ///< Scale parameter for function in derived class. ScalarType m_support_max; ///< Maximum of the support of the function. ScalarType m_support_tol; ///< Tolerance for computation of the support. }; @@ -219,12 +301,14 @@ struct StateAgeFunction { struct ExponentialDecay : public StateAgeFunction { /** - * @brief Constructs a new ExponentialDecay object + * @brief Constructs a new ExponentialDecay object. * * @param[in] init_parameter Specifies the initial function parameter of the function. + * @param[in] init_location Location paramter to shift the exponentialdecay function. + * Should be a positive number to fulfill characteristics of a TransitionDistribution. */ - ExponentialDecay(ScalarType init_parameter) - : StateAgeFunction(init_parameter) + ExponentialDecay(ScalarType init_parameter, ScalarType init_location = 0) + : StateAgeFunction(init_parameter, init_location) { } @@ -238,7 +322,10 @@ struct ExponentialDecay : public StateAgeFunction { */ ScalarType eval(ScalarType state_age) override { - return std::exp(-m_parameter * state_age); + if (state_age <= m_location) { + return 1; + } + return std::exp(-m_parameter * (state_age - m_location)); } protected: @@ -262,9 +349,11 @@ struct SmootherCosine : public StateAgeFunction { * @brief Constructs a new SmootherCosine object * * @param[in] init_parameter specifies the initial parameter of the function. + * @param[in] init_location Location paramter to shift the SmootherCosine function. + * Should be a positive number to fulfill characteristics of a TransitionDistribution. */ - SmootherCosine(ScalarType init_parameter) - : StateAgeFunction(init_parameter) + SmootherCosine(ScalarType init_parameter, ScalarType init_location = 0) + : StateAgeFunction(init_parameter, init_location) { } @@ -278,7 +367,10 @@ struct SmootherCosine : public StateAgeFunction { */ ScalarType eval(ScalarType state_age) override { - return smoother_cosine(state_age, 0.0, m_parameter, 1.0, 0.0); + if (state_age <= m_location) { + return 1; + } + return smoother_cosine(state_age - m_location, 0.0, m_parameter, 1.0, 0.0); } /** @@ -294,7 +386,7 @@ struct SmootherCosine : public StateAgeFunction { { unused(dt); unused(tol); - m_support_max = m_parameter; + m_support_max = m_parameter + m_location; return m_support_max; } @@ -310,6 +402,105 @@ struct SmootherCosine : public StateAgeFunction { } }; +/** + * @brief Class that defines an GammaSurvivalFunction function depending on the state age. + * A survival function is defined as 1 - cumulative density function. + * GammaSurvivalFunction is derived from StateAgeFunction. + * The shape parameter of the Gamma function is the parameter of the StateAgeFunction. + * If shape is an unsigned Integer, the Gamma distribution is an Erlang function. + */ +struct GammaSurvivalFunction : public StateAgeFunction { + + /** + * @brief Constructs a new GammaSurvivalFunction object. + * + * @param[in] init_shape Parameter shape of the GammaSurvivalFunction. For the Erlang distribution, shape has to be a positive integer. + * Choosing shape = 1 leads to an exponential function with parameter 1/scale. + * @param[in] init_location Location paramter to shift the GammaSurvivalFunction. + * Should be a positive number to fulfill characteristics of a TransitionDistribution. + * @param[in] init_scale Parameter shape of the GammaSurvivalFunction. Corresponds to the inverse of the rate parameter of a Gamma distribution. + */ + GammaSurvivalFunction(ScalarType init_shape = 1, ScalarType init_location = 0, ScalarType init_scale = 1) + : StateAgeFunction(init_shape, init_location, init_scale) + { + } + + /** + * @brief Defines GammaSurvivalFunction depending on state_age. + * + * @param[in] state_age Time at which the function is evaluated. + * @return Evaluation of the function at state_age. + */ + ScalarType eval(ScalarType state_age) override + { + if (state_age <= m_location) { + return 1; + } + boost::math::gamma_distribution> gamma(m_parameter, m_scale); + return boost::math::cdf(boost::math::complement(gamma, state_age - m_location)); + } + +protected: + /** + * @brief Implements clone for GammaSurvivalFunction. + * + * @return Pointer to StateAgeFunction. + */ + StateAgeFunction* clone_impl() const override + { + return new GammaSurvivalFunction(*this); + } +}; + +/** + * @brief Class that defines an LognormSurvivalFunction function depending on the state age. + * A survival function is defined as 1 - cumulative density function. + */ +struct LognormSurvivalFunction : public StateAgeFunction { + + /** + * @brief Constructs a new LognormSurvivalFunction object. + * + * Location and scale parameters are according to these parameters in the python package scipy. + * + * @param[in] init_parameter Specifies the initial function parameter of the function. + * @param[in] init_location Location paramter of LognormSurvivalFunction. The parameter can be + * used to shift the function. Should be non-negative to fulfill the conditions of a + * StateAgeFunction. + * @param[in] init_scale Scale paramter of LognormSurvivalFunction. + */ + LognormSurvivalFunction(ScalarType init_parameter, ScalarType init_location = 0, ScalarType init_scale = 1) + : StateAgeFunction(init_parameter, init_location, init_scale) + { + } + + /** + * @brief Defines the value of the LognormSurvivalFunction depending on state_age. + * + * @param[in] state_age Time at which the function is evaluated. + * @return Evaluation of the function at state_age. + */ + ScalarType eval(ScalarType state_age) override + { + if (state_age < m_location) { + return 1; + } + boost::math::lognormal_distribution> logn(0., m_parameter); + return boost::math::cdf(boost::math::complement(logn, (state_age - m_location) / m_scale)); + } + +protected: + /** + * @brief Implements clone for LognormSurvivalFunction. + * + * @return Pointer to StateAgeFunction. + */ + StateAgeFunction* clone_impl() const override + { + return new LognormSurvivalFunction(*this); + } +}; + /** * @brief Class that defines a constant function. */ @@ -377,6 +568,84 @@ struct ConstantFunction : public StateAgeFunction { } }; +/** + * @brief Class that defines an Erlang density function with the parameters shape and scale depending on the state age. + * Class is needed for the initialization of the subcompartments for LCT model. + * ErlangDensity is derived from StateAgeFunction. + * The shape parameter of the Erlang function is the parameter of the Stateagefunction. + * Attention: The density does not have the characteristics of a TransitionDistribution!! + */ +struct ErlangDensity : public StateAgeFunction { + + /** + * @brief Constructs a new ErlangDensity object. + * + * @param[in] init_shape Parameter shape of the ErlangDensity. For the Erlang distribution, shape has to be a positive integer. + * @param[in] init_scale Parameter scale of the ErlangDensity. Corresponds to the inverse rate parameter. + */ + ErlangDensity(unsigned int init_shape, ScalarType init_scale) + : StateAgeFunction(init_shape, 0., init_scale) + { + } + + /** + * @brief Defines ErlangDensity depending on state_age. + * + * Parameters scale and shape are used. + * + * @param[in] state_age Time at which the function is evaluated. + * @return Evaluation of the function at state_age. + */ + ScalarType eval(ScalarType state_age) override + { + if (state_age < 0) { + return 0; + } + int shape = (int)m_parameter; + return std::pow(state_age / m_scale, shape - 1) / (m_scale * boost::math::factorial(shape - 1)) * + std::exp(-state_age / m_scale); + } + + /** + * @brief Computes the maximum of the support of the function. + * + * For small time steps and small variance of the density it is possible that dt is returned with the function of StateAgeFunction. + * StateAgeFunction is designed for survialfunctions, not for densities. + * Therefore with this function we calculate the smallest time value t where function(tau)=0 for all tau>t. + * + * @param[in] dt Time step size. + * @param[in] tol Tolerance used for cutting the support if the function value falls below. + * @return ScalarType support_max + */ + ScalarType get_support_max(ScalarType dt, ScalarType tol = 1e-10) override + { // We are looking for the smallest time value t where function(tau)=0 for all tau>t. Thus support max is bigger than the mean. + ScalarType mean = m_parameter * m_scale; + ScalarType support_max = (ScalarType)dt * (int)(mean / dt); + + if (!floating_point_equal(m_support_tol, tol, 1e-14) || floating_point_equal(m_support_max, -1., 1e-14)) { + while (eval(support_max) >= tol) { + support_max += dt; + } + + m_support_max = support_max; + m_support_tol = tol; + } + + return m_support_max; + } + +protected: + /** + * @brief Implements clone for ErlangDensity. + * + * @return Pointer to StateAgeFunction. + */ + StateAgeFunction* clone_impl() const override + { + return new ErlangDensity(*this); + } +}; + /********************************* * Define StateAgeFunctionWrapper * *********************************/ @@ -444,7 +713,8 @@ struct StateAgeFunctionWrapper { bool operator==(const StateAgeFunctionWrapper& other) const { return (m_function->get_state_age_function_type() == other.get_state_age_function_type() && - m_function->get_parameter() == other.get_parameter()); + m_function->get_parameter() == other.get_parameter() && + m_function->get_location() == other.get_location() && m_function->get_scale() == other.get_scale()); } /** @@ -497,6 +767,44 @@ struct StateAgeFunctionWrapper { { m_function->set_parameter(new_parameter); } + /** + * @brief Get the m_location object of m_function. + * + * @return ScalarType + */ + ScalarType get_location() const + { + return m_function->get_location(); + } + + /** + * @brief Set the m_location object of m_function. + * + * @param[in] new_location New location for StateAgeFunction. + */ + void set_location(ScalarType new_location) + { + m_function->set_location(new_location); + } + /** + * @brief Get the m_scale object of m_function. + * + * @return ScalarType + */ + ScalarType get_scale() const + { + return m_function->get_scale(); + } + + /** + * @brief Set the m_scale object of m_function. + * + * @param[in] new_scale New scale for StateAgeFunction. + */ + void set_scale(ScalarType new_scale) + { + m_function->set_scale(new_scale); + } ScalarType get_support_max(ScalarType dt, ScalarType tol = 1e-10) const { diff --git a/cpp/memilio/io/epi_data.h b/cpp/memilio/io/epi_data.h index 75f99a18e2..6416deb696 100644 --- a/cpp/memilio/io/epi_data.h +++ b/cpp/memilio/io/epi_data.h @@ -65,6 +65,58 @@ class StringDate : public Date } }; +/** + * Represents the entries of a confirmed cases data file, e.g., from RKI. + * Number of confirmed, recovered and deceased in a Germany on a specific date. + * ConfirmedCasesNoAgeEntry is a simplified version of ConfirmedCasesDataEntry without agegroups. + */ +class ConfirmedCasesNoAgeEntry +{ +public: + double num_confirmed; + double num_recovered; + double num_deaths; + Date date; + + template + static IOResult deserialize(IOContext& io) + { + auto obj = io.expect_object("ConfirmedCasesNoAgeEntry"); + auto num_confirmed = obj.expect_element("Confirmed", Tag{}); + auto num_recovered = obj.expect_element("Recovered", Tag{}); + auto num_deaths = obj.expect_element("Deaths", Tag{}); + auto date = obj.expect_element("Date", Tag{}); + return apply( + io, + [](auto&& nc, auto&& nr, auto&& nd, auto&& d) { + return ConfirmedCasesNoAgeEntry{nc, nr, nd, d}; + }, + num_confirmed, num_recovered, num_deaths, date); + } +}; + +/** + * Deserialize a list of ConfirmedCasesNoAgeEntry from json. + * @param jsvalue json value, must be an array of objects, objects must match ConfirmedCasesNoAgeEntry. + * Exactly one entry per date should be provided, for example no entries per age group. + * @return list of ConfirmedCasesNoAgeEntry. + */ +inline IOResult> deserialize_confirmed_cases_noage(const Json::Value& jsvalue) +{ + return deserialize_json(jsvalue, Tag>{}); +} + +/** + * Deserialize a list of ConfirmedCasesNoAgeEntry from json. + * @param jsvalue json value, must be an array of objects, objects must match ConfirmedCasesNoAgeEntry. + * Exactly one entry per date should be provided, for example no entries per age group + * @return list of ConfirmedCasesNoAgeEntry. + */ +inline IOResult> read_confirmed_cases_noage(const std::string& filename) +{ + return read_json(filename, Tag>{}); +} + /** * Represents the entries of a confirmed cases data file, e.g., from RKI. * Number of confirmed, recovered and deceased in a region on a specific date. diff --git a/cpp/models/ide_secir/model.cpp b/cpp/models/ide_secir/model.cpp index a6af11407c..5e458f45d4 100644 --- a/cpp/models/ide_secir/model.cpp +++ b/cpp/models/ide_secir/model.cpp @@ -29,64 +29,90 @@ namespace mio namespace isecir { -Model::Model(TimeSeries&& init, ScalarType N_init, ScalarType Dead_before, +Model::Model(TimeSeries&& init, ScalarType N_init, ScalarType deaths, ScalarType total_confirmed_cases, const ParameterSet& Parameterset_init) : parameters{Parameterset_init} , m_transitions{std::move(init)} , m_populations{TimeSeries(Eigen::Index(InfectionState::Count))} , m_N{N_init} - , m_deaths_before{Dead_before} + , m_total_confirmed_cases(total_confirmed_cases) { + m_deaths_before = + deaths - m_transitions.get_last_value()[Eigen::Index(InfectionTransition::InfectedCriticalToDead)]; m_populations.add_time_point( 0, TimeSeries::Vector::Constant((int)InfectionState::Count, 0)); - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)] = - m_deaths_before + m_transitions.get_last_value()[Eigen::Index(InfectionTransition::InfectedCriticalToDead)]; + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)] = deaths; } void Model::initialize(ScalarType dt) { - // compute Susceptibles at time 0 and m_forceofinfection at time -m_dt as initial values for discretization scheme - // use m_forceofinfection at -m_dt to be consistent with further calculations of S (see compute_susceptibles()), - // where also the value of m_forceofinfection for the previous timestep is used - update_forceofinfection(dt, true); - if (m_forceofinfection > 0) { - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] = + if (m_total_confirmed_cases < 1e-12) { + // compute Susceptibles at time 0 and m_forceofinfection at time -m_dt as initial values for discretization scheme + // use m_forceofinfection at -m_dt to be consistent with further calculations of S (see compute_susceptibles()), + // where also the value of m_forceofinfection for the previous timestep is used + update_forceofinfection(dt, true); + if (m_forceofinfection > 1e-12) { + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] = m_transitions.get_last_value()[Eigen::Index(InfectionTransition::SusceptibleToExposed)] / (dt * m_forceofinfection); - //calculate other compartment sizes for t=0 - other_compartments_current_timestep(dt); - - //R; need an initial value for R, therefore do not calculate via compute_recovered() - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] = - m_N - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Exposed)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedNoSymptoms)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSymptoms)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSevere)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedCritical)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)]; + //calculate other compartment sizes for t=0 + other_compartments_current_timestep(dt); + + //R; need an initial value for R, therefore do not calculate via compute_recovered() + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] = + m_N - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Exposed)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedNoSymptoms)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSymptoms)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSevere)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedCritical)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)]; + } + else if (m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] > 1e-12) { + //take initialized value for Susceptibles if value can't be calculated via the standard formula + //calculate other compartment sizes for t=0 + other_compartments_current_timestep(dt); + + //R; need an initial value for R, therefore do not calculate via compute_recovered() + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] = + m_N - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Exposed)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedNoSymptoms)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSymptoms)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSevere)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedCritical)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)]; + } + else if (m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] > 1e-12) { + //if value for Recovered is initialized and standard method is not applicable, calculate Susceptibles via other compartments + //determining other compartment sizes is not dependent of Susceptibles(0), just of the transitions of the past. + //calculate other compartment sizes for t=0 + other_compartments_current_timestep(dt); + + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] = + m_N - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Exposed)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedNoSymptoms)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSymptoms)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSevere)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedCritical)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] - + m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)]; + } + else { + log_error("Error occured while initializing compartments: Force of infection is evaluated to 0 and neither " + "Susceptibles nor Recovered for time 0 were set. One of them should be larger 0."); + } } - else if (m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] > 1e-12) { - //take initialized value for Susceptibles if value can't be calculated via the standard formula - //calculate other compartment sizes for t=0 + else { other_compartments_current_timestep(dt); - //R; need an initial value for R, therefore do not calculate via compute_recovered() + // The scheme of the ODE model for initialization is applied here. m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] = - m_N - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Exposed)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedNoSymptoms)] - - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSymptoms)] - + m_total_confirmed_cases - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSymptoms)] - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedSevere)] - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::InfectedCritical)] - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)]; - } - else if (m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] > 1e-12) { - //if value for Recovered is initialized and standard method is not applicable, calculate Susceptibles via other compartments - //determining other compartment sizes is not dependent of Susceptibles(0), just of the transitions of the past. - //calculate other compartment sizes for t=0 - other_compartments_current_timestep(dt); m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Susceptible)] = m_N - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Exposed)] - @@ -97,11 +123,6 @@ void Model::initialize(ScalarType dt) m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Recovered)] - m_populations[Eigen::Index(0)][Eigen::Index(InfectionState::Dead)]; } - else { - log_error("Error occured while initializing compartments: Force of infection is evaluated to 0 and neither " - "Susceptibles nor Recovered for time 0 were set. One of them should be larger 0."); - } - // compute m_forceofinfection at time 0 needed for further simulation update_forceofinfection(dt); } @@ -126,7 +147,7 @@ void Model::compute_flow(int idx_InfectionTransitions, Eigen::Index idx_Incoming This needs to be adjusted if we are changing the finite difference scheme */ Eigen::Index calc_time_index = (Eigen::Index)std::ceil( - parameters.get()[idx_InfectionTransitions].get_support_max(dt) / dt); + parameters.get()[idx_InfectionTransitions].get_support_max(dt, m_tol) / dt); Eigen::Index num_time_points = m_transitions.get_num_time_points(); @@ -196,13 +217,13 @@ void Model::update_forceofinfection(ScalarType dt, bool initialization) // determine the relevant calculation area = union of the supports of the relevant transition distributions ScalarType calc_time = std::max( {parameters.get()[(int)InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedNoSymptomsToRecovered] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedSymptomsToInfectedSevere] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedSymptomsToRecovered] - .get_support_max(dt)}); + .get_support_max(dt, m_tol)}); // corresponding index /* need calc_time_index timesteps in sum, @@ -262,8 +283,8 @@ void Model::compute_compartment(Eigen::Index idx_InfectionState, Eigen::Index id // determine relevant calculation area and corresponding index ScalarType calc_time = - std::max(parameters.get()[idx_TransitionDistribution1].get_support_max(dt), - parameters.get()[idx_TransitionDistribution2].get_support_max(dt)); + std::max(parameters.get()[idx_TransitionDistribution1].get_support_max(dt, m_tol), + parameters.get()[idx_TransitionDistribution2].get_support_max(dt, m_tol)); Eigen::Index calc_time_index = (Eigen::Index)std::ceil(calc_time / dt) - 1; @@ -287,9 +308,9 @@ void Model::other_compartments_current_timestep(ScalarType dt) { // E compute_compartment(Eigen::Index(InfectionState::Exposed), Eigen::Index(InfectionTransition::SusceptibleToExposed), - (int)InfectionTransition::ExposedToInfectedNoSymptoms, 0, - dt); // this is a dummy index as there is no transition from E to R in our model, - // write any transition here as probability from E to R is 0 + (int)InfectionTransition::ExposedToInfectedNoSymptoms, + (int)InfectionTransition::ExposedToInfectedNoSymptoms, + dt); // Second index is a dummy index as probability from E to R is 0. Repeat first Index here. // C compute_compartment(Eigen::Index(InfectionState::InfectedNoSymptoms), Eigen::Index(InfectionTransition::ExposedToInfectedNoSymptoms), diff --git a/cpp/models/ide_secir/model.h b/cpp/models/ide_secir/model.h index 35b3c75bb1..9de8c1cb8d 100644 --- a/cpp/models/ide_secir/model.h +++ b/cpp/models/ide_secir/model.h @@ -46,10 +46,11 @@ class Model * A warning is displayed if the condition is violated. * @param[in] dt_init The size of the time step used for numerical simulation. * @param[in] N_init The population of the considered region. - * @param[in] Dead_before The total number of deaths at the time point - dt_init. + * @param[in] deaths The total number of deaths at the time zero. + * @param[in] total_confirmed_cases Total confirmed cases at time t0 can be set if it should be used for initialisation. * @param[in, out] Parameterset_init Used Parameters for simulation. */ - Model(TimeSeries&& init, ScalarType N_init, ScalarType Dead_before, + Model(TimeSeries&& init, ScalarType N_init, ScalarType deaths, ScalarType total_confirmed_cases = 0, const ParameterSet& Parameterset_init = ParameterSet()); /** @@ -74,24 +75,25 @@ class Model ScalarType support_max = std::max( {parameters.get()[(int)InfectionTransition::ExposedToInfectedNoSymptoms] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedNoSymptomsToRecovered] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedSymptomsToInfectedSevere] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedSymptomsToRecovered] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedSevereToInfectedCritical] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedSevereToRecovered] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedCriticalToDead] - .get_support_max(dt), + .get_support_max(dt, m_tol), parameters.get()[(int)InfectionTransition::InfectedCriticalToRecovered] - .get_support_max(dt)}); - + .get_support_max(dt, m_tol)}); + ScalarType a = support_max; + unused(a); if (m_transitions.get_num_time_points() < (Eigen::Index)std::ceil(support_max / dt)) { log_error( "Initialization failed. Not enough time points for transitions given before start of simulation."); @@ -173,6 +175,7 @@ class Model * @param[in] idx_TransitionDistribution2 Specifies the index of the second relevant TransitionDistribution, * related to a flow from the considered #InfectionState to any other #InfectionState (in most cases to Recovered). * Necessary related probability is calculated via 1-probability[idx_TransitionDistribution1]. + * If no second index is needed eg if probability[idx_TransitionDistribution1]=1, repeat the first index. * @param[in] dt Time discretization step size. */ void compute_compartment(Eigen::Index idx_InfectionState, Eigen::Index idx_IncomingFlow, @@ -195,6 +198,26 @@ class Model */ void compute_recovered(); + /** + * @brief Setter for total number of confirmed cases at time t0. + * + * @param[in] total_confirmed_cases Total confirmed cases at time t0 can be set if it should be used for initialisation. + */ + void set_total_confirmed_cases(ScalarType total_confirmed_cases) + { + m_total_confirmed_cases = total_confirmed_cases; + } + + /** + * @brief Setter for the tolerance used to calculate the maximum support of the TransitionDistributions. + * + * @param[in] new_tol New tolerance. + */ + void set_tol_for_support_max(ScalarType new_tol) + { + m_tol = new_tol; + } + ParameterSet parameters{}; ///< ParameterSet of Model Parameters. /* Attention: m_populations and m_transitions do not necessarily have the same number of time points due to the initialization part. */ TimeSeries @@ -205,7 +228,9 @@ class Model private: ScalarType m_forceofinfection{0}; ///< Force of infection term needed for numerical scheme. ScalarType m_N{0}; ///< Total population size of the considered region. - ScalarType m_deaths_before{0}; ///< Deaths before start of simulation (at time -m_dt). + ScalarType m_deaths_before{0}; ///< Total number of deaths at the time point - dt_init. + ScalarType m_total_confirmed_cases{0}; ///< Total number of confirmed cases at time t0. + ScalarType m_tol{1e-10}; ///< Tolerance used to calculate the maximum support of the TransitionDistributions. }; } // namespace isecir diff --git a/cpp/models/ide_secir/parameters.h b/cpp/models/ide_secir/parameters.h index 5abe62ae25..0f4d2d60e4 100644 --- a/cpp/models/ide_secir/parameters.h +++ b/cpp/models/ide_secir/parameters.h @@ -270,9 +270,9 @@ class Parameters : public ParametersBase return true; } - for (size_t i = 0; i < (int)InfectionTransition::Count; i++) { + for (size_t i = 1; i < (int)InfectionTransition::Count; i++) { if (floating_point_less(this->get()[i].get_support_max(10), 0.0, 1e-14)) { - log_error("Constraint check: One parameter in TransitionDistributions has invalid support."); + log_error("Constraint check: One function in TransitionDistributions has invalid support."); return true; } } diff --git a/cpp/models/lct_secir/CMakeLists.txt b/cpp/models/lct_secir/CMakeLists.txt new file mode 100644 index 0000000000..c22a4a3931 --- /dev/null +++ b/cpp/models/lct_secir/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(lct_secir + infection_state.h + model.h + model.cpp + simulation.h + simulation.cpp + parameters.h + initialization.h + initialization.cpp + parameters_io.h + parameters_io.cpp +) +target_link_libraries(lct_secir PUBLIC memilio) +target_include_directories(lct_secir PUBLIC + $ + $ +) +target_compile_options(lct_secir PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) \ No newline at end of file diff --git a/cpp/models/lct_secir/README.md b/cpp/models/lct_secir/README.md new file mode 100755 index 0000000000..aaf43055ad --- /dev/null +++ b/cpp/models/lct_secir/README.md @@ -0,0 +1,24 @@ +# LCT SECIR model + +This model is based on the Linear Chain Trick. The eight compartments +- Susceptible, may become exposed at any time +- Exposed, becomes infected after some time +- InfectedNoSymptoms, becomes InfectedSymptoms or Recovered after some time +- InfectedSymptoms, becomes InfectedSevere or Recovered after some time +- InfectedSevere, becomes InfectedCritical or Recovered after some time +- InfectedCritical, becomes Recovered or Dead after some time +- Recovered +- Dead + +are used to simulate the spread of the disease. +It ist possible to include subcompartments for the five compartments Exposed, InfectedNoSymptoms, InfectedSymptoms, InfectedSevere and InfectedCritical. + + +## Examples + +A simple example can be found at: examples/lct_secir.cpp. +More complex examples with simulations of different scenarios can be found at: +- examples/lct_secir_initializations.cpp +- examples/lct_secir_fictional_scenario.cpp +- examples/lct_secir_fictional_scenario.cpp +The parameters used for simulations are calculated via examples/compute_parameters.cpp. diff --git a/cpp/models/lct_secir/infection_state.h b/cpp/models/lct_secir/infection_state.h new file mode 100644 index 0000000000..2930f718a5 --- /dev/null +++ b/cpp/models/lct_secir/infection_state.h @@ -0,0 +1,229 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#ifndef LCTSECIR_INFECTIONSTATE_H +#define LCTSECIR_INFECTIONSTATE_H + +#include "memilio/utils/logging.h" +#include + +namespace mio +{ +namespace lsecir +{ + +/** + * @brief The InfectionStateBase enum describes the possible basic + * categories for the infection state of persons + */ +enum class InfectionStateBase +{ + Susceptible = 0, + Exposed = 1, + InfectedNoSymptoms = 2, + InfectedSymptoms = 3, + InfectedSevere = 4, + InfectedCritical = 5, + Recovered = 6, + Dead = 7, + Count = 8 +}; + +/** + * @brief The InfectionTransition enum describes the possible + * transitions of the infectious state of persons. + */ +enum class InfectionTransition +{ + SusceptibleToExposed = 0, + ExposedToInfectedNoSymptoms = 1, + InfectedNoSymptomsToInfectedSymptoms = 2, + InfectedNoSymptomsToRecovered = 3, + InfectedSymptomsToInfectedSevere = 4, + InfectedSymptomsToRecovered = 5, + InfectedSevereToInfectedCritical = 6, + InfectedSevereToRecovered = 7, + InfectedCriticalToDead = 8, + InfectedCriticalToRecovered = 9, + Count = 10 +}; + +class InfectionState +{ +public: + /** + * @brief Constructor for the InfectionState class. + * + * InfectionState class defines the possible InfectionState%s with the number of Subcompartments for the LCT model. + * With the default constructor, the class is defined without subcompartments, i.e. only the subdivision in InfectionStateBase + * is used. + */ + InfectionState() + : m_SubcompartmentNumbers(std::vector((int)InfectionStateBase::Count, 1)) + , m_SubcompartmentNumbersindexfirst(std::vector((int)InfectionStateBase::Count, 1)) + { + set_compartment_index(); + } + + /** + * @brief Constructor for the InfectionState class. + * + * InfectionState class defines the possible InfectionState%s with the number of Subcompartments for the LCT model. + * @param[in] SubcompartmentNumbers Vector which defines the number of Subcompartments for each infection state of InfectionStateBase. + */ + InfectionState(std::vector SubcompartmentNumbers) + : m_SubcompartmentNumbers(std::move(SubcompartmentNumbers)) + , m_SubcompartmentNumbersindexfirst(std::vector((int)InfectionStateBase::Count, 1)) + { + bool constraint_check = check_constraints(); + if (!constraint_check) { + set_compartment_index(); + } + } + + /** + * @brief Setter for the number of Subcompartments. + * + * @param[in] SubcompartmentNumbers Vector which defines the number of Subcompartments for each infection state of InfectionStateBase. + */ + void set_SubcompartmentNumbers(std::vector SubcompartmentNumbers) + { + m_SubcompartmentNumbers = SubcompartmentNumbers; + bool constraint_check = check_constraints(); + if (!constraint_check) { + set_compartment_index(); + } + } + + /** + * @brief Gets the number of subcompartments in an infection state. + * + * @param[in] infectionstatebase Infection state for which the number of subcompartments should be returned. + * @return Number of Subcompartments for infectionstatebase. + */ + int get_number(InfectionStateBase infectionstatebase) const + { + return m_SubcompartmentNumbers[(int)infectionstatebase]; + } + + /** + * @brief Gets the number of subcompartments in an infection state. + * + * @param[in] infectionstatebase Index of an infection state for which the number of subcompartments should be returned. + * @return Number of Subcompartments for infectionstatebase. + */ + int get_number(int infectionstatebaseindex) const + { + return m_SubcompartmentNumbers[infectionstatebaseindex]; + } + + /** + * @brief Gets the index of the first subcompartment in an vector with all subcompartments for an infection state. + * + * In a simulation, the number of individuals in the subcompartments are stored in vectors. + * Accordingly, the index in such a vector of the first subcompartment of an infection state is given. + * @param[in] infectionstatebase Infection state for which the index should be returned. + * @return Index of the first Subcompartment for a vector with one entry per subcompartment. + */ + int get_firstindex(InfectionStateBase infectionstatebase) const + { + return m_SubcompartmentNumbersindexfirst[(int)infectionstatebase]; + } + + /** + * @brief Gets the index of the first subcompartment in an vector with all subcompartments for an infection state. + * + * In a simulation, the number of individuals in the subcompartments are stored in vectors. + * Accordingly, the index in such a vector of the first subcompartment of an infection state is given. + * @param[in] infectionstatebase Index of an infection state for which the index of a vector should be returned. + * @return Index of the first Subcompartment for a vector with one entry per subcompartment. + */ + int get_firstindex(int infectionstatebaseindex) const + { + return m_SubcompartmentNumbersindexfirst[infectionstatebaseindex]; + } + + /** + * @brief Gets the total number of (sub-)compartments of infection states. + */ + int get_count() const + { + return m_Count; + } + + /** + * @brief Checks constraints on infection states. + * + * @return Returns true if one (or more) constraint(s) are not satisfied, otherwise false. + */ + bool check_constraints() const + { + if (!(m_SubcompartmentNumbers.size() == (int)InfectionStateBase::Count)) { + log_error("Vector for number of subcompartments has the wrong size."); + return true; + } + if (!(m_SubcompartmentNumbers[(int)InfectionStateBase::Susceptible] == 1)) { + log_error("Susceptible compartment can not have Subcompartments."); + return true; + } + if (!(m_SubcompartmentNumbers[(int)InfectionStateBase::Recovered] == 1)) { + log_error("Recovered compartment can not have Subcompartments."); + return true; + } + if (!(m_SubcompartmentNumbers[(int)InfectionStateBase::Dead] == 1)) { + log_error("Dead compartment can not have Subcompartments."); + return true; + } + for (int i = 0; i < (int)InfectionStateBase::Count; ++i) { + if (m_SubcompartmentNumbers[i] < 1) { + log_error("All compartments should have at least one Subcompartment."); + return true; + } + } + return false; + } + +private: + /** + * @brief Calculates Index of the first Subcompartment for a vector with one entry per subcompartment. + * + * Therefore the vector with number of subcompartments per infection state is used. + */ + void set_compartment_index() + { + int index = 0; + for (int i = 0; i < (int)(InfectionStateBase::Count); i++) { + m_SubcompartmentNumbersindexfirst[i] = index; + index = index + m_SubcompartmentNumbers[i]; + } + m_Count = index; + } + + std::vector + m_SubcompartmentNumbers; ///< Vector which defines the number of Subcompartments for each infection state of InfectionStateBase. + std::vector + m_SubcompartmentNumbersindexfirst; ///< Vector with Indexes for all infection states of the first Subcompartment for a vector with one entry per subcompartment. + int m_Count; ///< Total number of (sub-)compartments of infection states. +}; + +} // namespace lsecir +} // namespace mio + +#endif \ No newline at end of file diff --git a/cpp/models/lct_secir/initialization.cpp b/cpp/models/lct_secir/initialization.cpp new file mode 100644 index 0000000000..ef91aca09c --- /dev/null +++ b/cpp/models/lct_secir/initialization.cpp @@ -0,0 +1,164 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#include "lct_secir/initialization.h" +#include "lct_secir/infection_state.h" +#include "lct_secir/parameters.h" +#include "memilio/config.h" +#include "memilio/math/eigen.h" +#include "memilio/math/floating_point.h" +#include "memilio/utils/time_series.h" +#include "memilio/utils/logging.h" +#include "memilio/epidemiology/state_age_function.h" + +namespace mio +{ +namespace lsecir +{ + +bool Initializer::check_constraints() const +{ + if (!((int)InfectionTransition::Count == (int)m_flows.get_num_elements())) { + log_error("Initial condition size does not match Subcompartments."); + return true; + } + + if (!(floating_point_equal(0., m_flows.get_last_time(), 1.0, 1e-14))) { + log_error("Last time point in flows has to be 0."); + return true; + } + + for (int i = 1; i < m_flows.get_num_time_points(); i++) { + if (!(floating_point_equal(m_dt, m_flows.get_time(i) - m_flows.get_time(i - 1), 1.0, 1e-14))) { + log_error("Time points in flows have to be equidistant."); + return true; + } + } + + if (!(m_dt < 1)) { + log_warning("Step size was set very large. The result could be distorted."); + return true; + } + + return parameters.check_constraints(); +} + +Eigen::VectorXd Initializer::compute_compartment(InfectionStateBase base, Eigen::Index idx_incoming_flow, + ScalarType transition_rate) const +{ + int num_infectionstates = infectionState.get_number(base); + Eigen::VectorXd subcompartments(num_infectionstates); + // Initialize relevant density for the compartment base. + // For the first subcompartment a shape parameter of one is needed. + ErlangDensity erlang(1, 1. / (num_infectionstates * transition_rate)); + + // Initialize other relevant parameters. + ScalarType calc_time{0}; + Eigen::Index calc_time_index{0}; + ScalarType state_age{0}; + ScalarType sum{0}; + Eigen::Index num_time_points = m_flows.get_num_time_points(); + + // Calculate number of people in each subcomaprtment. + for (int j = 0; j < num_infectionstates; j++) { + // For subcompartment number j+1, shape parameter j+1 is needed. + erlang.set_parameter(j + 1); + + // Determine relevant calculation area and corresponding index. + calc_time = erlang.get_support_max(m_dt, 1e-6); + calc_time_index = (Eigen::Index)std::ceil(calc_time / m_dt) - 1; + + if (num_time_points < calc_time_index) { + log_error("Initialization failed. Not enough time points for the transitions are given. {} are needed but " + "just {} are given.", + calc_time_index, num_time_points); + subcompartments[j] = -1; + } + else { + + // Approximate integral with non-standard scheme. + for (Eigen::Index i = num_time_points - calc_time_index; i < num_time_points; i++) { + state_age = (num_time_points - i) * m_dt; + sum += erlang.eval(state_age) * m_flows[i][idx_incoming_flow]; + } + subcompartments[j] = 1 / (num_infectionstates * transition_rate) * sum; + } + sum = 0; + } + return subcompartments; +} + +Eigen::VectorXd Initializer::compute_initializationvector(ScalarType total_population, ScalarType deaths, + ScalarType total_confirmed_cases) const +{ + check_constraints(); + + int infectionStates_count = infectionState.get_count(); + Eigen::VectorXd init(infectionStates_count); + + //E + init.segment(infectionState.get_firstindex(InfectionStateBase::Exposed), + infectionState.get_number(InfectionStateBase::Exposed)) = + compute_compartment(InfectionStateBase::Exposed, Eigen::Index(InfectionTransition::SusceptibleToExposed), + 1 / parameters.get()); + //C + init.segment(infectionState.get_firstindex(InfectionStateBase::InfectedNoSymptoms), + infectionState.get_number(InfectionStateBase::InfectedNoSymptoms)) = + compute_compartment(InfectionStateBase::InfectedNoSymptoms, + Eigen::Index(InfectionTransition::ExposedToInfectedNoSymptoms), + 1 / parameters.get()); + //I + init.segment(infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms), + infectionState.get_number(InfectionStateBase::InfectedSymptoms)) = + compute_compartment(InfectionStateBase::InfectedSymptoms, + Eigen::Index(InfectionTransition::InfectedNoSymptomsToInfectedSymptoms), + 1 / parameters.get()); + //H + init.segment(infectionState.get_firstindex(InfectionStateBase::InfectedSevere), + infectionState.get_number(InfectionStateBase::InfectedSevere)) = + compute_compartment(InfectionStateBase::InfectedSevere, + Eigen::Index(InfectionTransition::InfectedSymptomsToInfectedSevere), + 1 / parameters.get()); + //U + init.segment(infectionState.get_firstindex(InfectionStateBase::InfectedCritical), + infectionState.get_number(InfectionStateBase::InfectedCritical)) = + compute_compartment(InfectionStateBase::InfectedCritical, + Eigen::Index(InfectionTransition::InfectedSevereToInfectedCritical), + 1 / parameters.get()); + //R + // Number of recovered is equal to the cumulative number of confirmed cases minus the number of people who are infected at the moment. + init[infectionStates_count - 2] = total_confirmed_cases - + init.segment(infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms), + infectionState.get_number(InfectionStateBase::InfectedSymptoms) + + infectionState.get_number(InfectionStateBase::InfectedSevere) + + infectionState.get_number(InfectionStateBase::InfectedCritical)) + .sum() - + deaths; + + //S + init[0] = total_population - init.segment(1, infectionStates_count - 2).sum() - deaths; + + //D + init[infectionStates_count - 1] = deaths; + + return init; +} + +} // namespace lsecir +} // namespace mio \ No newline at end of file diff --git a/cpp/models/lct_secir/initialization.h b/cpp/models/lct_secir/initialization.h new file mode 100644 index 0000000000..627f1793ba --- /dev/null +++ b/cpp/models/lct_secir/initialization.h @@ -0,0 +1,103 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#ifndef LCTSECIR_INITIALIZATION_H +#define LCTSECIR_INITIALIZATION_H + +#include "memilio/config.h" +#include "lct_secir/infection_state.h" +#include "memilio/math/eigen.h" +#include "memilio/utils/time_series.h" +#include "lct_secir/parameters.h" + +namespace mio +{ +namespace lsecir +{ + +/** + * @brief Class that can be used to compute an initialization vector out of flows for a LCT Model. + */ +class Initializer +{ +public: + /** + * @brief Constructs a new Initializer object. + * + * @param[in] flows Initalizing TimeSeries with flows fitting to these defined in InfectionTransition. + * Timesteps should be equidistant. + * @param[in, out] infectionState_init InfectionState%s for the Initializer, specifies number of Subcompartments for each infection state. + * @param[in, out] parameters_init Specifies Parameters necessary for the Initializer. + */ + Initializer(TimeSeries&& flows, InfectionState infectionState_init = InfectionState(), + Parameters&& parameters_init = Parameters()) + : parameters(parameters_init) + , infectionState(infectionState_init) + , m_flows(std::move(flows)) + { + m_dt = m_flows.get_time(1) - m_flows.get_time(0); + } + + /** + * @brief Core function of Initializer. + * + * Computes a vector that can be used for the initalization of an LCT model with the number of persons for each subcompartment. + * + * @param[in] total_population The total size of the considered population. + * @param[in] deaths Number of deceased people from the disease at time 0. + * @param[in] total_confirmed_cases Total number of confirmed cases at time 0. + * @return Vector with a possible initialization for an LCT model computed out of the flows. + * A subcompartment is set to -1 if calculation was not possible. + */ + Eigen::VectorXd compute_initializationvector(ScalarType total_population, ScalarType deaths, + ScalarType total_confirmed_cases) const; + + /** + * @brief Checks constraints of the Initializer inclusive check for parameters. + * @return Returns true if one (or more) constraint(s) are not satisfied, otherwise false. + */ + bool check_constraints() const; + + Parameters parameters{}; ///< Parameters with the Initalizers Parameters. + InfectionState infectionState; ///< InfectionState specifies number of subcompartments. + +private: + /** + * @brief Computes a vector with the number of people in each compartment for one InfectionStateBase. + * + * With this function, partial result of compute_initializationvector are achieved. + * + * @param[in] base The InfectionStateBase for which the partial result should be calculated. + * @param[in] idx_incoming_flow Index of the flow which is relevant for the calculation, so the flow to the InfectionStateBase base. + * @param[in] transition_rate Specifies the transition rate of the InfectionsStateBase. Is equal to 1 / (expected Time in base). + * @return Vector with a possible initialization for the subcompartments of base. + * Subcompartment is set to -1 if calculation was not possible. + */ + Eigen::VectorXd compute_compartment(InfectionStateBase base, Eigen::Index idx_incoming_flow, + ScalarType transition_rate) const; + + TimeSeries m_flows; ///< TimeSeries with the flows which are used to calculate the initial vector. + ScalarType m_dt{}; ///< Step size of the times in m_flows and time step for the approximation of the integral. +}; + +} // namespace lsecir +} // namespace mio + +#endif \ No newline at end of file diff --git a/cpp/models/lct_secir/model.cpp b/cpp/models/lct_secir/model.cpp new file mode 100755 index 0000000000..fb145a0ef4 --- /dev/null +++ b/cpp/models/lct_secir/model.cpp @@ -0,0 +1,252 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#include "lct_secir/model.h" +#include "infection_state.h" +#include "memilio/config.h" +#include "memilio/math/eigen.h" +#include "memilio/utils/logging.h" +#include "memilio/math/eigen.h" +#include + +namespace mio +{ +namespace lsecir +{ + +Model::Model(Eigen::VectorXd init, const InfectionState infectionState_init, Parameters&& parameters_init) + : parameters{parameters_init} + , infectionState{infectionState_init} + , m_initial_values{std::move(init)} +{ + m_N0 = m_initial_values.sum(); +} + +bool Model::check_constraints() const +{ + if (!(infectionState.get_count() == m_initial_values.size())) { + log_error("Size of the initial values does not match Subcompartments."); + return true; + } + for (int i = 0; i < infectionState.get_count(); i++) { + if (m_initial_values[i] < 0) { + log_warning( + "Initial values for one subcompartment are less than zero. Simulation results are not realistic."); + return true; + } + } + return parameters.check_constraints(); +} + +void Model::eval_right_hand_side(Eigen::Ref y, ScalarType t, + Eigen::Ref dydt) const +{ + dydt.setZero(); + + ScalarType C = 0; + ScalarType I = 0; + ScalarType dummy = 0; + + // Calculate sum of all subcompartments for InfectedNoSymptoms. + C = y.segment(infectionState.get_firstindex(InfectionStateBase::InfectedNoSymptoms), + infectionState.get_number(InfectionStateBase::InfectedNoSymptoms)) + .sum(); + // Calculate sum of all subcompartments for InfectedSymptoms. + I = y.segment(infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms), + infectionState.get_number(InfectionStateBase::InfectedSymptoms)) + .sum(); + + // S' + ScalarType season_val = + 1 + parameters.get() * + sin(3.141592653589793 * (std::fmod((parameters.get() + t), 365.0) / 182.5 + 0.5)); + dydt[0] = + -y[0] / (m_N0 - y[infectionState.get_firstindex(InfectionStateBase::Dead)]) * season_val * + parameters.get() * + parameters.get().get_cont_freq_mat().get_matrix_at(t)(0, 0) * + (parameters.get() * C + parameters.get() * I); + + // E' + dydt[1] = -dydt[0]; + for (int i = 0; i < infectionState.get_number(InfectionStateBase::Exposed); i++) { + // Dummy stores the value of the flow from dydt[1 + i] to dydt[2 + i]. + // 1+i is always the index of a (sub-)compartment of E and 2+i can also be the index of the first (sub-)compartment of C. + dummy = infectionState.get_number(InfectionStateBase::Exposed) * (1 / parameters.get()) * y[1 + i]; + // Subtract flow from dydt[1 + i] and add to dydt[2 + i]. + dydt[1 + i] = dydt[1 + i] - dummy; + dydt[2 + i] = dummy; + } + + // C' + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedNoSymptoms); i++) { + dummy = infectionState.get_number(InfectionStateBase::InfectedNoSymptoms) * + (1 / parameters.get()) * + y[infectionState.get_firstindex(InfectionStateBase::InfectedNoSymptoms) + i]; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedNoSymptoms) + i] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedNoSymptoms) + i] - dummy; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedNoSymptoms) + i + 1] = dummy; + } + + // I' + // Flow from last (sub-) compartment of C must be split between I_1 and R. + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered)] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms)] * + parameters.get(); + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms)] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms)] * + (1 - parameters.get()); + + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedSymptoms); i++) { + dummy = infectionState.get_number(InfectionStateBase::InfectedSymptoms) * + (1 / parameters.get()) * + y[infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms) + i]; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms) + i] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms) + i] - dummy; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms) + i + 1] = dummy; + } + + // H' + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered)] = + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered)] + + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSevere)] * + (1 - parameters.get()); + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSevere)] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSevere)] * + parameters.get(); + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedSevere); i++) { + dummy = infectionState.get_number(InfectionStateBase::InfectedSevere) * + (1 / parameters.get()) * + y[infectionState.get_firstindex(InfectionStateBase::InfectedSevere) + i]; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSevere) + i] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSevere) + i] - dummy; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedSevere) + i + 1] = dummy; + } + + // U' + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered)] = + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered)] + + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedCritical)] * + (1 - parameters.get()); + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedCritical)] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedCritical)] * parameters.get(); + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedCritical) - 1; i++) { + dummy = infectionState.get_number(InfectionStateBase::InfectedCritical) * + (1 / parameters.get()) * + y[infectionState.get_firstindex(InfectionStateBase::InfectedCritical) + i]; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedCritical) + i] = + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedCritical) + i] - dummy; + dydt[infectionState.get_firstindex(InfectionStateBase::InfectedCritical) + i + 1] = dummy; + } + // Last flow from U has to be divided between R and D. + // Must be calculated separately in order not to overwrite the already calculated values ​​for R. + dummy = infectionState.get_number(InfectionStateBase::InfectedCritical) * + (1 / parameters.get()) * + y[infectionState.get_firstindex(InfectionStateBase::Recovered) - 1]; + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered) - 1] = + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered) - 1] - dummy; + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered)] = + dydt[infectionState.get_firstindex(InfectionStateBase::Recovered)] + + (1 - parameters.get()) * dummy; + dydt[infectionState.get_firstindex(InfectionStateBase::Dead)] = parameters.get() * dummy; +} + +TimeSeries Model::calculate_populations(const TimeSeries& result) const +{ + if (!(infectionState.get_count() == result.get_num_elements())) { + log_error("Result does not match infectionState of the Model."); + TimeSeries populations((int)InfectionStateBase::Count); + Eigen::VectorXd wrong_size = Eigen::VectorXd::Constant((int)InfectionStateBase::Count, -1); + populations.add_time_point(-1, wrong_size); + return populations; + } + TimeSeries populations((int)InfectionStateBase::Count); + Eigen::VectorXd dummy((int)InfectionStateBase::Count); + for (Eigen::Index i = 0; i < result.get_num_time_points(); ++i) { + for (int j = 0; j < dummy.size(); ++j) { + // Use segment of vector of the rsult with subcompartments of InfectionStateBase with index j and sum up values of subcompartments. + dummy[j] = + result[i] + .segment(Eigen::Index(infectionState.get_firstindex(j)), Eigen::Index(infectionState.get_number(j))) + .sum(); + } + populations.add_time_point(result.get_time(i), dummy); + } + + return populations; +} + +std::string Model::get_heading_CompartmentsBase() const +{ + return "S | E | C | I | H | U | R | D"; +} + +std::string Model::get_heading_Subcompartments() const +{ + + std::string heading = "S"; + std::string filler = " | "; + // If E has subcompartments in the model, append E1 to En to the heading. + if (infectionState.get_number(InfectionStateBase::Exposed) > 1) { + for (int i = 0; i < infectionState.get_number(InfectionStateBase::Exposed); i++) { + heading = heading + filler + "E" + std::to_string(i + 1); + } + } + else { + // If E does not have subcompartments in the model, just append E. + heading = heading + filler + "E"; + } + if (infectionState.get_number(InfectionStateBase::InfectedNoSymptoms) > 1) { + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedNoSymptoms); i++) { + heading = heading + filler + "C" + std::to_string(i + 1); + } + } + else { + heading = heading + filler + "C"; + } + if (infectionState.get_number(InfectionStateBase::InfectedSymptoms) > 1) { + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedSymptoms); i++) { + heading = heading + filler + "I" + std::to_string(i + 1); + } + } + else { + heading = heading + filler + "I"; + } + if (infectionState.get_number(InfectionStateBase::InfectedSevere) > 1) { + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedSevere); i++) { + heading = heading + filler + "H" + std::to_string(i + 1); + } + } + else { + heading = heading + filler + "H"; + } + if (infectionState.get_number(InfectionStateBase::InfectedCritical) > 1) { + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedCritical); i++) { + heading = heading + filler + "U" + std::to_string(i + 1); + } + } + else { + heading = heading + filler + "U"; + } + heading = heading + filler + "R"; + heading = heading + filler + "D"; + return heading; +} + +} // namespace lsecir +} // namespace mio diff --git a/cpp/models/lct_secir/model.h b/cpp/models/lct_secir/model.h new file mode 100644 index 0000000000..955ca96a9e --- /dev/null +++ b/cpp/models/lct_secir/model.h @@ -0,0 +1,118 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#ifndef LCT_SECIR_MODEL_H +#define LCT_SECIR_MODEL_H + +#include "lct_secir/parameters.h" +#include "lct_secir/infection_state.h" +#include "memilio/config.h" +#include "memilio/utils/time_series.h" +#include "memilio/math/eigen.h" +#include + +namespace mio +{ +namespace lsecir +{ +class Model +{ + +public: + /** + * @brief Constructor to create an LCT SECIR Model. + * + * @param[in] init Vector with initial values for all infection states inclusive subcompartments. + * @param[in, out] infectionState_init infectionState for the Model, specifies number of Subcompartments for each infection state. + * @param[in, out] parameters_init Specifies Parameters necessary for the Model. + */ + Model(Eigen::VectorXd init, InfectionState infectionState_init = InfectionState(), + Parameters&& parameters_init = Parameters()); + + /** + * @brief Checks constraints of the model inclusive check for model parameters. + */ + bool check_constraints() const; + + /** + * @brief Evaulates the right-hand-side f of the LCT dydt = f(y, t). + * + * The LCT-SECIR model is defined through ordinary differetial equations of the form dydt = f(y, t). + * y is a vector containing number of individuals for each (sub-) compartment. + * This function evaluates the right-hand-side f of the ODE and can be used in an ODE solver. + * @param[in] y the current state of the model + * @param[in] t the current time + * @param[out] dydt a reference to the calculated output + */ + void eval_right_hand_side(Eigen::Ref y, ScalarType t, + Eigen::Ref dydt) const; + + /** + * @brief Calculates the population divided in states defined in InfectionStateBase out of a result with subcompartments. + * + * If the model is used for simulation, we will get a result in form of a TimeSeries with infection states divided in Subcompartments. + * Function transforms this TimeSeries in another TimeSeries with just the Basic infectionState. + * This is done by summing up the numbers in the Subcompartments. + * @param[in] result result of a simulation with the model. + * @return result of the simulation divided in the Base infection states. + * Returns TimeSeries with values -1 if calculation is not possible. + */ + TimeSeries calculate_populations(const TimeSeries& result) const; + + /** + * @brief Gives a string with the names of base infection states. + * + * Can be used as a heading for printing the result without subcompartments. + * @return heading in form of a string. + */ + std::string get_heading_CompartmentsBase() const; + + /** + * @brief Gives a string with the names of infection stateswith subcompartmens. + * + * Can be used as a heading for printing the result with subcompartments. + * @return heading in form of a string. + */ + std::string get_heading_Subcompartments() const; + + /** + * @brief Returns the initial values for the model. + * + * This can be used as initial conditions in an ODE solver. + * @return Vector with initial values for all (sub-)compartments. + */ + Eigen::VectorXd get_initial_values() + { + return m_initial_values; + } + + Parameters parameters{}; ///< Parameters of Model Parameters. + InfectionState infectionState; ///< InfectionState specifies number of subcompartments. + +private: + Eigen::VectorXd m_initial_values; ///< Initial values of the model. + ScalarType m_N0{ + 0}; ///< Total population size at time t_0 for the considered region (inclusive initial value for Dead). +}; + +} // namespace lsecir +} // namespace mio + +#endif // LCTSECIR_MODEL_H diff --git a/cpp/models/lct_secir/parameters.h b/cpp/models/lct_secir/parameters.h new file mode 100755 index 0000000000..a72ad20ac1 --- /dev/null +++ b/cpp/models/lct_secir/parameters.h @@ -0,0 +1,391 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#ifndef LCT_SECIR_PARAMS_H +#define LCT_SECIR_PARAMS_H + +#include "memilio/config.h" +#include "memilio/utils/parameter_set.h" +#include "memilio/math/eigen.h" +#include "memilio/epidemiology/uncertain_matrix.h" +#include "memilio/utils/logging.h" + +namespace mio +{ +namespace lsecir +{ + +/********************************************** +* Define Parameters of the LCT-SECIHURD model * +**********************************************/ + +/** + * @brief Average Time spent in the Exposed compartment. + */ +struct TimeExposed { + using Type = ScalarType; + static Type get_default() + { + return 2.0; + } + static std::string name() + { + return "TimeExposed"; + } +}; + +/** + * @brief Average time spent in the TimeInfectedNoSymptoms before developing + * Symptoms or recover in the SECIR model in day unit. + */ +struct TimeInfectedNoSymptoms { + using Type = ScalarType; + static Type get_default() + { + return 1.0; + } + static std::string name() + { + return "TimeInfectedNoSymptoms"; + } +}; + +/** + * @brief Average time spent in the TimeInfectedSymptoms before going to Hospital + * or recover in the SECIR model in day unit. + */ +struct TimeInfectedSymptoms { + using Type = ScalarType; + static Type get_default() + { + return 1.5; + } + static std::string name() + { + return "TimeInfectedNoSymptoms"; + } +}; + +/** + * @brief Average time being in the Hospital before treated by ICU or recover in the + * SECIR model in day unit. + */ +struct TimeInfectedSevere { + using Type = ScalarType; + static Type get_default() + { + return 1.0; + } + static std::string name() + { + return "TimeInfectedSevere"; + } +}; + +/** + * @brief Average time treated by ICU before dead or recover in the SECIR model in day unit. + */ +struct TimeInfectedCritical { + using Type = ScalarType; + static Type get_default() + { + return 1.0; + } + static std::string name() + { + return "TimeInfectedCritical"; + } +}; + +/** + * @brief Probability of getting infected from a contact. + */ +struct TransmissionProbabilityOnContact { + using Type = ScalarType; + static Type get_default() + { + return 1.0; + } + static std::string name() + { + return "TransmissionProbabilityOnContact"; + } +}; + +/** + * @brief The contact patterns within the society are modelled using an UncertainContactMatrix. + */ +struct ContactPatterns { + using Type = UncertainContactMatrix; + + static Type get_default() + { + ContactMatrixGroup contact_matrix = ContactMatrixGroup(1, 1); + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10.)); + return Type(contact_matrix); + } + static std::string name() + { + return "ContactPatterns"; + } +}; + +/** + * @brief The relative InfectedNoSymptoms infectability. + */ +struct RelativeTransmissionNoSymptoms { + using Type = ScalarType; + static Type get_default() + { + return 0.5; + } + static std::string name() + { + return "RelativeTransmissionNoSymptoms"; + } +}; + +/** + * @brief The risk of infection from symptomatic cases in the SECIR model. + */ +struct RiskOfInfectionFromSymptomatic { + using Type = ScalarType; + static Type get_default() + { + return 0.5; + } + static std::string name() + { + return "RiskOfInfectionFromSymptomatic"; + } +}; + +/** + * @brief The percentage of asymptomatic cases in the SECIR model. + */ +struct RecoveredPerInfectedNoSymptoms { + using Type = ScalarType; + static Type get_default() + { + return 0.5; + } + static std::string name() + { + return "RecoveredPerInfectedNoSymptoms"; + } +}; + +/** + * @brief The percentage of hospitalized patients per infected patients in the SECIR model. + */ +struct SeverePerInfectedSymptoms { + using Type = ScalarType; + static Type get_default() + { + return 0.5; + } + static std::string name() + { + return "SeverePerInfectedSymptoms"; + } +}; + +/** + * @brief The percentage of ICU patients per hospitalized patients in the SECIR model. + */ +struct CriticalPerSevere { + using Type = ScalarType; + static Type get_default() + { + return 0.5; + } + static std::string name() + { + return "CriticalPerSevere"; + } +}; + +/** + * @brief The percentage of dead patients per ICU patients in the SECIR model. + */ +struct DeathsPerCritical { + using Type = ScalarType; + static Type get_default() + { + return 0.1; + } + static std::string name() + { + return "DeathsPerCritical"; + } +}; + +/** + * @brief The start day in the LCT SECIR model. + * The start day defines in which season the simulation is started. + * If the start day is 180 and simulation takes place from t0=0 to + * tmax=100 the days 180 to 280 of the year are simulated. + */ +struct StartDay { + using Type = ScalarType; + static Type get_default() + { + return 0.; + } + static std::string name() + { + return "StartDay"; + } +}; + +/** + * @brief The seasonality in the LCT-SECIR model. + * The seasonality is given as (1+k*sin()) where the sine + * curve is below one in summer and above one in winter. + */ +struct Seasonality { + using Type = ScalarType; + static Type get_default() + { + return Type(0.); + } + static std::string name() + { + return "Seasonality"; + } +}; + +using ParametersBase = + ParameterSet; + +/** + * @brief Parameters of an LCT-SECIR model. + */ +class Parameters : public ParametersBase +{ +public: + /** + * @brief Default constructor. + */ + Parameters() + : ParametersBase() + { + } + + /** + * @brief checks whether all Parameters satisfy their corresponding constraints and throws errors, if they do not. + * @return Returns true if one (or more) constraint(s) are not satisfied, otherwise false. + */ + bool check_constraints() const + { + if (this->get() < 1.0) { + log_error("Constraint check: Parameter TimeExposed is smaller than {:.4f}", 1.0); + return true; + } + + if (this->get() < 1.0) { + log_error("Constraint check: Parameter TimeInfectedNoSymptoms is smaller than {:.4f}", 1.0); + return true; + } + + if (this->get() < 1.0) { + log_error("Constraint check: Parameter TimeInfectedSymptoms is smaller than {:.4f}", 1.0); + return true; + } + + if (this->get() < 1.0) { + log_error("Constraint check: Parameter TimeInfectedSevere is smaller than {:.4f}", 1.0); + return true; + } + + if (this->get() < 1.0) { + log_error("Constraint check: Parameter TimeInfectedCritical is smaller than {:.4f}", 1.0); + return true; + } + + if (this->get() < 0.0 || + this->get() > 1.0) { + log_error("Constraint check: Parameter TransmissionProbabilityOnContact smaller {:d} or larger {:d}", 0, 1); + return true; + } + + if (this->get() < 0.0 || this->get() > 1.0) { + log_error("Constraint check: Parameter RelativeTransmissionNoSymptoms smaller {:d} or larger {:d}", 0, 1); + return true; + } + + if (this->get() < 0.0 || this->get() > 1.0) { + log_error("Constraint check: Parameter RiskOfInfectionFromSymptomatic smaller {:d} or larger {:d}", 0, 1); + return true; + } + + if (this->get() < 0.0 || this->get() > 1.0) { + log_error("Constraint check: Parameter RecoveredPerInfectedNoSymptoms smaller {:d} or larger {:d}", 0, 1); + return true; + } + + if (this->get() < 0.0 || this->get() > 1.0) { + log_error("Constraint check: Parameter SeverePerInfectedSymptoms smaller {:d} or larger {:d}", 0, 1); + return true; + } + + if (this->get() < 0.0 || this->get() > 1.0) { + log_error("Constraint check: Parameter CriticalPerSevere smaller {:d} or larger {:d}", 0, 1); + return true; + } + + if (this->get() < 0.0 || this->get() > 1.0) { + log_error("Constraint check: Parameter DeathsPerCritical smaller {:d} or larger {:d}", 0, 1); + return true; + } + + if (this->get() < 0.0 || this->get() > 0.5) { + log_warning("Constraint check: Parameter Seasonality should lie between {:0.4f} and {:.4f}", 0.0, 0.5); + return true; + } + + return false; + } + +private: + Parameters(ParametersBase&& base) + : ParametersBase(std::move(base)) + { + } + +public: + /** + * deserialize an object of this class. + * @see mio::deserialize + */ + template + static IOResult deserialize(IOContext& io) + { + BOOST_OUTCOME_TRY(base, ParametersBase::deserialize(io)); + return success(Parameters(std::move(base))); + } +}; + +} // namespace lsecir +} // namespace mio + +#endif // LCT_SECIR_PARAMS_H \ No newline at end of file diff --git a/cpp/models/lct_secir/parameters_io.cpp b/cpp/models/lct_secir/parameters_io.cpp new file mode 100644 index 0000000000..61b4253910 --- /dev/null +++ b/cpp/models/lct_secir/parameters_io.cpp @@ -0,0 +1,275 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "lct_secir/parameters_io.h" +#include "memilio/config.h" + +#ifdef MEMILIO_HAS_JSONCPP + +#include "lct_secir/infection_state.h" +#include "lct_secir/parameters.h" +#include "memilio/io/epi_data.h" +#include "memilio/io/io.h" +#include "memilio/utils/date.h" +#include "memilio/utils/logging.h" +#include "memilio/math/eigen.h" + +#include + +namespace mio +{ +namespace lsecir +{ + +IOResult get_initial_data_from_file(std::string const& path, Date date, InfectionState infectionState, + Parameters&& parameters, ScalarType total_population, + ScalarType scale_confirmed_cases) +{ + // Try to get rki data from path. + BOOST_OUTCOME_TRY(rki_data, mio::read_confirmed_cases_noage(path)); + auto max_date_entry = std::max_element(rki_data.begin(), rki_data.end(), [](auto&& a, auto&& b) { + return a.date < b.date; + }); + if (max_date_entry == rki_data.end()) { + log_error("RKI data file is empty."); + return failure(StatusCode::InvalidFileFormat, path + ", file is empty."); + } + auto max_date = max_date_entry->date; + if (max_date < date) { + log_error("Specified date does not exist in RKI data."); + return failure(StatusCode::OutOfRange, path + ", specified date does not exist in RKI data."); + } + // Compute initial values for all subcompartments. + Eigen::VectorXd init = Eigen::VectorXd::Zero(infectionState.get_count()); + // Define variables for parameters that are often needed. + ScalarType timeExposed = parameters.get(); + ScalarType timeInfectedNoSymptoms = parameters.get(); + ScalarType timeInfectedSymptoms = parameters.get(); + ScalarType timeInfectedSevere = parameters.get(); + ScalarType timeInfectedCritical = parameters.get(); + ScalarType scale_mu_CR = 1 / (1 - parameters.get()); + + ScalarType min_offset_needed = std::floor(-timeInfectedSymptoms - timeInfectedSevere - timeInfectedCritical); + ScalarType max_offset_needed = std::ceil(timeExposed + timeInfectedNoSymptoms); + + bool min_offset_needed_avail = false; + bool max_offset_needed_avail = false; + + // Go through the entries of rki_data and check if date is needed for calculation. Confirmed cases are scaled. + for (auto&& entry : rki_data) { + auto offset = get_offset_in_days(entry.date, date); + if (!(offset < min_offset_needed) || !(offset > max_offset_needed)) { + // Add confirmed cases at date to compartment Recovered. + if (offset == 0) { + init[infectionState.get_firstindex(InfectionStateBase::Recovered)] += + scale_confirmed_cases * entry.num_confirmed; + } + + // Compute initial values for compartment InfectedNoSymptoms. + if (offset >= 0 && offset <= std::ceil(timeInfectedNoSymptoms)) { + ScalarType TCi = + timeInfectedNoSymptoms / infectionState.get_number(InfectionStateBase::InfectedNoSymptoms); + int idx_Cn = infectionState.get_firstindex(InfectionStateBase::InfectedNoSymptoms) + + infectionState.get_number(InfectionStateBase::InfectedNoSymptoms) - 1; + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedNoSymptoms); i++) { + if (offset == std::floor(i * TCi)) { + init[idx_Cn - i] -= (1 - (i * TCi - std::floor(i * TCi))) * scale_mu_CR * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(i * TCi)) { + init[idx_Cn - i] -= + (i * TCi - std::floor(i * TCi)) * scale_mu_CR * scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::floor((i + 1) * TCi)) { + init[idx_Cn - i] += (1 - ((i + 1) * TCi - std::floor((i + 1) * TCi))) * scale_mu_CR * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil((i + 1) * TCi)) { + init[idx_Cn - i] += ((i + 1) * TCi - std::floor((i + 1) * TCi)) * scale_mu_CR * + scale_confirmed_cases * entry.num_confirmed; + } + } + } + + // Compute initial values for compartment Exposed. + if (offset >= std::floor(timeInfectedNoSymptoms) && offset <= max_offset_needed) { + ScalarType TEi = timeExposed / infectionState.get_number(InfectionStateBase::Exposed); + int idx_En = infectionState.get_firstindex(InfectionStateBase::Exposed) + + infectionState.get_number(InfectionStateBase::Exposed) - 1; + for (int i = 0; i < infectionState.get_number(InfectionStateBase::Exposed); i++) { + if (offset == std::floor(timeInfectedNoSymptoms + i * TEi)) { + init[idx_En - i] -= + (1 - (timeInfectedNoSymptoms + i * TEi - std::floor(timeInfectedNoSymptoms + i * TEi))) * + scale_mu_CR * scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(timeInfectedNoSymptoms + i * TEi)) { + init[idx_En - i] -= + (timeInfectedNoSymptoms + i * TEi - std::floor(timeInfectedNoSymptoms + i * TEi)) * + scale_mu_CR * scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::floor(timeInfectedNoSymptoms + (i + 1) * TEi)) { + init[idx_En - i] += (1 - (timeInfectedNoSymptoms + (i + 1) * TEi - + std::floor(timeInfectedNoSymptoms + (i + 1) * TEi))) * + scale_mu_CR * scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(timeInfectedNoSymptoms + (i + 1) * TEi)) { + init[idx_En - i] += (timeInfectedNoSymptoms + (i + 1) * TEi - + std::floor(timeInfectedNoSymptoms + (i + 1) * TEi)) * + scale_mu_CR * scale_confirmed_cases * entry.num_confirmed; + } + } + } + + // Compute initial values for compartment InfectedSymptoms. + if (offset >= std::floor(-timeInfectedSymptoms) && offset <= 0) { + ScalarType TIi = timeInfectedSymptoms / infectionState.get_number(InfectionStateBase::InfectedSymptoms); + int idx_I1 = infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms); + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedSymptoms); i++) { + if (offset == std::floor(-TIi * (i + 1))) { + init[idx_I1 + i] -= (1 - (-(i + 1) * TIi - std::floor(-(i + 1) * TIi))) * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(-TIi * (i + 1))) { + init[idx_I1 + i] -= + (-(i + 1) * TIi - std::floor(-(i + 1) * TIi)) * scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::floor(-TIi * i)) { + init[idx_I1 + i] += + (1 - (-i * TIi - std::floor(-i * TIi))) * scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(-TIi * i)) { + init[idx_I1 + i] += + (-i * TIi - std::floor(-i * TIi)) * scale_confirmed_cases * entry.num_confirmed; + } + } + } + + // Compute initial values for compartment InfectedSevere. + if (offset >= std::floor(-timeInfectedSymptoms - timeInfectedSevere) && + offset <= std::ceil(-timeInfectedSymptoms)) { + ScalarType THi = timeInfectedSevere / infectionState.get_number(InfectionStateBase::InfectedSevere); + ScalarType mu_IH = parameters.get(); + int idx_H1 = infectionState.get_firstindex(InfectionStateBase::InfectedSevere); + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedSevere); i++) { + if (offset == std::floor(-timeInfectedSymptoms - THi * (i + 1))) { + init[idx_H1 + i] -= mu_IH * + (1 - (-timeInfectedSymptoms - (i + 1) * THi - + std::floor(-timeInfectedSymptoms - (i + 1) * THi))) * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(-timeInfectedSymptoms - THi * (i + 1))) { + init[idx_H1 + i] -= mu_IH * + (-timeInfectedSymptoms - (i + 1) * THi - + std::floor(-timeInfectedSymptoms - (i + 1) * THi)) * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::floor(-timeInfectedSymptoms - THi * i)) { + init[idx_H1 + i] += + mu_IH * + (1 - (-timeInfectedSymptoms - i * THi - std::floor(-timeInfectedSymptoms - i * THi))) * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(-timeInfectedSymptoms - THi * i)) { + init[idx_H1 + i] += + mu_IH * (-timeInfectedSymptoms - i * THi - std::floor(-timeInfectedSymptoms - i * THi)) * + scale_confirmed_cases * entry.num_confirmed; + } + } + } + + // Compute initial values for compartment InfectedCritical. + if (offset >= min_offset_needed && offset <= std::ceil(-timeInfectedSymptoms - timeInfectedSevere)) { + ScalarType TUi = timeInfectedCritical / infectionState.get_number(InfectionStateBase::InfectedCritical); + ScalarType mu_IH = parameters.get(); + ScalarType mu_HU = parameters.get(); + int idx_U1 = infectionState.get_firstindex(InfectionStateBase::InfectedCritical); + for (int i = 0; i < infectionState.get_number(InfectionStateBase::InfectedCritical); i++) { + if (offset == std::floor(-timeInfectedSymptoms - timeInfectedSevere - TUi * (i + 1))) { + init[idx_U1 + i] -= + mu_IH * mu_HU * + (1 - (-timeInfectedSymptoms - timeInfectedSevere - (i + 1) * TUi - + std::floor(-timeInfectedSymptoms - timeInfectedSevere - (i + 1) * TUi))) * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(-timeInfectedSymptoms - timeInfectedSevere - TUi * (i + 1))) { + init[idx_U1 + i] -= mu_IH * mu_HU * + (-timeInfectedSymptoms - timeInfectedSevere - (i + 1) * TUi - + std::floor(-timeInfectedSymptoms - timeInfectedSevere - (i + 1) * TUi)) * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::floor(-timeInfectedSymptoms - timeInfectedSevere - TUi * i)) { + init[idx_U1 + i] += mu_IH * mu_HU * + (1 - (-timeInfectedSymptoms - timeInfectedSevere - i * TUi - + std::floor(-timeInfectedSymptoms - timeInfectedSevere - i * TUi))) * + scale_confirmed_cases * entry.num_confirmed; + } + if (offset == std::ceil(-timeInfectedSymptoms - timeInfectedSevere - TUi * i)) { + init[idx_U1 + i] += mu_IH * mu_HU * + (-timeInfectedSymptoms - timeInfectedSevere - i * TUi - + std::floor(-timeInfectedSymptoms - timeInfectedSevere - i * TUi)) * + scale_confirmed_cases * entry.num_confirmed; + } + } + } + + // Compute Dead. + if (offset == min_offset_needed) { + min_offset_needed_avail = true; + init[infectionState.get_firstindex(InfectionStateBase::Dead)] += + (1 - (-timeInfectedSymptoms - timeInfectedSevere - timeInfectedCritical - + std::floor(-timeInfectedSymptoms - timeInfectedSevere - timeInfectedCritical))) * + entry.num_deaths; + } + if (offset == std::ceil(-timeInfectedSymptoms - timeInfectedSevere - timeInfectedCritical)) { + init[infectionState.get_firstindex(InfectionStateBase::Dead)] += + (-timeInfectedSymptoms - timeInfectedSevere - timeInfectedCritical - + std::floor(-timeInfectedSymptoms - timeInfectedSevere - timeInfectedCritical)) * + entry.num_deaths; + } + if (offset == max_offset_needed) { + max_offset_needed_avail = true; + } + } + } + + // Compute Recovered. + init[infectionState.get_firstindex(InfectionStateBase::Recovered)] -= + init.segment(infectionState.get_firstindex(InfectionStateBase::InfectedSymptoms), + infectionState.get_number(InfectionStateBase::InfectedSymptoms) + + infectionState.get_number(InfectionStateBase::InfectedSevere) + + infectionState.get_number(InfectionStateBase::InfectedCritical)) + .sum(); + init[infectionState.get_firstindex(InfectionStateBase::Recovered)] -= + init[infectionState.get_firstindex(InfectionStateBase::Dead)]; + + // Compute Susceptibles + init[infectionState.get_firstindex(InfectionStateBase::Susceptible)] = + total_population - init.segment(1, infectionState.get_count() - 1).sum(); + if (!max_offset_needed_avail || !min_offset_needed_avail) { + log_error("Necessary range of dates needed to compute initial values does not exist in RKI data."); + return failure(StatusCode::OutOfRange, path + ", necessary range of dates does not exist in RKI data."); + } + + return init; +} + +} // namespace lsecir +} // namespace mio +#endif // MEMILIO_HAS_JSONCPP \ No newline at end of file diff --git a/cpp/models/lct_secir/parameters_io.h b/cpp/models/lct_secir/parameters_io.h new file mode 100644 index 0000000000..97583926b9 --- /dev/null +++ b/cpp/models/lct_secir/parameters_io.h @@ -0,0 +1,66 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#ifndef LCTSECIR_PARAMETERS_IO_H +#define LCTSECIR_PARAMETERS_IO_H + +#include "memilio/config.h" + +#ifdef MEMILIO_HAS_JSONCPP + +#include "lct_secir/parameters.h" +#include "lct_secir/infection_state.h" +#include "memilio/math/eigen.h" + +#include + +namespace mio +{ +namespace lsecir +{ + +/** +* @brief Computes an initialization vector for a LCT model with data from RKI. +* +* Computes an initial value vector for an LCT model with the defined infectionState and the parameters. +* For the computation expected sojourntime in the subcompartments are used. To calculate the initial values, +* we assume for simplicity that individuals stay in the subcompartment for exactly the expected time. +* The RKI data are linearly interpolated within one day. +* The RKI data should contain data for each needed day without division of age groups, the completeness of the dates is not verified. +* Data could be downloaded eg with the file pycode/memilio-epidata/memilio/epidata/getCaseData.py, +* which creates a file named cases_all_germany.json or a similar name. One should set impute_dates=True so that missing dates are imputed. +* +* @param path Path to the RKI file. +* @param date Date for which the initial values should be computed. date is the start date of the simulation. +* @param infectionState InfectionState used to calculate the initial values. Defines eg the size of the calculated vector. +* @param parameters Parameters used to calculate the initial values. Defines eg the expected sojourntimes. +* @param total_population Total size of the population of the considered region. +* The sum of all values in the returned vector will be equal to that value. +* @param scale_confirmed_cases Factor by which to scale the confirmed cases of rki data to consider unreported cases. +* @returns Initialization Vector or any io errors that happen during reading of the files. +*/ +IOResult get_initial_data_from_file(std::string const& path, Date date, InfectionState infectionState, + Parameters&& parameters, ScalarType total_population, + ScalarType scale_confirmed_cases = 1.); + +} // namespace lsecir +} // namespace mio +#endif // MEMILIO_HAS_JSONCPP + +#endif // LCTSECIR_PARAMETERS_IO_H \ No newline at end of file diff --git a/cpp/models/lct_secir/simulation.cpp b/cpp/models/lct_secir/simulation.cpp new file mode 100755 index 0000000000..02dbeb76d2 --- /dev/null +++ b/cpp/models/lct_secir/simulation.cpp @@ -0,0 +1,75 @@ +/* +* Copyright (C) 2020-2021 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#include "lct_secir/simulation.h" +#include "lct_secir/model.h" +#include "lct_secir/parameters.h" +#include "memilio/config.h" +#include "memilio/utils/time_series.h" +#include "memilio/math/stepper_wrapper.h" +#include "memilio/math/eigen.h" +#include "boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp" + +#include +#include + +namespace mio +{ +namespace lsecir +{ + +Simulation::Simulation(Model const& model, ScalarType t0, ScalarType dt) + : m_integratorCore( + std::make_shared>()) + , m_model(std::make_unique(model)) + , m_integrator( + [&model = *m_model](auto&& y, auto&& t, auto&& dydt) { + model.eval_right_hand_side(y, t, dydt); + }, + t0, m_model->get_initial_values(), dt, m_integratorCore) +{ +} + +void print_TimeSeries(const TimeSeries& result, std::string heading) +{ + // Print result after simulation. + std::cout << "# time | " + heading << std::endl; + for (Eigen::Index i = 0; i < result.get_num_time_points(); ++i) { + std::cout << result.get_time(i); + for (Eigen::Index j = 0; j < result.get_num_elements(); ++j) { + std::cout << " | " << std::fixed << std::setprecision(8) << result[i][j]; + } + std::cout << "\n" << std::endl; + } +} + +TimeSeries simulate(ScalarType t0, ScalarType tmax, ScalarType dt, Model const& model, + std::shared_ptr integrator) +{ + model.check_constraints(); + Simulation sim(model, t0, dt); + if (integrator) { + sim.set_integrator(integrator); + } + sim.advance(tmax); + return sim.get_result(); +} + +} // namespace lsecir +} // namespace mio \ No newline at end of file diff --git a/cpp/models/lct_secir/simulation.h b/cpp/models/lct_secir/simulation.h new file mode 100755 index 0000000000..1e29c83f8c --- /dev/null +++ b/cpp/models/lct_secir/simulation.h @@ -0,0 +1,171 @@ +/* +* Copyright (C) 2020-2021 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#ifndef LCT_SECIR_SIMULATION_H +#define LCT_SECIR_SIMULATION_H + +#include "lct_secir/model.h" +#include "memilio/config.h" +#include "memilio/utils/time_series.h" +#include "memilio/utils/metaprogramming.h" +#include "memilio/math/stepper_wrapper.h" +#include "memilio/math/eigen.h" +#include + +namespace mio +{ +namespace lsecir +{ +/** + * @brief A class for the simulation of a LCT model. + */ +class Simulation +{ +public: + /** + * @brief Set up the simulation with an ODE solver. + * + * Per default Runge Kutta Cash Karp 54 solver is used. + * Use function set_integrator() to set a different integrator. + * @param[in] model An instance of a LCT model. + * @param[in] t0 The Start time, usually 0. + * @param[in] dt Initial step size of integration. + */ + Simulation(Model const& model, ScalarType t0 = 0., ScalarType dt = 0.1); + + /** + * @brief Set the core integrator used in the simulation. + * + * @param[in] integrator Core integrator that should be used for simulation. + */ + void set_integrator(std::shared_ptr integrator) + { + m_integratorCore = std::move(integrator); + m_integrator.set_integrator(m_integratorCore); + } + + /** + * @brief Get a reference to the used integrator. + * + * @return Reference to the core integrator used in the simulation + */ + IntegratorCore& get_integrator() + { + return *m_integratorCore; + } + + /** + * @brief Get a reference to the used integrator. + * + * @return Reference to the core integrator used in the simulation + */ + IntegratorCore const& get_integrator() const + { + return *m_integratorCore; + } + + /** + * @brief Advance simulation to tmax. + * + * tmax must be greater than get_result().get_last_time_point(). + * @param tmax Stopping point of the simulation. + */ + Eigen::Ref advance(ScalarType tmax) + { + return m_integrator.advance(tmax); + } + + /** + * @brief Get the result of the simulation. + * + * Return the number of persons in all InfectionState%s (inclusive subcompartments). + * @return The result of the simulation in form of a TimeSeries. + */ + TimeSeries& get_result() + { + return m_integrator.get_result(); + } + + /** + * @brief Get the result of the simulation. + * + * Return the number of persons in all InfectionState%s (inclusive subcompartments). + * @return The result of the simulation in form of a TimeSeries. + */ + const TimeSeries& get_result() const + { + return m_integrator.get_result(); + } + + /** + * @brief Returns a reference to the simulation model used in simulation. + */ + const Model& get_model() const + { + return *m_model; + } + + /** + * @brief Returns a reference to the simulation model used in simulation. + */ + Model& get_model() + { + return *m_model; + } + + /** + * @brief Returns the next time step chosen by integrator. + */ + ScalarType get_dt() const + { + return m_integrator.get_dt(); + } + +private: + std::shared_ptr m_integratorCore; ///< InteratorCore used for Simulation. + std::unique_ptr m_model; ///< LCT-model the simulation should be performed with. + OdeIntegrator m_integrator; ///< OdeIntegrator used to perform simulation. +}; + +/** + * @brief Prints values in a TimeSeries. + * + * @param[in] result A TimeSeries that should be printed. + * @param[in] heading A heading that should be printed before the TimeSeries. + */ +void print_TimeSeries(const TimeSeries& result, std::string heading = " "); + +/** + * @brief Performs a simulation with specified parameters for given model. + * + * @param[in] t0 Start time of the simulation. + * @param[in] tmax End time of the simulation. + * @param[in] dt Initial step size of integration. + * @param[in] model An instance of a LCT model for which the simulation should be performed. + * @param[in] integrator An integrator that should be used to discretize the model equations. + * If default value is used the simulation will be performed with the runge_kutta_cash_karp54 method. + * @return A TimeSeries with the result of the simulation. + */ +TimeSeries simulate(ScalarType t0, ScalarType tmax, ScalarType dt, Model const& model, + std::shared_ptr integrator = nullptr); + +} // namespace lsecir +} // namespace mio + +#endif // LCT_SECIR_SIMULATION_H diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index c9aa540f65..e361f8287b 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -55,6 +55,8 @@ set(TESTSOURCES abm_helpers.h abm_helpers.cpp test_ide_secir.cpp + test_lct_secir.cpp + test_lct_initializer.cpp test_state_age_function.cpp distributions_helpers.h distributions_helpers.cpp @@ -68,6 +70,7 @@ if(MEMILIO_HAS_JSONCPP) set(TESTSOURCES ${TESTSOURCES} test_json_serializer.cpp test_epi_data_io.cpp +test_lct_parameters_io.cpp ) endif() @@ -80,7 +83,7 @@ endif() add_executable(memilio-test ${TESTSOURCES}) target_include_directories(memilio-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(memilio-test PRIVATE memilio ode_secir ode_seir ode_secirvvs ide_seir ide_secir abm gtest_main) +target_link_libraries(memilio-test PRIVATE memilio ode_secir ode_seir ode_secirvvs ide_seir ide_secir lct_secir abm gtest_main) target_compile_options(memilio-test PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) # make unit tests find the test data files diff --git a/cpp/tests/data/cases_all_germany.json b/cpp/tests/data/cases_all_germany.json new file mode 100644 index 0000000000..f4859ae673 --- /dev/null +++ b/cpp/tests/data/cases_all_germany.json @@ -0,0 +1,92 @@ +[ + { + "Date": "2020-05-24", + "Confirmed": 1.2, + "Deaths": 0.5, + "Recovered": 0 + }, + { + "Date": "2020-05-25", + "Confirmed": 5, + "Deaths": 2.0, + "Recovered": 2.5 + }, + { + "Date": "2020-05-26", + "Confirmed": 20.2, + "Deaths": 5.01, + "Recovered": 3.0 + }, + { + "Date": "2020-05-27", + "Confirmed": 25.6, + "Deaths": 6, + "Recovered": 3.0 + }, + { + "Date": "2020-05-28", + "Confirmed": 27.6, + "Deaths": 6, + "Recovered": 3.0 + }, + { + "Date": "2020-05-29", + "Confirmed": 30.6, + "Deaths": 6, + "Recovered": 3.0 + }, + { + "Date": "2020-05-30", + "Confirmed": 30.6, + "Deaths": 6, + "Recovered": 3.0 + }, + { + "Date": "2020-05-31", + "Confirmed": 35, + "Deaths": 6, + "Recovered": 3.0 + }, + { + "Date": "2020-06-01", + "Confirmed": 44, + "Deaths": 8, + "Recovered": 3.0 + }, + { + "Date": "2020-06-02", + "Confirmed": 44.5, + "Deaths": 8.999, + "Recovered": 6 + }, + { + "Date": "2020-06-03", + "Confirmed": 70, + "Deaths": 9, + "Recovered": 6 + }, + { + "Date": "2020-06-04", + "Confirmed": 100, + "Deaths": 20, + "Recovered": 6 + }, + { + "Date": "2020-06-05", + "Confirmed": 100.3, + "Deaths": 23.56, + "Recovered": 6 + }, + { + "Date": "2020-06-06", + "Confirmed": 115, + "Deaths": 24, + "Recovered": 6 + }, + { + "Date": "2020-06-07", + "Confirmed": 120.6, + "Deaths": 30, + "Recovered": 6 + } +] \ No newline at end of file diff --git a/cpp/tests/data/lct-secir-compartments-compare.csv b/cpp/tests/data/lct-secir-compartments-compare.csv new file mode 100755 index 0000000000..f6c90b6ce8 --- /dev/null +++ b/cpp/tests/data/lct-secir-compartments-compare.csv @@ -0,0 +1,14 @@ +# time | S | E | C | I | H | U | R | D +0.00000000 750.00000000 50.00000000 40.00000000 50.00000000 50.00000000 30.00000000 20.00000000 10.00000000 +0.10000000 748.46824343 50.26284184 39.76391713 50.50314806 49.64881726 29.98773082 21.32228601 10.04301546 +0.29072295 745.55860018 50.65687853 39.32541531 51.51432605 48.99408015 29.94667771 23.87442948 10.12959258 +0.48144590 742.66416112 50.92893708 38.82680068 52.64226763 48.35930851 29.88013044 26.47554743 10.22284710 +0.69951506 739.37529863 51.11312649 38.15900489 54.06952788 47.65846992 29.76917609 29.51661451 10.33878159 +0.94011476 735.77502876 51.18413896 37.33702852 55.75155953 46.91644175 29.59910565 32.95707813 10.47961870 +1.20982607 731.85282708 51.05289643 36.38618932 57.66826350 46.12324676 29.34392108 36.91743422 10.65522161 +1.49825088 728.46517521 50.03000624 35.41581910 59.64744582 45.31873551 28.99124062 41.26645170 10.86512581 +1.85517405 725.99585366 47.01366564 34.32697685 61.88928798 44.38205556 28.43822802 46.79653735 11.15739494 +2.12963864 724.78054190 44.06842389 33.54397453 63.42228150 43.70283544 27.92703310 51.14873009 11.40617955 +2.45390654 723.38317300 40.69846259 32.61593743 65.01365771 42.94248900 27.23273570 56.38817869 11.72536588 +2.89287892 721.51261986 36.46886578 31.26065299 66.80715050 41.97894676 26.15458682 63.62123679 12.19594050 +3.00000000 721.06078616 35.50223737 30.90410239 67.18501599 41.75434540 25.87051417 65.40643018 12.31656834 \ No newline at end of file diff --git a/cpp/tests/data/lct-secir-subcompartments-compare.csv b/cpp/tests/data/lct-secir-subcompartments-compare.csv new file mode 100755 index 0000000000..b1a7a9fa24 --- /dev/null +++ b/cpp/tests/data/lct-secir-subcompartments-compare.csv @@ -0,0 +1,14 @@ +# time | S | E1 | E2 | C1 | C2 | C3 | I | H | U1 | U2 | U3 | U4 | U5 | R | D +0 750.00000000 30.00000000 20.00000000 20.00000000 10.00000000 10.00000000 50.00000000 50.00000000 10.00000000 10.00000000 5.00000000 3.00000000 2.00000000 20.00000000 10.00000000 +0.10000000 748.46824343 29.66723995 20.59560189 18.39292262 11.27497032 10.09602418 50.50314806 49.64881726 9.44659952 9.98074799 5.33955113 3.14782359 2.07300858 21.32228601 10.04301546 +0.29072295 745.55860018 29.07626753 21.58061100 16.00693881 12.71846120 10.60001530 51.51432605 48.99408015 8.49092451 9.85090613 5.91568674 3.46129489 2.22786544 23.87442948 10.12959258 +0.48144590 742.66416112 28.53736239 22.39157469 14.30701953 13.30306581 11.21671534 52.64226763 48.35930851 7.65233409 9.62495724 6.39661907 3.80166291 2.40455713 26.47554743 10.22284710 +0.69951506 739.37529863 27.97565895 23.13746754 12.96585914 13.37340723 11.81973852 54.06952788 47.65846992 6.81828949 9.28178048 6.83204179 4.20428323 2.63278111 29.51661451 10.33878159 +0.94011476 735.77502876 27.41213738 23.77200158 12.00281102 13.08420856 12.25000894 55.75155953 46.91644175 6.03092978 8.83306189 7.17864315 4.64336713 2.91310371 32.95707813 10.47961870 +1.20982607 731.85282708 26.76494962 24.28794681 11.34519845 12.59793545 12.44305542 57.66826350 46.12324676 5.28826093 8.28010536 7.41601376 5.10528556 3.25425548 36.91743422 10.65522161 +1.49825088 728.46517521 25.43323249 24.59677375 10.94270800 12.07556224 12.39754886 59.64744582 45.31873551 4.63012700 7.66567073 7.51659762 5.54181441 3.63703086 41.26645170 10.86512581 +1.85517405 725.99585366 22.53301437 24.48065127 10.65304719 11.53623453 12.13769513 61.88928798 44.38205556 3.97335382 6.91208660 7.46202330 5.97764331 4.11312098 46.79653735 11.15739494 +2.12963864 724.78054190 20.09633142 23.97209247 10.46592242 11.20475910 11.87329300 63.42228150 43.70283544 3.56418839 6.35739777 7.31464373 6.22631004 4.46449317 51.14873009 11.40617955 +2.45390654 723.38317300 17.67435181 23.02411078 10.20372528 10.86770945 11.54450270 65.01365771 42.94248900 3.16689926 5.74360799 7.05333091 6.42218033 4.84671722 56.38817869 11.72536588 +2.89287892 721.51261986 15.06890402 21.39996177 9.73272421 10.42689561 11.10103317 66.80715050 41.97894676 2.74638652 4.99700433 6.59844111 6.52840395 5.28435092 63.62123679 12.19594050 +3.00000000 721.06078616 14.53009114 20.97214623 9.59804298 10.31367815 10.99238125 67.18501599 41.75434540 2.66050262 4.83027461 6.47590662 6.52900540 5.37482492 65.40643018 12.31656834 \ No newline at end of file diff --git a/cpp/tests/data/test_cases_all_germany.json b/cpp/tests/data/test_cases_all_germany.json new file mode 100644 index 0000000000..1279f400a7 --- /dev/null +++ b/cpp/tests/data/test_cases_all_germany.json @@ -0,0 +1,20 @@ +[ + { + "Date": "2020-06-01", + "Confirmed": 1.2, + "Deaths": 2.0, + "Recovered": 3.0 + }, + { + "Date": "2020-06-02", + "Confirmed": 4.5, + "Deaths": 5.1, + "Recovered": 6 + }, + { + "Date": "2020-06-03", + "Confirmed": 7, + "Deaths": 8, + "Recovered": 930.1 + } +] \ No newline at end of file diff --git a/cpp/tests/data/test_empty_file.json b/cpp/tests/data/test_empty_file.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/cpp/tests/data/test_empty_file.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/cpp/tests/test_epi_data_io.cpp b/cpp/tests/test_epi_data_io.cpp index 09c614bd70..954adb6478 100644 --- a/cpp/tests/test_epi_data_io.cpp +++ b/cpp/tests/test_epi_data_io.cpp @@ -94,6 +94,36 @@ TEST(TestEpiDataIo, read_rki_error_age) ASSERT_THAT(print_wrap(result), IsFailure(mio::StatusCode::InvalidValue)); } +TEST(TestEpiDataIo, read_confirmed_cases_noage) +{ + Json::Value js(Json::arrayValue); + js[0]["Date"] = "2021-12-01"; + js[0]["Confirmed"] = 1; + js[0]["Deaths"] = 2; + js[0]["Recovered"] = 3; + + js[1]["Date"] = "2021-12-02"; + js[1]["Confirmed"] = 4; + js[1]["Deaths"] = 5; + js[1]["Recovered"] = 6; + + auto result = mio::deserialize_confirmed_cases_noage(js); + ASSERT_THAT(print_wrap(result), IsSuccess()); + + auto rki_data_noage = result.value(); + ASSERT_EQ(rki_data_noage.size(), 2); + + ASSERT_EQ(rki_data_noage[0].date, mio::Date(2021, 12, 1)); + ASSERT_EQ(rki_data_noage[0].num_confirmed, 1); + ASSERT_EQ(rki_data_noage[0].num_deaths, 2); + ASSERT_EQ(rki_data_noage[0].num_recovered, 3); + + ASSERT_EQ(rki_data_noage[1].date, mio::Date(2021, 12, 2)); + ASSERT_EQ(rki_data_noage[1].num_confirmed, 4); + ASSERT_EQ(rki_data_noage[1].num_deaths, 5); + ASSERT_EQ(rki_data_noage[1].num_recovered, 6); +} + TEST(TestEpiDataIo, read_divi) { Json::Value js(Json::arrayValue); @@ -301,6 +331,29 @@ TEST(TestEpiDataIo, read_confirmed_cases_data) ASSERT_EQ(case_data[2].state_id, boost::none); } +TEST(TestEpiDataIo, read_confirmed_cases_noage_data) +{ + auto rki_data_noage = + mio::read_confirmed_cases_noage(mio::path_join(TEST_DATA_DIR, "test_cases_all_germany.json")).value(); + + ASSERT_EQ(rki_data_noage.size(), 3); + + ASSERT_EQ(rki_data_noage[0].date, mio::Date(2020, 06, 01)); + ASSERT_EQ(rki_data_noage[0].num_confirmed, 1.2); + ASSERT_EQ(rki_data_noage[0].num_deaths, 2); + ASSERT_EQ(rki_data_noage[0].num_recovered, 3); + + ASSERT_EQ(rki_data_noage[1].date, mio::Date(2020, 06, 02)); + ASSERT_EQ(rki_data_noage[1].num_confirmed, 4.5); + ASSERT_EQ(rki_data_noage[1].num_deaths, 5.1); + ASSERT_EQ(rki_data_noage[1].num_recovered, 6); + + ASSERT_EQ(rki_data_noage[2].date, mio::Date(2020, 06, 03)); + ASSERT_EQ(rki_data_noage[2].num_confirmed, 7); + ASSERT_EQ(rki_data_noage[2].num_deaths, 8); + ASSERT_EQ(rki_data_noage[2].num_recovered, 930.1); +} + TEST(TestEpiDataIO, read_vaccination_data) { auto vacc_data = mio::read_vaccination_data(mio::path_join(TEST_DATA_DIR, "test_all_ageinf_vacc.json")).value(); diff --git a/cpp/tests/test_ide_secir.cpp b/cpp/tests/test_ide_secir.cpp index 731110ce85..22eb2275d7 100755 --- a/cpp/tests/test_ide_secir.cpp +++ b/cpp/tests/test_ide_secir.cpp @@ -41,8 +41,8 @@ class ModelTestIdeSecir : public testing::Test using Vec = mio::TimeSeries::Vector; //Set initial conditions - ScalarType N = 10000; - ScalarType Dead_before = 12; + ScalarType N = 10000; + ScalarType deaths = 13.10462213; int num_transitions = (int)mio::isecir::InfectionTransition::Count; @@ -65,7 +65,7 @@ class ModelTestIdeSecir : public testing::Test } // Initialize model - model = new mio::isecir::Model(std::move(init), N, Dead_before); + model = new mio::isecir::Model(std::move(init), N, deaths); // Set working parameters. mio::SmootherCosine smoothcos(2.0); @@ -86,6 +86,8 @@ class ModelTestIdeSecir : public testing::Test model->parameters.set(prob); model->parameters.set(prob); model->parameters.set(prob); + + model->set_tol_for_support_max(1e-10); } virtual void TearDown() @@ -161,10 +163,10 @@ TEST(IdeSecir, checkSimulationFunctions) { using Vec = mio::TimeSeries::Vector; - ScalarType tmax = 0.5; - ScalarType N = 10000; - ScalarType Dead_before = 10; - ScalarType dt = 0.5; + ScalarType tmax = 0.5; + ScalarType N = 10000; + ScalarType deaths = 10; + ScalarType dt = 0.5; int num_transitions = (int)mio::isecir::InfectionTransition::Count; @@ -190,7 +192,7 @@ TEST(IdeSecir, checkSimulationFunctions) } // Initialize model. - mio::isecir::Model model(std::move(init), N, Dead_before); + mio::isecir::Model model(std::move(init), N, deaths); // Set working parameters. // In this example, SmootherCosine with parameter 1 (and thus with a maximum support of 1) @@ -244,15 +246,181 @@ TEST(IdeSecir, checkSimulationFunctions) } } +// Check if the model uses uses the correct method for initialization. +// This is done by comparing the compartment sizes at t0 with a previous run with the same initialization method. +TEST(IdeSecir, checkInitializations) +{ + using Vec = mio::TimeSeries::Vector; + using ParameterSet = mio::isecir::Parameters; + + ScalarType tmax = 1; + ScalarType N = 10000; + ScalarType deaths = 13.10462213; + ScalarType dt = 1; + + int num_transitions = (int)mio::isecir::InfectionTransition::Count; + + // Define vectors to compare with. + Vec vec_forceofinfection((int)mio::isecir::InfectionState::Count); + Vec vec_total_confirmed((int)mio::isecir::InfectionState::Count); + Vec vec_S((int)mio::isecir::InfectionState::Count); + Vec vec_R((int)mio::isecir::InfectionState::Count); + Vec vec_noinit((int)mio::isecir::InfectionState::Count); + vec_forceofinfection << 4376.75616533, 13.80777657, 16.50441264, 4.41848850, 0.55231106, 0.55231106, 5574.30391270, + 13.10462213; + vec_total_confirmed << 8969.68781079, 13.80777657, 16.50441264, 4.41848850, 0.55231106, 0.55231106, 981.37226724, + 13.10462213; + vec_S << 5000.00000000, 13.80777657, 16.50441264, 4.41848850, 0.55231106, 0.55231106, 4951.06007803, 13.10462213; + vec_R << 8951.06007803, 13.80777657, 16.50441264, 4.41848850, 0.55231106, 0.55231106, 1000.00000000, 13.10462213; + vec_noinit << 0.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000, 13.10462213; + + // Create TimeSeries with num_transitions elements where transitions needed for simulation will be stored. + mio::TimeSeries init(num_transitions); + + // Add time points for initialization of transitions. + Vec vec_init(num_transitions); + vec_init[(int)mio::isecir::InfectionTransition::SusceptibleToExposed] = 25.0; + vec_init[(int)mio::isecir::InfectionTransition::ExposedToInfectedNoSymptoms] = 15.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] = 8.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToRecovered] = 4.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedSymptomsToInfectedSevere] = 1.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedSymptomsToRecovered] = 4.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedSevereToInfectedCritical] = 1.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedSevereToRecovered] = 1.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedCriticalToDead] = 1.0; + vec_init[(int)mio::isecir::InfectionTransition::InfectedCriticalToRecovered] = 1.0; + // Add initial time point to time series. + init.add_time_point(-10, vec_init); + // Add further time points until time 0. + while (init.get_last_time() < 0) { + vec_init *= 1.01; + init.add_time_point(init.get_last_time() + dt, vec_init); + } + ParameterSet parameters; + + // Set working parameters. + mio::SmootherCosine smoothcos(2.0); + mio::StateAgeFunctionWrapper delaydistribution(smoothcos); + std::vector vec_delaydistrib(num_transitions, delaydistribution); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::SusceptibleToExposed].set_parameter(3.0); + vec_delaydistrib[(int)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms].set_parameter(4.0); + parameters.set(vec_delaydistrib); + + std::vector vec_prob((int)mio::isecir::InfectionTransition::Count, 0.5); + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::SusceptibleToExposed)] = 1; + vec_prob[Eigen::Index(mio::isecir::InfectionTransition::ExposedToInfectedNoSymptoms)] = 1; + parameters.set(vec_prob); + + mio::ContactMatrixGroup contact_matrix = mio::ContactMatrixGroup(1, 1); + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10.)); + parameters.get() = mio::UncertainContactMatrix(contact_matrix); + + mio::ExponentialDecay expdecay(0.5); + mio::StateAgeFunctionWrapper prob(expdecay); + parameters.set(prob); + parameters.set(prob); + parameters.set(prob); + + // --- Case with forceofinfection. + mio::TimeSeries init_copy(init); + mio::isecir::Model model(std::move(init_copy), N, deaths, 0, std::move(parameters)); + model.check_constraints(dt); + + // Carry out simulation. + mio::isecir::Simulation sim(model, 0, dt); + sim.advance(tmax); + + mio::TimeSeries result = sim.get_result(); + + // Compare with previous run. + for (Eigen::Index i = 0; i < (Eigen::Index)mio::isecir::InfectionState::Count; i++) { + EXPECT_NEAR(result[0][i], vec_forceofinfection[i], 1e-8); + } + + // --- Case with total_confirmed_cases. + mio::TimeSeries init_copy2(init); + mio::isecir::Model model2(std::move(init_copy2), N, deaths, 1000, std::move(parameters)); + model2.check_constraints(dt); + + // Carry out simulation. + mio::isecir::Simulation sim2(model2, 0, dt); + sim2.advance(tmax); + result = sim2.get_result(); + + // Compare with previous run. + for (Eigen::Index i = 0; i < (Eigen::Index)mio::isecir::InfectionState::Count; i++) { + EXPECT_NEAR(result[0][i], vec_total_confirmed[i], 1e-8); + } + + // --- Case with S. + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 0)); + parameters.get() = mio::UncertainContactMatrix(contact_matrix); + + mio::TimeSeries init_copy3(init); + mio::isecir::Model model3(std::move(init_copy3), N, deaths, 0, std::move(parameters)); + + model3.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Susceptible] = 5000; + model3.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Recovered] = 0; + + model3.check_constraints(dt); + + // Carry out simulation. + mio::isecir::Simulation sim3(model3, 0, dt); + sim3.advance(tmax); + result = sim3.get_result(); + + // Compare with previous run. + for (Eigen::Index i = 0; i < (Eigen::Index)mio::isecir::InfectionState::Count; i++) { + EXPECT_NEAR(result[0][i], vec_S[i], 1e-8); + } + + // --- Case with R. + mio::TimeSeries init_copy4(init); + mio::isecir::Model model4(std::move(init_copy4), N, deaths, 0, std::move(parameters)); + + model4.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Susceptible] = 0; + model4.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Recovered] = 1000; + + model4.check_constraints(dt); + + // Carry out simulation. + mio::isecir::Simulation sim4(model4, 0, dt); + sim4.advance(tmax); + result = sim4.get_result(); + + // Compare with previous run. + for (Eigen::Index i = 0; i < (Eigen::Index)mio::isecir::InfectionState::Count; i++) { + EXPECT_NEAR(result[0][i], vec_R[i], 1e-8); + } + + // --- Case without fitting initialization method. + mio::isecir::Model model5(std::move(init), N, deaths, 0, std::move(parameters)); + + model5.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Susceptible] = 0; + model5.m_populations.get_last_value()[(Eigen::Index)mio::isecir::InfectionState::Recovered] = 0; + + model5.check_constraints(dt); + + // Carry out simulation. + mio::isecir::Simulation sim5(model5, 0, dt); + sim5.advance(tmax); + result = sim5.get_result(); + + // Compare with previous run + for (Eigen::Index i = 0; i < (Eigen::Index)mio::isecir::InfectionState::Count; i++) { + EXPECT_NEAR(result[0][i], vec_noinit[i], 1e-8); + } +} + // a) Test if check_constraints() function correctly reports wrongly set parameters. // b) Test if check_constraints() does not complain if parameters are set within correct ranges. TEST(IdeSecir, testValueConstraints) { using Vec = mio::TimeSeries::Vector; - ScalarType N = 10000; - ScalarType Dead_before = 10; - ScalarType dt = 1; + ScalarType N = 10000; + ScalarType deaths = 10; + ScalarType dt = 1; int num_transitions = (int)mio::isecir::InfectionTransition::Count; @@ -278,7 +446,7 @@ TEST(IdeSecir, testValueConstraints) } // Initialize a model. - mio::isecir::Model model(std::move(init), N, Dead_before); + mio::isecir::Model model(std::move(init), N, deaths); // Deactivate temporarily log output for next tests. mio::set_log_level(mio::LogLevel::off); @@ -406,10 +574,10 @@ TEST(IdeSecir, checkProportionRecoveredDeath) { using Vec = mio::TimeSeries::Vector; - ScalarType tmax = 30; - ScalarType N = 10000; - ScalarType Dead_before = 10; - ScalarType dt = 1; + ScalarType tmax = 30; + ScalarType N = 10000; + ScalarType deaths = 10; + ScalarType dt = 1; int num_transitions = (int)mio::isecir::InfectionTransition::Count; @@ -435,7 +603,7 @@ TEST(IdeSecir, checkProportionRecoveredDeath) } // Initialize model. - mio::isecir::Model model(std::move(init), N, Dead_before); + mio::isecir::Model model(std::move(init), N, deaths); // Set working parameters. // All TransitionDistribution%s are ExponentialDecay functions. @@ -494,10 +662,10 @@ TEST(IdeSecir, compareEquilibria) { using Vec = mio::TimeSeries::Vector; - ScalarType tmax = 20; - ScalarType N = 10000; - ScalarType Dead_before = 10; - ScalarType dt = 1; + ScalarType tmax = 20; + ScalarType N = 10000; + ScalarType deaths = 10; + ScalarType dt = 1; int num_transitions = (int)mio::isecir::InfectionTransition::Count; @@ -525,8 +693,8 @@ TEST(IdeSecir, compareEquilibria) mio::TimeSeries init2(init); // Initialize two models. - mio::isecir::Model model(std::move(init), N, Dead_before); - mio::isecir::Model model2(std::move(init2), N, Dead_before); + mio::isecir::Model model(std::move(init), N, deaths); + mio::isecir::Model model2(std::move(init2), N, deaths); // Set working parameters. // Here the maximum support for the TransitionDistribution%s is set differently for each model diff --git a/cpp/tests/test_lct_initializer.cpp b/cpp/tests/test_lct_initializer.cpp new file mode 100644 index 0000000000..9d42460181 --- /dev/null +++ b/cpp/tests/test_lct_initializer.cpp @@ -0,0 +1,200 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "lct_secir/infection_state.h" +#include "lct_secir/initialization.h" +#include "memilio/config.h" +#include "memilio/epidemiology/state_age_function.h" +#include "memilio/utils/compiler_diagnostics.h" +#include "memilio/math/eigen.h" + +#include + +// Test compares a calculation of an initial vector using data for flows with a previous result. +TEST(TestInitializer, compareWithPrevious) +{ + + ScalarType dt = 0.5; + + // Define number of subcompartments. + std::vector SubcompartmentNumbers((int)mio::lsecir::InfectionStateBase::Count, 1); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Exposed] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = 3; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = 3; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 2; + mio::lsecir::InfectionState InfState(SubcompartmentNumbers); + + // Previous result. + Eigen::VectorXd compare(InfState.get_count()); + compare << 82810889.00545, 850.70432, 970.04980, 315.32890, 391.51799, 391.39351, 565.45854, 580.79267, 85.97421, + 86.02738, 80.26791, 189.53449, 167.57963, 329757.36512, 9710; + + // Define parameters. + mio::lsecir::Parameters parameters_lct; + parameters_lct.get() = 3.1; + parameters_lct.get() = 3.1; + parameters_lct.get() = 6.1; + parameters_lct.get() = 11.1; + parameters_lct.get() = 17.1; + parameters_lct.get() = 0.01; + mio::ContactMatrixGroup contact_matrix = mio::ContactMatrixGroup(1, 1); + parameters_lct.get() = mio::UncertainContactMatrix(contact_matrix); + + parameters_lct.get() = 1; + parameters_lct.get() = 1; + parameters_lct.get() = 0; + parameters_lct.get() = 0; + parameters_lct.get() = 0.1; + parameters_lct.get() = 0.1; + parameters_lct.get() = 0.1; + parameters_lct.get() = 0.1; + + ScalarType total_confirmed_cases = 341223; + ScalarType deaths = 9710; + ScalarType total_population = 83155031.0; + + // Add time points for initialization of transitions. + mio::TimeSeries init((int)mio::lsecir::InfectionTransition::Count); + mio::TimeSeries::Vector vec_init((int)mio::lsecir::InfectionTransition::Count); + vec_init[(int)mio::lsecir::InfectionTransition::SusceptibleToExposed] = 25.0; + vec_init[(int)mio::lsecir::InfectionTransition::ExposedToInfectedNoSymptoms] = 15.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] = 8.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedNoSymptomsToRecovered] = 4.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedSymptomsToInfectedSevere] = 1.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedSymptomsToRecovered] = 4.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedSevereToInfectedCritical] = 1.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedSevereToRecovered] = 1.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedCriticalToDead] = 1.0; + vec_init[(int)mio::lsecir::InfectionTransition::InfectedCriticalToRecovered] = 1.0; + // Add initial time point to time series. + init.add_time_point(-130, vec_init); + // Add further time points until time 0. + while (init.get_last_time() < 0) { + vec_init *= 1.01; + init.add_time_point(init.get_last_time() + dt, vec_init); + } + + // Calculate initial vector and compare with previous reult. + mio::lsecir::Initializer initializer(std::move(init), InfState, std::move(parameters_lct)); + auto init_compartments = initializer.compute_initializationvector(total_population, deaths, total_confirmed_cases); + + for (int i = 0; i < InfState.get_count(); i++) { + EXPECT_NEAR(init_compartments[i], compare[i], 1e-4) << "at subcompartment number " << i; + } +} + +// Check if the constraints of te Initializer are validated as expected. +TEST(TestInitializer, testConstraints) +{ + // Deactivate temporarily log output for next tests. + mio::set_log_level(mio::LogLevel::off); + + ScalarType dt = 0.5; + + // Define number of subcompartments. + std::vector SubcompartmentNumbers((int)mio::lsecir::InfectionStateBase::Count, 1); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Exposed] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = 3; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = 3; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 2; + mio::lsecir::InfectionState InfState(SubcompartmentNumbers); + + // Check wrong size of initial flows. + mio::TimeSeries init_wrong_size((int)mio::lsecir::InfectionTransition::Count - 1); + Eigen::VectorXd vec_wrong_size = Eigen::VectorXd::Ones((int)mio::lsecir::InfectionTransition::Count - 1); + init_wrong_size.add_time_point(-10, vec_wrong_size); + init_wrong_size.add_time_point(-9, vec_wrong_size); + + mio::lsecir::Initializer initializer_init_wrong_size(std::move(init_wrong_size), InfState); + + bool constraint_check = initializer_init_wrong_size.check_constraints(); + EXPECT_TRUE(constraint_check); + + // Check if last time of initial flows is not zero. + mio::TimeSeries init_wrong((int)mio::lsecir::InfectionTransition::Count); + Eigen::VectorXd vec_init = Eigen::VectorXd::Ones((int)mio::lsecir::InfectionTransition::Count); + init_wrong.add_time_point(-10, vec_init); + init_wrong.add_time_point(-9, vec_init); + + mio::TimeSeries init_copy(init_wrong); + mio::lsecir::Initializer initializer_init_wrong_last_time(std::move(init_copy), InfState); + + constraint_check = initializer_init_wrong_last_time.check_constraints(); + EXPECT_TRUE(constraint_check); + + // Check if time steps of initial flows are not equidistant. + init_wrong.add_time_point(init_wrong.get_last_time() + 1., vec_init); + while (init_wrong.get_last_time() < 0) { + init_wrong.add_time_point(init_wrong.get_last_time() + dt, vec_init); + } + + mio::lsecir::Initializer initializer_init_wrong_equidistant(std::move(init_wrong), InfState); + + constraint_check = initializer_init_wrong_equidistant.check_constraints(); + EXPECT_TRUE(constraint_check); + + // Check large step size. + mio::TimeSeries init_wrong_step((int)mio::lsecir::InfectionTransition::Count); + init_wrong_step.add_time_point(-10, vec_init); + init_wrong_step.add_time_point(init_wrong_step.get_last_time() + 2., vec_init); + while (init_wrong_step.get_last_time() < 0) { + init_wrong_step.add_time_point(init_wrong_step.get_last_time() + dt, vec_init); + } + + mio::lsecir::Initializer initializer_init_wrong_step(std::move(init_wrong_step), InfState); + + constraint_check = initializer_init_wrong_step.check_constraints(); + EXPECT_TRUE(constraint_check); + + // Check with correct flows. + mio::TimeSeries init_right((int)mio::lsecir::InfectionTransition::Count); + init_right.add_time_point(-10, vec_init); + while (init_right.get_last_time() < 0) { + init_right.add_time_point(init_right.get_last_time() + dt, vec_init); + } + + mio::lsecir::Initializer initializer_right(std::move(init_right), InfState); + + constraint_check = initializer_right.check_constraints(); + EXPECT_FALSE(constraint_check); + + // Check with too short time period of initial data. The time period above was long enough. + mio::TimeSeries init_short((int)mio::lsecir::InfectionTransition::Count); + init_short.add_time_point(-1., vec_init); + while (init_short.get_last_time() < 0) { + init_short.add_time_point(init_short.get_last_time() + dt, vec_init); + } + + mio::lsecir::Initializer initializer_init_short(std::move(init_short), InfState); + ScalarType total_confirmed_cases = 341223; + ScalarType deaths = 9710; + ScalarType total_population = 83155031.0; + auto initialconditions = + initializer_init_short.compute_initializationvector(total_population, deaths, total_confirmed_cases); + + for (int i = 2; i < InfState.get_count() - 2; i++) { + EXPECT_EQ(-1, initialconditions[i]); + } + + // Reactive log output. + mio::set_log_level(mio::LogLevel::warn); +} \ No newline at end of file diff --git a/cpp/tests/test_lct_parameters_io.cpp b/cpp/tests/test_lct_parameters_io.cpp new file mode 100644 index 0000000000..9f56c868ce --- /dev/null +++ b/cpp/tests/test_lct_parameters_io.cpp @@ -0,0 +1,143 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#include "memilio/config.h" + +#include "lct_secir/parameters_io.h" +#include "lct_secir/parameters.h" +#include "lct_secir/infection_state.h" +#include "memilio/math/eigen.h" +#include "memilio/utils/date.h" +#include "test_data_dir.h" +#include "memilio/io/io.h" +#include + +#include + +// Check that Initialization based on synthetic RKI data match previous result. +TEST(TestLCTParametersIo, ReadPopulationDataRKI) +{ + ScalarType total_population = 1000.0; + auto start_date = mio::Date(2020, 6, 1); + + // Define parameters used for simulation and initialization. + mio::lsecir::Parameters parameters; + parameters.get() = 2.3; + parameters.get() = 3.3; + parameters.get() = 2.4; + parameters.get() = 1.8; + parameters.get() = 3.0; + + parameters.get() = 0.2; + parameters.get() = 0.1; + parameters.get() = 0.3; + parameters.get() = 0.2; + + // Define number of subcompartments. + std::vector vec_subcompartments((int)mio::lsecir::InfectionStateBase::Count, 1); + // Use subcompartments with a soujourn time of approximately one day in each subcompartment. + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = + (int)round(parameters.get()); + // Both realistic distributions for times corresponding to InfectedCritical of the IDE model are exponential distributions. + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 1; + mio::lsecir::InfectionState infectionState(vec_subcompartments); + + // Calculate initial value vector for subcompartments with RKI data. + auto read_result = + mio::lsecir::get_initial_data_from_file(mio::path_join(TEST_DATA_DIR, "cases_all_germany.json"), start_date, + infectionState, std::move(parameters), total_population, 1.); + + ASSERT_THAT(print_wrap(read_result), IsSuccess()); + + auto init_subcompartments = read_result.value(); + // Previous result. + Eigen::VectorXd compare(infectionState.get_count()); + compare << 863.05, 14.30625, 8.53125, 30.1125, 36.1875, 3.8125, 9.88, 3.52, 0.09, 0.25, 0.6888, 27.8712, 1.7; + + for (int i = 0; i < infectionState.get_count(); i++) { + EXPECT_NEAR(init_subcompartments[i], compare[i], 1e-4) << "at subcompartment number " << i; + } +} + +// Check some cases where computation of initial values for an LCT model based on RKI data should fail. +TEST(TestLCTParametersIo, ReadPopulationDataRKIFailure) +{ + ScalarType total_population = 1000.0; + auto start_date = mio::Date(2020, 6, 6); + + // Define parameters used for simulation and initialization. + mio::lsecir::Parameters parameters; + parameters.get() = 2.3; + parameters.get() = 3.3; + parameters.get() = 2.4; + parameters.get() = 1.8; + parameters.get() = 3.0; + + parameters.get() = 0.2; + parameters.get() = 0.08; + parameters.get() = 0.15; + parameters.get() = 0.22; + + // Define number of subcompartments. + std::vector vec_subcompartments((int)mio::lsecir::InfectionStateBase::Count, 1); + // Use subcompartments with a soujourn time of approximately one day in each subcompartment. + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::Exposed] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = + (int)round(parameters.get()); + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = + (int)round(parameters.get()); + // Both realistic distributions for times corresponding to InfectedCritical of the IDE model are exponential distributions. + vec_subcompartments[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 1; + mio::lsecir::InfectionState infectionState(vec_subcompartments); + + // Deactivate temporarily log output for next tests. + mio::set_log_level(mio::LogLevel::off); + + // Case where start_date is later than maximal provided date in file. + auto read_result1 = mio::lsecir::get_initial_data_from_file( + mio::path_join(TEST_DATA_DIR, "test_cases_all_germany.json"), start_date, infectionState, std::move(parameters), + total_population, 1.); + + ASSERT_THAT(print_wrap(read_result1), IsFailure(mio::StatusCode::OutOfRange)); + // Case where not all needed dates are provided. + auto read_result2 = + mio::lsecir::get_initial_data_from_file(mio::path_join(TEST_DATA_DIR, "cases_all_germany.json"), start_date, + infectionState, std::move(parameters), total_population, 1.); + + ASSERT_THAT(print_wrap(read_result2), IsFailure(mio::StatusCode::OutOfRange)); + + // Case with empty RKI data file. + auto read_result3 = + mio::lsecir::get_initial_data_from_file(mio::path_join(TEST_DATA_DIR, "test_empty_file.json"), start_date, + infectionState, std::move(parameters), total_population, 1.); + + ASSERT_THAT(print_wrap(read_result3), IsFailure(mio::StatusCode::InvalidFileFormat)); + + // Reactive log output. + mio::set_log_level(mio::LogLevel::warn); +} \ No newline at end of file diff --git a/cpp/tests/test_lct_secir.cpp b/cpp/tests/test_lct_secir.cpp new file mode 100644 index 0000000000..1c1b553a5f --- /dev/null +++ b/cpp/tests/test_lct_secir.cpp @@ -0,0 +1,550 @@ +/* +* Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +* +* Authors: Lena Ploetzke +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "lct_secir/model.h" +#include "lct_secir/infection_state.h" +#include "lct_secir/simulation.h" +#include "lct_secir/parameters.h" +#include "ode_secir/model.h" +#include "memilio/config.h" +#include "memilio/utils/time_series.h" +#include "memilio/epidemiology/uncertain_matrix.h" +#include "memilio/math/eigen.h" +#include "load_test_data.h" + +#include +#include +#include +#include "boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp" + +// Test confirms that default construction of an LCT model works. +TEST(TestLCTSecir, simulateDefault) +{ + ScalarType t0 = 0; + ScalarType tmax = 1; + ScalarType dt = 0.1; + + Eigen::VectorXd init = Eigen::VectorXd::Constant((int)mio::lsecir::InfectionStateBase::Count, 15); + init[0] = 200; + init[3] = 50; + init[5] = 30; + + mio::lsecir::Model model(init); + mio::TimeSeries result = mio::lsecir::simulate(t0, tmax, dt, model); + + EXPECT_NEAR(result.get_last_time(), tmax, 1e-10); + ScalarType sum_pop = init.sum(); + for (Eigen::Index i = 0; i < result.get_num_time_points(); i++) { + ASSERT_NEAR(sum_pop, result[i].sum(), 1e-5); + } +} + +/* Test compares the result for an LCT SECIR model with one single Subcompartment for each infection state + with the result of the equivalent ODE SECIR model. */ +TEST(TestLCTSecir, compareWithOdeSecir) +{ + ScalarType t0 = 0; + ScalarType tmax = 5; + ScalarType dt = 0.1; + + // Initialization vector for both models. + Eigen::VectorXd init = Eigen::VectorXd::Constant((int)mio::lsecir::InfectionStateBase::Count, 15); + init[0] = 200; + init[3] = 50; + init[5] = 30; + + // Define LCT model. + mio::lsecir::Model model_lct(init); + // Set Parameters. + model_lct.parameters.get() = 2 * 4.2 - 5.2; + model_lct.parameters.get() = 2 * (5.2 - 4.2); + model_lct.parameters.get() = 5.8; + model_lct.parameters.get() = 9.5; + model_lct.parameters.get() = 7.1; + + model_lct.parameters.get() = 0.05; + + mio::ContactMatrixGroup& contact_matrix_lct = model_lct.parameters.get(); + contact_matrix_lct[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10)); + contact_matrix_lct[0].add_damping(0.7, mio::SimulationTime(2.)); + + model_lct.parameters.get() = 0.7; + model_lct.parameters.get() = 0.25; + model_lct.parameters.get() = 50; + model_lct.parameters.get() = 0.1; + model_lct.parameters.get() = 0.09; + model_lct.parameters.get() = 0.2; + model_lct.parameters.get() = 0.25; + model_lct.parameters.get() = 0.3; + + // Simulate. + mio::TimeSeries result_lct = mio::lsecir::simulate( + t0, tmax, dt, model_lct, + std::make_shared>()); + + // Initialize ODE model with one age group. + mio::osecir::Model model_ode(1); + // Set initial distribution of the population. + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::Exposed}] = + init[Eigen::Index(mio::lsecir::InfectionStateBase::Exposed)]; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedNoSymptoms}] = + init[Eigen::Index(mio::lsecir::InfectionStateBase::InfectedNoSymptoms)]; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedNoSymptomsConfirmed}] = 0; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedSymptoms}] = + init[Eigen::Index(mio::lsecir::InfectionStateBase::InfectedSymptoms)]; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedSymptomsConfirmed}] = 0; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedSevere}] = + init[Eigen::Index(mio::lsecir::InfectionStateBase::InfectedSevere)]; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedCritical}] = + init[Eigen::Index(mio::lsecir::InfectionStateBase::InfectedCritical)]; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::Recovered}] = + init[Eigen::Index(mio::lsecir::InfectionStateBase::Recovered)]; + model_ode.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::Dead}] = + init[Eigen::Index(mio::lsecir::InfectionStateBase::Dead)]; + model_ode.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecir::InfectionState::Susceptible}, + init.sum()); + + // Set parameters according to the parameters of the LCT model. + // No restrictions by additional parameters. + model_ode.parameters.get() = std::numeric_limits::max(); + model_ode.parameters.get() = std::numeric_limits::max(); + + model_ode.parameters.set(50); + model_ode.parameters.set(0.1); + model_ode.parameters.get()[(mio::AgeGroup)0] = + 5.2; // TimeExposed = 2 * SerialInterval - IncubationTime. + model_ode.parameters.get()[(mio::AgeGroup)0] = + 4.2; // TimeInfectedNoSymptoms = 2* (IncubationTime - SerialInterval). + model_ode.parameters.get()[(mio::AgeGroup)0] = 5.8; + model_ode.parameters.get()[(mio::AgeGroup)0] = 9.5; + model_ode.parameters.get()[(mio::AgeGroup)0] = 7.1; + + mio::ContactMatrixGroup& contact_matrix_ode = model_ode.parameters.get(); + contact_matrix_ode[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10)); + contact_matrix_ode[0].add_damping(0.7, mio::SimulationTime(2.)); + + model_ode.parameters.get()[(mio::AgeGroup)0] = 0.05; + model_ode.parameters.get()[(mio::AgeGroup)0] = 0.7; + model_ode.parameters.get()[(mio::AgeGroup)0] = 0.09; + model_ode.parameters.get()[(mio::AgeGroup)0] = 0.25; + model_ode.parameters.get()[(mio::AgeGroup)0] = 0.2; + model_ode.parameters.get()[(mio::AgeGroup)0] = 0.25; + model_ode.parameters.get()[(mio::AgeGroup)0] = 0.3; + + // Simulate. + mio::TimeSeries result_ode = + simulate(t0, tmax, dt, model_ode, + std::make_shared>()); + + // Simulation results should be equal. + ASSERT_EQ(result_lct.get_num_time_points(), result_ode.get_num_time_points()); + for (int i = 0; i < 4; ++i) { + ASSERT_NEAR(result_lct.get_time(i), result_ode.get_time(i), 1e-5); + + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::Susceptible], + result_ode[i][(int)mio::osecir::InfectionState::Susceptible], 1e-5); + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::Exposed], + result_ode[i][(int)mio::osecir::InfectionState::Exposed], 1e-5); + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms], + result_ode[i][(int)mio::osecir::InfectionState::InfectedNoSymptoms], 1e-5); + ASSERT_NEAR(0, result_ode[i][(int)mio::osecir::InfectionState::InfectedNoSymptomsConfirmed], 1e-5); + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::InfectedSymptoms], + result_ode[i][(int)mio::osecir::InfectionState::InfectedSymptoms], 1e-5); + ASSERT_NEAR(0, result_ode[i][(int)mio::osecir::InfectionState::InfectedSymptomsConfirmed], 1e-5); + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::InfectedCritical], + result_ode[i][(int)mio::osecir::InfectionState::InfectedCritical], 1e-5); + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::InfectedSevere], + result_ode[i][(int)mio::osecir::InfectionState::InfectedSevere], 1e-5); + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::Recovered], + result_ode[i][(int)mio::osecir::InfectionState::Recovered], 1e-5); + ASSERT_NEAR(result_lct[i][(int)mio::lsecir::InfectionStateBase::Dead], + result_ode[i][(int)mio::osecir::InfectionState::Dead], 1e-5); + } +} + +// Test if the function eval_right_hand_side() is working using a hand calculated result. +TEST(TestLCTSecir, testEvalRightHandSide) +{ + // Define model. + /* Number of subcompartments, chose more than one subcompartment for all compartments except S, R, D + so that the function is correct for all selections. */ + std::vector SubcompartmentNumbers((int)mio::lsecir::InfectionStateBase::Count, 1); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Exposed] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = 3; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 2; + mio::lsecir::InfectionState InfState(SubcompartmentNumbers); + + // Define initial population distribution in infection states, one entry per subcompartment. + Eigen::VectorXd init(InfState.get_count()); + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Susceptible)] = 750; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Exposed)] = 30; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Exposed) + 1] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms)] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms) + 1] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms) + 2] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSymptoms)] = 30; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSymptoms) + 1] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSevere)] = 40; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSevere) + 1] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical)] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 1] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Recovered)] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Dead)] = 10; + + mio::lsecir::Model model(std::move(init), InfState); + + // Set parameters. + model.parameters.set(2 * 4.2 - 5.2); + model.parameters.get() = 2 * (5.2 - 4.2); + model.parameters.get() = 5.8; + model.parameters.get() = 9.5; + model.parameters.get() = 7.1; + + model.parameters.get() = 0.05; + + mio::ContactMatrixGroup& contact_matrix = model.parameters.get(); + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10)); + + model.parameters.get() = 0.7; + model.parameters.get() = 0.25; + model.parameters.get() = 0.09; + model.parameters.get() = 0.; + model.parameters.get() = 0; + model.parameters.get() = 0.2; + model.parameters.get() = 0.25; + model.parameters.get() = 0.3; + + // Compare the result of eval_right_hand_side() with a hand calculated result. + size_t num_subcompartments = model.infectionState.get_count(); + Eigen::VectorXd dydt(num_subcompartments); + model.eval_right_hand_side(model.get_initial_values(), 0, dydt); + + Eigen::VectorXd compare(num_subcompartments); + compare << -15.3409, -3.4091, 6.25, -17.5, 15, 0, 3.3052, 3.4483, -7.0417, 6.3158, -2.2906, -2.8169, 12.3899, + 1.6901; + + for (size_t i = 0; i < num_subcompartments; i++) { + ASSERT_NEAR(compare[i], dydt[i], 1e-3); + } +} + +// Model setup to compare result with a previous output. +class ModelTestLCTSecir : public testing::Test +{ +protected: + virtual void SetUp() + { + // Define number of subcompartments. + std::vector SubcompartmentNumbers((int)mio::lsecir::InfectionStateBase::Count, 1); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Exposed] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = 3; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 5; + mio::lsecir::InfectionState InfState(SubcompartmentNumbers); + + // Define initial population distribution in infection states, one entry per Subcompartment. + Eigen::VectorXd init(InfState.get_count()); + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Susceptible)] = 750; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Exposed)] = 30; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Exposed) + 1] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms)] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms) + 1] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedNoSymptoms) + 2] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSymptoms)] = 50; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedSevere)] = 50; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical)] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 1] = 10; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 2] = 5; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 3] = 3; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::InfectedCritical) + 4] = 2; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Recovered)] = 20; + init[InfState.get_firstindex(mio::lsecir::InfectionStateBase::Dead)] = 10; + + // Initialize model and set parameters. + model = new mio::lsecir::Model(std::move(init), InfState); + model->parameters.get() = 2 * 4.2 - 5.2; + model->parameters.get() = 2 * (5.2 - 4.2); + model->parameters.get() = 5.8; + model->parameters.get() = 9.5; + model->parameters.get() = 7.1; + model->parameters.get() = 0.05; + + mio::ContactMatrixGroup& contact_matrix = model->parameters.get(); + contact_matrix[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 10)); + contact_matrix[0].add_damping(0.7, mio::SimulationTime(2.)); + + model->parameters.get() = 0.7; + model->parameters.get() = 0.25; + model->parameters.get() = 0.09; + model->parameters.get() = 0.2; + model->parameters.get() = 0.25; + model->parameters.get() = 0.3; + } + + virtual void TearDown() + { + delete model; + } + +public: + mio::lsecir::Model* model = nullptr; +}; + +// Test compares a simulation with the result of a previous run stored in a .csv file. +TEST_F(ModelTestLCTSecir, compareWithPreviousRun) +{ + ScalarType tmax = 3; + mio::TimeSeries result = mio::lsecir::simulate( + 0, tmax, 0.5, *model, + std::make_shared>()); + + // Compare subcompartments. + auto compare = load_test_data_csv("lct-secir-subcompartments-compare.csv"); + + ASSERT_EQ(compare.size(), static_cast(result.get_num_time_points())); + for (size_t i = 0; i < compare.size(); i++) { + ASSERT_EQ(compare[i].size(), static_cast(result.get_num_elements()) + 1) << "at row " << i; + ASSERT_NEAR(result.get_time(i), compare[i][0], 1e-7) << "at row " << i; + for (size_t j = 1; j < compare[i].size(); j++) { + ASSERT_NEAR(result.get_value(i)[j - 1], compare[i][j], 1e-7) << " at row " << i; + } + } + + // Compare base compartments. + mio::TimeSeries population = model->calculate_populations(result); + auto compare_population = load_test_data_csv("lct-secir-compartments-compare.csv"); + + ASSERT_EQ(compare_population.size(), static_cast(population.get_num_time_points())); + for (size_t i = 0; i < compare_population.size(); i++) { + ASSERT_EQ(compare_population[i].size(), static_cast(population.get_num_elements()) + 1) + << "at row " << i; + ASSERT_NEAR(population.get_time(i), compare_population[i][0], 1e-7) << "at row " << i; + for (size_t j = 1; j < compare_population[i].size(); j++) { + ASSERT_NEAR(population.get_value(i)[j - 1], compare_population[i][j], 1e-7) << " at row " << i; + } + } +} + +// Test some additional functions and exceptions of the model. +TEST_F(ModelTestLCTSecir, testModelFunctions) +{ + // Test calculate_populations with a vector of a wrong size. + // Deactivate temporarily log output because an error is expected. + mio::set_log_level(mio::LogLevel::off); + mio::TimeSeries init_wrong_size((int)mio::lsecir::InfectionStateBase::Count); + Eigen::VectorXd vec_wrong_size = Eigen::VectorXd::Ones((int)mio::lsecir::InfectionStateBase::Count); + init_wrong_size.add_time_point(-10, vec_wrong_size); + init_wrong_size.add_time_point(-9, vec_wrong_size); + mio::TimeSeries population = model->calculate_populations(init_wrong_size); + EXPECT_EQ(1, population.get_num_time_points()); + for (int i = 0; i < population.get_num_elements(); i++) { + EXPECT_EQ(-1, population.get_last_value()[i]); + } + // Reactive log output. + mio::set_log_level(mio::LogLevel::warn); + + // Test get heading functions. + EXPECT_TRUE(model->get_heading_CompartmentsBase().compare("S | E | C | I | H | U | R | D") == 0); + EXPECT_TRUE(model->get_heading_Subcompartments().compare( + "S | E1 | E2 | C1 | C2 | C3 | I | H | U1 | U2 | U3 | U4 | U5 | R | D") == 0); + + // Test with other Subcompartments. + std::vector SubcompartmentNumbers((int)mio::lsecir::InfectionStateBase::Count, 1); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = 2; + mio::lsecir::InfectionState InfState(SubcompartmentNumbers); + + mio::lsecir::Model model2(std::move(Eigen::VectorXd::Ones(InfState.get_count())), InfState); + EXPECT_TRUE(model2.get_heading_Subcompartments().compare("S | E | C | I1 | I2 | H1 | H2 | U | R | D") == 0); +} + +// Check constraints of InfectionState and Parameters. +TEST(TestLCTSecir, testConstraints) +{ + // Deactivate temporarily log output for next tests. + mio::set_log_level(mio::LogLevel::off); + + // Check InfectionState with a wrong size of the initialization vector. + std::vector SubcompartmentNumbers1((int)mio::lsecir::InfectionStateBase::Count - 1, 1); + mio::lsecir::InfectionState InfState(SubcompartmentNumbers1); + bool constraint_check = InfState.check_constraints(); + EXPECT_TRUE(constraint_check); + + // Check with right size but wrong number of Subcompartments for Susceptibles. + std::vector SubcompartmentNumbers((int)mio::lsecir::InfectionStateBase::Count, 1); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Susceptible] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Exposed] = 2; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedNoSymptoms] = 3; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSymptoms] = 4; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedSevere] = 5; + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::InfectedCritical] = 6; + InfState.set_SubcompartmentNumbers(SubcompartmentNumbers); + constraint_check = InfState.check_constraints(); + EXPECT_TRUE(constraint_check); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Susceptible] = 1; + + // Wrong number of subcompartments for Recovered. + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Recovered] = 5; + InfState.set_SubcompartmentNumbers(SubcompartmentNumbers); + constraint_check = InfState.check_constraints(); + EXPECT_TRUE(constraint_check); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Recovered] = 1; + + // For Dead. + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Dead] = 3; + InfState.set_SubcompartmentNumbers(SubcompartmentNumbers); + constraint_check = InfState.check_constraints(); + EXPECT_TRUE(constraint_check); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Dead] = 1; + + // Check if number of Subcompartments is zero. + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Exposed] = 0; + InfState.set_SubcompartmentNumbers(SubcompartmentNumbers); + constraint_check = InfState.check_constraints(); + EXPECT_TRUE(constraint_check); + SubcompartmentNumbers[(int)mio::lsecir::InfectionStateBase::Exposed] = 2; + + // Check with correct parameters. + InfState.set_SubcompartmentNumbers(SubcompartmentNumbers); + constraint_check = InfState.check_constraints(); + EXPECT_FALSE(constraint_check); + + // Check for exceptions of parameters. + mio::lsecir::Parameters parameters_lct; + parameters_lct.get() = 0; + parameters_lct.get() = 3.1; + parameters_lct.get() = 6.1; + parameters_lct.get() = 11.1; + parameters_lct.get() = 17.1; + parameters_lct.get() = 0.01; + mio::ContactMatrixGroup contact_matrix = mio::ContactMatrixGroup(1, 1); + parameters_lct.get() = mio::UncertainContactMatrix(contact_matrix); + + parameters_lct.get() = 1; + parameters_lct.get() = 1; + parameters_lct.get() = 0.1; + parameters_lct.get() = 0.1; + parameters_lct.get() = 0.1; + parameters_lct.get() = 0.1; + + // Check improper TimeExposed. + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 3.1; + + // Check TimeInfectedNoSymptoms. + parameters_lct.get() = 0.1; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 3.1; + + // Check TimeInfectedSymptoms. + parameters_lct.get() = -0.1; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 6.1; + + // Check TimeInfectedSevere. + parameters_lct.get() = 0.5; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 11.1; + + // Check TimeInfectedCritical. + parameters_lct.get() = 0.; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 17.1; + + // Check TransmissionProbabilityOnContact. + parameters_lct.get() = -1; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 0.01; + + // Check RelativeTransmissionNoSymptoms. + parameters_lct.get() = 1.5; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 1; + + // Check RiskOfInfectionFromSymptomatic. + parameters_lct.get() = 1.5; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 1; + + // Check RecoveredPerInfectedNoSymptoms. + parameters_lct.get() = 1.5; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 0.1; + + // Check SeverePerInfectedSymptoms. + parameters_lct.get() = -1; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 0.1; + + // Check CriticalPerSevere. + parameters_lct.get() = -1; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 0.1; + + // Check DeathsPerCritical. + parameters_lct.get() = -1; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 0.1; + + // Check Seasonality. + parameters_lct.get() = 1; + constraint_check = parameters_lct.check_constraints(); + EXPECT_TRUE(constraint_check); + parameters_lct.get() = 0.1; + + // Check with correct parameters. + constraint_check = parameters_lct.check_constraints(); + EXPECT_FALSE(constraint_check); + + // Check for model. + // Check wrong size of initial value vector. + mio::lsecir::Model model1(std::move(Eigen::VectorXd::Ones(InfState.get_count() - 1)), InfState, + std::move(parameters_lct)); + constraint_check = model1.check_constraints(); + EXPECT_TRUE(constraint_check); + + // Check with values smaller than zero. + mio::lsecir::Model model2(std::move(Eigen::VectorXd::Constant(InfState.get_count(), -1)), InfState, + std::move(parameters_lct)); + constraint_check = model2.check_constraints(); + EXPECT_TRUE(constraint_check); + + // Check with correct conditions. + mio::lsecir::Model model3(std::move(Eigen::VectorXd::Constant(InfState.get_count(), 100)), InfState, + std::move(parameters_lct)); + constraint_check = model3.check_constraints(); + EXPECT_FALSE(constraint_check); + + // Reactive log output. + mio::set_log_level(mio::LogLevel::warn); +} \ No newline at end of file diff --git a/cpp/tests/test_state_age_function.cpp b/cpp/tests/test_state_age_function.cpp index 1acfced877..d642f0bd8d 100644 --- a/cpp/tests/test_state_age_function.cpp +++ b/cpp/tests/test_state_age_function.cpp @@ -18,126 +18,174 @@ * limitations under the License. */ -#include "boost/fusion/functional/invocation/invoke.hpp" -#include "load_test_data.h" -#include "ide_secir/infection_state.h" -#include "ide_secir/model.h" -#include "ide_secir/parameters.h" -#include "ide_secir/simulation.h" -#include "memilio/math/eigen.h" -#include "memilio/utils/time_series.h" -#include "memilio/utils/logging.h" #include "memilio/config.h" -#include "memilio/epidemiology/uncertain_matrix.h" #include "memilio/epidemiology/state_age_function.h" -#include + #include TEST(TestStateAgeFunction, testSpecialMember) -{ +{ /* Copy and move (assignment) are defined in base class StateAgeFunction and are equal for all derived classes, therefore test for one Special Member. + Constructors of other members will be tested in other tests. */ ScalarType dt = 0.5; - mio::SmootherCosine smoothcos(1.0); + mio::GammaSurvivalFunction gamma(1.0, 2.0, 3); // constructor - EXPECT_EQ(smoothcos.get_parameter(), 1.0); - EXPECT_EQ(smoothcos.get_support_max(dt), 1.0); + EXPECT_EQ(gamma.get_parameter(), 1.0); + EXPECT_EQ(gamma.get_location(), 2); + EXPECT_EQ(gamma.get_scale(), 3); + EXPECT_NEAR(gamma.get_support_max(dt), 71.5, 1e-14); // copy - mio::SmootherCosine smoothcos2(smoothcos); - EXPECT_EQ(smoothcos.get_state_age_function_type(), smoothcos2.get_state_age_function_type()); - EXPECT_EQ(smoothcos.get_parameter(), smoothcos2.get_parameter()); - EXPECT_EQ(smoothcos.get_support_max(dt), smoothcos2.get_support_max(dt)); + mio::GammaSurvivalFunction gamma2(gamma); + EXPECT_EQ(gamma.get_state_age_function_type(), gamma2.get_state_age_function_type()); + EXPECT_EQ(gamma.get_parameter(), gamma2.get_parameter()); + EXPECT_EQ(gamma.get_location(), gamma2.get_location()); + EXPECT_EQ(gamma.get_scale(), gamma2.get_scale()); + EXPECT_EQ(gamma.get_support_max(dt), gamma2.get_support_max(dt)); // check copy is true copy, not reference - smoothcos.set_parameter(2.0); - EXPECT_NE(smoothcos.get_parameter(), smoothcos2.get_parameter()); - smoothcos.set_parameter(1.0); + gamma.set_parameter(2.0); + EXPECT_NE(gamma.get_parameter(), gamma2.get_parameter()); + gamma.set_parameter(1.0); // move - mio::SmootherCosine smoothcos3(std::move(smoothcos2)); - EXPECT_EQ(smoothcos.get_state_age_function_type(), smoothcos3.get_state_age_function_type()); - EXPECT_EQ(smoothcos.get_parameter(), smoothcos3.get_parameter()); - EXPECT_EQ(smoothcos.get_support_max(dt), smoothcos3.get_support_max(dt)); + mio::GammaSurvivalFunction gamma3(std::move(gamma2)); + EXPECT_EQ(gamma.get_state_age_function_type(), gamma3.get_state_age_function_type()); + EXPECT_EQ(gamma.get_parameter(), gamma3.get_parameter()); + EXPECT_EQ(gamma.get_location(), gamma3.get_location()); + EXPECT_EQ(gamma.get_scale(), gamma3.get_scale()); + EXPECT_EQ(gamma.get_support_max(dt), gamma3.get_support_max(dt)); // copy assignment - mio::SmootherCosine smoothcos4 = smoothcos3; - EXPECT_EQ(smoothcos.get_state_age_function_type(), smoothcos4.get_state_age_function_type()); - EXPECT_EQ(smoothcos.get_parameter(), smoothcos4.get_parameter()); - EXPECT_EQ(smoothcos.get_support_max(dt), smoothcos4.get_support_max(dt)); + mio::GammaSurvivalFunction gamma4 = gamma3; + EXPECT_EQ(gamma.get_state_age_function_type(), gamma4.get_state_age_function_type()); + EXPECT_EQ(gamma.get_parameter(), gamma4.get_parameter()); + EXPECT_EQ(gamma.get_location(), gamma4.get_location()); + EXPECT_EQ(gamma.get_scale(), gamma4.get_scale()); + EXPECT_EQ(gamma.get_support_max(dt), gamma4.get_support_max(dt)); // check copy is true copy, not reference - smoothcos.set_parameter(2.0); - EXPECT_NE(smoothcos.get_parameter(), smoothcos4.get_parameter()); - smoothcos.set_parameter(1.0); + gamma.set_scale(2.0); + EXPECT_NE(gamma.get_scale(), gamma4.get_scale()); + gamma.set_scale(3.0); // move assignment - mio::SmootherCosine smoothcos5 = std::move(smoothcos4); - EXPECT_EQ(smoothcos.get_state_age_function_type(), smoothcos5.get_state_age_function_type()); - EXPECT_EQ(smoothcos.get_parameter(), smoothcos5.get_parameter()); - EXPECT_EQ(smoothcos.get_support_max(dt), smoothcos5.get_support_max(dt)); - - // also test the constructor of ExponentialDecay and ConstantFunction - // copy and move (assignment) are defined in base class StateAgeFunction and are equal for all derived classes - mio::ExponentialDecay expdecay(1.0); - EXPECT_EQ(expdecay.get_parameter(), 1.0); - - mio::ConstantFunction constfunc(1.0); - EXPECT_EQ(constfunc.get_parameter(), 1.0); + mio::GammaSurvivalFunction gamma5 = std::move(gamma4); + EXPECT_EQ(gamma.get_state_age_function_type(), gamma5.get_state_age_function_type()); + EXPECT_EQ(gamma.get_parameter(), gamma5.get_parameter()); + EXPECT_EQ(gamma.get_location(), gamma5.get_location()); + EXPECT_EQ(gamma.get_scale(), gamma5.get_scale()); + EXPECT_EQ(gamma.get_support_max(dt), gamma5.get_support_max(dt)); } TEST(TestStateAgeFunction, testSettersAndGettersForParameter) { - ScalarType testvalue_before = 1.0; + ScalarType testvalue_before = 0.5; ScalarType testvalue_after = 2.0; // test get and set for function parameter - // only for SmootherCosine as set_parameter and get_parameter are equal for all derived classes - mio::SmootherCosine smoothcos(testvalue_before); - EXPECT_EQ(smoothcos.get_parameter(), testvalue_before); - - smoothcos.set_parameter(testvalue_after); - EXPECT_EQ(smoothcos.get_parameter(), testvalue_after); + // only for GammaSurvivalfunction as set_parameter and get_parameter are equal for all derived classes + mio::GammaSurvivalFunction gamma(testvalue_before, testvalue_before, -1); + + EXPECT_EQ(gamma.get_parameter(), testvalue_before); + gamma.set_parameter(testvalue_after); + EXPECT_EQ(gamma.get_parameter(), testvalue_after); + + EXPECT_EQ(gamma.get_location(), testvalue_before); + gamma.set_location(testvalue_after); + EXPECT_EQ(gamma.get_location(), testvalue_after); + + EXPECT_EQ(gamma.get_scale(), 1); + gamma.set_scale(-1); + EXPECT_EQ(gamma.get_scale(), 1); + gamma.set_scale(testvalue_after); + EXPECT_EQ(gamma.get_scale(), testvalue_after); } TEST(TestStateAgeFunction, testGetSupportMax) { ScalarType dt = 0.5; - // test get_support_max for all derived classes as this method can be overridden - // Check that the maximum support is correct after setting the parameter object of a StateAgeFunction. - mio::ExponentialDecay expdecay(1.0); - EXPECT_NEAR(expdecay.get_support_max(dt), 23.5, 1e-14); + /* Test get_support_max for all derived classes as this method can be overridden. + Check that the maximum support is correct after setting the parameters of a StateAgeFunction. */ + + mio::ExponentialDecay expdecay(1.0, 1.0); + EXPECT_NEAR(expdecay.get_support_max(dt), 24.5, 1e-14); expdecay.set_parameter(2.0); - EXPECT_NEAR(expdecay.get_support_max(dt), 12.0, 1e-14); + EXPECT_NEAR(expdecay.get_support_max(dt), 13.0, 1e-14); + expdecay.set_location(1.0); + EXPECT_NEAR(expdecay.get_support_max(dt), 13.0, 1e-14); + expdecay.set_scale(300.0); + EXPECT_NEAR(expdecay.get_support_max(dt), 13.0, 1e-14); - mio::SmootherCosine smoothcos(1.0); - EXPECT_NEAR(smoothcos.get_support_max(dt), 1.0, 1e-14); + mio::SmootherCosine smoothcos(1.0, 1.); + EXPECT_NEAR(smoothcos.get_support_max(dt), 2.0, 1e-14); smoothcos.set_parameter(2.0); + EXPECT_NEAR(smoothcos.get_support_max(dt), 3.0, 1e-14); + smoothcos.set_location(0); EXPECT_NEAR(smoothcos.get_support_max(dt), 2.0, 1e-14); + smoothcos.set_scale(300.0); + EXPECT_NEAR(smoothcos.get_support_max(dt), 2.0, 1e-14); + + mio::GammaSurvivalFunction gamma(1.0, 1, 0.5); + EXPECT_NEAR(gamma.get_support_max(dt), 13, 1e-14); + gamma.set_parameter(0.5); + EXPECT_NEAR(gamma.get_support_max(dt), 11.5, 1e-14); + gamma.set_location(0); + EXPECT_NEAR(gamma.get_support_max(dt), 10.5, 1e-14); + gamma.set_scale(1); + EXPECT_NEAR(gamma.get_support_max(dt), 21, 1e-14); + + mio::LognormSurvivalFunction logn(0.1, 1, 0.5); + EXPECT_NEAR(logn.get_support_max(dt), 2, 1e-14); + logn.set_parameter(0.5); + EXPECT_NEAR(logn.get_support_max(dt), 13.5, 1e-14); + logn.set_location(0); + EXPECT_NEAR(logn.get_support_max(dt), 12.5, 1e-14); + logn.set_scale(0.1); + EXPECT_NEAR(logn.get_support_max(dt), 2.5, 1e-14); mio::ConstantFunction constfunc(1.0); EXPECT_NEAR(constfunc.get_support_max(dt), -2.0, 1e-14); constfunc.set_parameter(2.0); EXPECT_NEAR(constfunc.get_support_max(dt), -2.0, 1e-14); + constfunc.set_location(2.0); + EXPECT_NEAR(constfunc.get_support_max(dt), -2.0, 1e-14); + constfunc.set_scale(2.0); + EXPECT_NEAR(constfunc.get_support_max(dt), -2.0, 1e-14); + + mio::ErlangDensity erl(2, 0.5); + EXPECT_NEAR(erl.get_support_max(dt), 14, 1e-14); + erl.set_parameter(1); + EXPECT_NEAR(erl.get_support_max(dt), 12, 1e-14); + erl.set_scale(0.1); + EXPECT_NEAR(erl.get_support_max(dt), 3, 1e-14); + erl.set_location(300); + EXPECT_NEAR(erl.get_support_max(dt), 3, 1e-14); } TEST(TestStateAgeFunction, testSAFWrapperSpecialMember) -{ +{ // Same as testSpecialMember for Wrapper. + ScalarType dt = 0.5; - mio::SmootherCosine smoothcos(1.0); - mio::StateAgeFunctionWrapper wrapper(smoothcos); + mio::GammaSurvivalFunction gamma(1.0, 2.0, 3.0); + mio::StateAgeFunctionWrapper wrapper(gamma); // constructor EXPECT_EQ(wrapper.get_parameter(), 1.0); - EXPECT_EQ(wrapper.get_support_max(dt), 1.0); + EXPECT_EQ(wrapper.get_location(), 2); + EXPECT_EQ(wrapper.get_scale(), 3); + EXPECT_NEAR(wrapper.get_support_max(dt), 71.5, 1e-14); // copy mio::StateAgeFunctionWrapper wrapper2(wrapper); EXPECT_EQ(wrapper.get_state_age_function_type(), wrapper2.get_state_age_function_type()); EXPECT_EQ(wrapper.get_parameter(), wrapper2.get_parameter()); + EXPECT_EQ(wrapper.get_location(), wrapper2.get_location()); + EXPECT_EQ(wrapper.get_scale(), wrapper2.get_scale()); EXPECT_EQ(wrapper.get_support_max(dt), wrapper2.get_support_max(dt)); - // test true copy, not reference + // check copy is true copy, not reference wrapper.set_parameter(2.0); EXPECT_NE(wrapper.get_parameter(), wrapper2.get_parameter()); wrapper.set_parameter(1.0); @@ -146,78 +194,150 @@ TEST(TestStateAgeFunction, testSAFWrapperSpecialMember) mio::StateAgeFunctionWrapper wrapper3(std::move(wrapper2)); EXPECT_EQ(wrapper.get_state_age_function_type(), wrapper3.get_state_age_function_type()); EXPECT_EQ(wrapper.get_parameter(), wrapper3.get_parameter()); + EXPECT_EQ(wrapper.get_location(), wrapper3.get_location()); + EXPECT_EQ(wrapper.get_scale(), wrapper3.get_scale()); EXPECT_EQ(wrapper.get_support_max(dt), wrapper3.get_support_max(dt)); // copy assignment mio::StateAgeFunctionWrapper wrapper4 = wrapper3; EXPECT_EQ(wrapper.get_state_age_function_type(), wrapper4.get_state_age_function_type()); EXPECT_EQ(wrapper.get_parameter(), wrapper4.get_parameter()); + EXPECT_EQ(wrapper.get_location(), wrapper4.get_location()); + EXPECT_EQ(wrapper.get_scale(), wrapper4.get_scale()); EXPECT_EQ(wrapper.get_support_max(dt), wrapper4.get_support_max(dt)); - // test true copy, not reference - wrapper.set_parameter(2.0); - EXPECT_NE(wrapper.get_parameter(), wrapper4.get_parameter()); - wrapper.set_parameter(1.0); + // check copy is true copy, not reference + wrapper.set_scale(2.0); + EXPECT_NE(wrapper.get_scale(), wrapper4.get_scale()); + wrapper.set_scale(3.0); // move assignment mio::StateAgeFunctionWrapper wrapper5 = std::move(wrapper4); EXPECT_EQ(wrapper.get_state_age_function_type(), wrapper5.get_state_age_function_type()); EXPECT_EQ(wrapper.get_parameter(), wrapper5.get_parameter()); + EXPECT_EQ(wrapper.get_location(), wrapper5.get_location()); + EXPECT_EQ(wrapper.get_scale(), wrapper5.get_scale()); EXPECT_EQ(wrapper.get_support_max(dt), wrapper5.get_support_max(dt)); - // Also test the constructor of StateAgeFunctionWrapper initialized with ExponentialDecay and ConstantFunction. - // This way we can be sure that clone_impl works for all derived classes of StateAgeFunction. - mio::ExponentialDecay expdecay(1.0); + /* Also test the constructor of StateAgeFunctionWrapper initialized with ExponentialDecay and ConstantFunction. + This way we can be sure that clone_impl works for all derived classes of StateAgeFunction.*/ + mio::ExponentialDecay expdecay(1.0, 1.0); mio::StateAgeFunctionWrapper wrapper_exp(expdecay); - EXPECT_NEAR(wrapper_exp.get_parameter(), 1.0, 1e-14); - EXPECT_NEAR(wrapper_exp.get_support_max(dt), 23.5, 1e-14); + EXPECT_EQ(wrapper_exp.get_parameter(), 1.0); + EXPECT_EQ(wrapper_exp.get_location(), 1.0); + EXPECT_NEAR(wrapper_exp.get_support_max(dt), 24.5, 1e-14); + + mio::SmootherCosine smoothcos(1.0, 1.); + mio::StateAgeFunctionWrapper wrapper_smoothcos(smoothcos); + EXPECT_EQ(wrapper_smoothcos.get_parameter(), 1.0); + EXPECT_EQ(wrapper_smoothcos.get_location(), 1.0); + EXPECT_NEAR(wrapper_smoothcos.get_support_max(dt), 2.0, 1e-14); + + mio::LognormSurvivalFunction logn(0.1, 1, 0.5); + mio::StateAgeFunctionWrapper wrapper_logn(logn); + EXPECT_EQ(wrapper_logn.get_parameter(), 0.1); + EXPECT_EQ(wrapper_logn.get_location(), 1.0); + EXPECT_EQ(wrapper_logn.get_scale(), 0.5); + EXPECT_NEAR(wrapper_logn.get_support_max(dt), 2.0, 1e-14); mio::ConstantFunction constfunc(1.0); mio::StateAgeFunctionWrapper wrapper_const(constfunc); - EXPECT_NEAR(wrapper_const.get_parameter(), 1.0, 1e-14); + EXPECT_EQ(wrapper_const.get_parameter(), 1.); EXPECT_NEAR(wrapper_const.get_support_max(dt), -2.0, 1e-14); + + // test if set_state_age_function works. + mio::ErlangDensity erl(2, 0.5); + wrapper_const.set_state_age_function(erl); + EXPECT_EQ(wrapper_const.get_parameter(), 2); + EXPECT_EQ(wrapper_const.get_scale(), 0.5); + EXPECT_NEAR(wrapper_const.get_support_max(dt), 14, 1e-14); } TEST(TestStateAgeFunction, testSAFWrapperSettersAndGetters) { - // test only for SmootherCosine since we have checked set_parameter and get_parameter - // as well as get_support_max for all derived classes in previous test + /* Test Getter and Setter for Wrapper only for GammaSurvivalFunction since we have checked getter and setter + as well as get_support_max for all derived classes in previous test. */ - ScalarType testvalue_before = 1.0; - ScalarType testvalue_after = 2.0; - ScalarType dt = 0.5; - - mio::SmootherCosine smoothcos(testvalue_before); - mio::StateAgeFunctionWrapper wrapper(smoothcos); - - EXPECT_EQ(wrapper.get_parameter(), testvalue_before); - EXPECT_EQ(wrapper.get_support_max(dt), testvalue_before); + ScalarType dt = 0.5; - wrapper.set_parameter(testvalue_after); - EXPECT_EQ(wrapper.get_parameter(), testvalue_after); - EXPECT_EQ(wrapper.get_support_max(dt), testvalue_after); + mio::GammaSurvivalFunction gamma(1, 2, 3); + mio::StateAgeFunctionWrapper wrapper(gamma); + + EXPECT_EQ(wrapper.get_parameter(), 1); + EXPECT_EQ(wrapper.get_location(), 2); + EXPECT_EQ(wrapper.get_scale(), 3); + EXPECT_EQ(wrapper.get_support_max(dt), 71.5); + + wrapper.set_parameter(4); + wrapper.set_location(0); + wrapper.set_scale(1); + EXPECT_EQ(wrapper.get_parameter(), 4); + EXPECT_EQ(wrapper.get_location(), 0); + EXPECT_EQ(wrapper.get_scale(), 1); + EXPECT_EQ(wrapper.get_support_max(dt), 32); } TEST(TestStateAgeFunction, testComparisonOperator) { // Check that StateAgeFunctions are only considered equal if they are of the same derived - // class and have the same parameter - mio::ExponentialDecay expdecay(0.5); - mio::ExponentialDecay expdecay2(0.5); - mio::ExponentialDecay expdecay3(1.0); + // class and have the same parameters + mio::GammaSurvivalFunction gamma(0.5, 0, 1); + mio::GammaSurvivalFunction gamma2(0.5, 0, 1); + mio::GammaSurvivalFunction gamma3(2, 0, 1); + mio::GammaSurvivalFunction gamma4(0.5, 1.5, 1); + mio::GammaSurvivalFunction gamma5(0.5, 0, 2); mio::SmootherCosine smoothcos(0.5); - EXPECT_TRUE(expdecay == expdecay2); - EXPECT_FALSE(expdecay == expdecay3); - EXPECT_FALSE(expdecay == smoothcos); + EXPECT_TRUE(gamma == gamma2); + EXPECT_FALSE(gamma == gamma3); + EXPECT_FALSE(gamma == gamma4); + EXPECT_FALSE(gamma == gamma5); + EXPECT_FALSE(gamma == smoothcos); // Check that it also holds when a StateAgeFunctionWrapper is set with the respective functions - mio::StateAgeFunctionWrapper wrapper(expdecay); - mio::StateAgeFunctionWrapper wrapper2(expdecay2); - mio::StateAgeFunctionWrapper wrapper3(expdecay3); - mio::StateAgeFunctionWrapper wrapper4(smoothcos); + mio::StateAgeFunctionWrapper wrapper(gamma); + mio::StateAgeFunctionWrapper wrapper2(gamma2); + mio::StateAgeFunctionWrapper wrapper3(gamma3); + mio::StateAgeFunctionWrapper wrapper4(gamma3); + mio::StateAgeFunctionWrapper wrapper5(gamma3); + mio::StateAgeFunctionWrapper wrapper6(smoothcos); EXPECT_TRUE(wrapper == wrapper2); EXPECT_FALSE(wrapper == wrapper3); EXPECT_FALSE(wrapper == wrapper4); -} \ No newline at end of file + EXPECT_FALSE(wrapper == wrapper5); + EXPECT_FALSE(wrapper == wrapper6); +} + +TEST(TestStateAgeFunction, testGamma) +{ + // test if connection between Gammasurvivalfunction and ErlangDensity is fulfilled. + ScalarType rate[] = {0.5, 0.8, 1, 3}; + int shape[] = {1, 3, 5, 10, 20}; + ScalarType times[] = {0, 0.5, 3, 5.555, 20, 70.34}; + for (int r = 0; r < 4; r++) { + for (int s = 0; s < 5; s++) { + mio::GammaSurvivalFunction survival(shape[s], 0, 1. / (rate[r])); + EXPECT_EQ(survival.eval(0), 1.0); + mio::ErlangDensity density(1, 1. / (rate[r])); + EXPECT_EQ(density.eval(-1), 0.0); + + for (int tau = 0; tau < 6; tau++) { + ScalarType f = 0; + for (int k = 1; k < shape[s] + 1; k++) { + density.set_parameter(k); + f += density.eval(times[tau]); + } + EXPECT_NEAR(f * (1 / rate[r]), survival.eval(times[tau]), 1e-10); + } + } + } + // test if connection between Gammasurvivalfunction and ExponentialDecay (ExponentialSurvivalfunction) is fulfilled + for (int r = 0; r < 4; r++) { + mio::GammaSurvivalFunction gamma(1, 0, 1. / (rate[r])); + mio::ExponentialDecay exp(rate[r]); + for (int tau = 0; tau < 6; tau++) { + EXPECT_NEAR(gamma.eval(times[tau]), exp.eval(times[tau]), 1e-10); + } + } +} diff --git a/cpp/thirdparty/boost_1_75_0.tar.gz b/cpp/thirdparty/boost_1_75_0.tar.gz index 04d1e16733..f692b19bac 100644 Binary files a/cpp/thirdparty/boost_1_75_0.tar.gz and b/cpp/thirdparty/boost_1_75_0.tar.gz differ diff --git a/tools/plot_results_lct_secir.py b/tools/plot_results_lct_secir.py new file mode 100644 index 0000000000..db2d2dee2a --- /dev/null +++ b/tools/plot_results_lct_secir.py @@ -0,0 +1,469 @@ +############################################################################# +# Copyright (C) 2020-2023 German Aerospace Center (DLR-SC) +# +# Authors: Lena Ploetzke +# +# Contact: Martin J. Kuehn +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################# +"""@plot_results_lct_secir.py +Functions to plot and compare results of simulations with different kind of models, +eg LCT, IDE or ODE SECIR models without division in agegroups. + +The data to be plotted should be stored in a '../data/simulation_lct' folder as .h5 files. +Data could be generated eg by executing the file ./cpp/examples/lct_secir_initializations.cpp. +""" + +import h5py +import os +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +import math + +import memilio.epidata.getDataIntoPandasDataFrame as gd + +# Define compartments. +secir_dict = {0: 'Susceptible', 1: 'Exposed', 2: 'Carrier', 3: 'Infected', 4: 'Hospitalized', + 5: 'ICU', 6: 'Recovered', 7: 'Death'} + +# Define parameters used for simulation, used for plotting real data. +parameters = { + 'TimeExposed': 3.335, + 'TimeInfectedNoSymptoms': 3.31331, + 'TimeInfectedSymptoms': 6.94547, + 'TimeInfectedSevere': 11.634346, + 'TimeInfectedCritical': 17.476959, + 'RecoveredPerInfectedNoSymptoms': 0.206901, + 'start_date': pd.Timestamp('2020.10.01'), + 'end_date': pd.Timestamp('2020.10.01')+pd.DateOffset(days=45), + 'scaleConfirmed': 2. +} + +# Define color and style to be used while plotting for different models to make plots consistent. +color_dict = {'ODE': '#1f77b4', + 'LCT 3': '#2ca02c', + 'LCT 10': '#ff7f0e', + 'LCT 20': '#9467bd', + 'LCT var': '#d62728', + 'IDE 3': '#e377c2', + 'IDE 10': '#17becf' + } +linestyle_dict = {'ODE': 'solid', + 'LCT 3': 'solid', + 'LCT 10': 'solid', + 'LCT 20': 'dashdot', + 'LCT var': 'dashdot', + 'IDE 3': 'dashed', + 'IDE 10': 'dashed' + } + + +def load_data(file): + """ Loads RKI data and computes 'InfectedSymptoms', 'Deaths' and 'NewInfectionsDay' using scales, dates etc from the dictionary parameters. + Method matches the method for computing initial values for the LCT model. See also cpp/models/lct_secir/parameters_io.h. + @param[in] file Path to the RKI data file for whole Germany. Can be downloaded eg via pycode/memilio-epidata/memilio/epidata/getCaseData.py. + """ + # Read data. + df = pd.read_json(file) + df = df.drop(columns=['Recovered']) + + # Remove unnecessary dates. + df = df[(df['Date'] >= parameters['start_date']+pd.DateOffset(days=-math.ceil(parameters['TimeInfectedSymptoms']+parameters['TimeInfectedSevere']+parameters['TimeInfectedCritical']))) + & (df['Date'] <= parameters['end_date'] + pd.DateOffset(days=math.ceil(parameters['TimeExposed']+parameters['TimeInfectedNoSymptoms'])))] + # Scale confirmed cases because of undetected infections. + df['Confirmed'] = parameters['scaleConfirmed']*df['Confirmed'] + # df2 stores the result of the computation. + df2 = df.copy() + df2 = df2[(df['Date'] >= parameters['start_date']) + & (df['Date'] <= parameters['end_date'])] + df2 = df2.reset_index() + df2 = df2.drop(columns=['index', 'Confirmed', 'Deaths']) + # Calculate individuals in compartment InfectedSymptoms. + help_I = df['Confirmed'][(df['Date'] >= parameters['start_date']) + & (df['Date'] <= parameters['end_date'])].to_numpy() + help_I = help_I - (1-math.fmod(parameters['TimeInfectedSymptoms'], 1))*df['Confirmed'][(df['Date'] >= parameters['start_date']+pd.DateOffset(days=-math.floor(parameters['TimeInfectedSymptoms']))) + & (df['Date'] <= parameters['end_date'] + pd.DateOffset(days=-math.floor(parameters['TimeInfectedSymptoms'])))].to_numpy() + help_I = help_I - math.fmod(parameters['TimeInfectedSymptoms'], 1) * df['Confirmed'][(df['Date'] >= parameters['start_date']+pd.DateOffset(days=-math.ceil( + parameters['TimeInfectedSymptoms']))) & (df['Date'] <= parameters['end_date']+pd.DateOffset(days=-math.ceil(parameters['TimeInfectedSymptoms'])))].to_numpy() + df2['InfectedSymptoms'] = help_I + # Calculate number of dead individuals. + help_D = (1-(1-math.fmod(parameters['TimeInfectedSymptoms']+parameters['TimeInfectedSevere']+parameters['TimeInfectedCritical'], 1)))*df['Deaths'][(df['Date'] >= parameters['start_date']+pd.DateOffset(days=-math.ceil(parameters['TimeInfectedSymptoms']+parameters['TimeInfectedSevere']+parameters['TimeInfectedCritical']))) + & (df['Date'] <= parameters['end_date']+pd.DateOffset(days=-math.ceil(parameters['TimeInfectedSymptoms']+parameters['TimeInfectedSevere']+parameters['TimeInfectedCritical'])))].to_numpy() + help_D = help_D + (1-math.fmod(parameters['TimeInfectedSymptoms']+parameters['TimeInfectedSevere']+parameters['TimeInfectedCritical'], 1))*df['Deaths'][(df['Date'] >= parameters['start_date']+pd.DateOffset(days=-math.floor(parameters['TimeInfectedSymptoms']+parameters['TimeInfectedSevere']+parameters['TimeInfectedCritical']))) + & (df['Date'] <= parameters['end_date']+pd.DateOffset(days=-math.floor(parameters['TimeInfectedSymptoms']+parameters['TimeInfectedSevere']+parameters['TimeInfectedCritical'])))].to_numpy() + df2['Deaths'] = help_D + # Calculate new infections per day. + fmod = math.fmod( + parameters['TimeInfectedNoSymptoms']+parameters['TimeExposed'], 1) + help_newE = fmod*df['Confirmed'][(df['Date'] >= parameters['start_date']+pd.DateOffset(days=math.ceil(parameters['TimeInfectedNoSymptoms']+parameters['TimeExposed']))) + & (df['Date'] <= parameters['end_date'] + pd.DateOffset(days=math.ceil(parameters['TimeInfectedNoSymptoms']+parameters['TimeExposed'])))].to_numpy() + help_newE = help_newE+(1-2*fmod)*df['Confirmed'][(df['Date'] >= parameters['start_date']+pd.DateOffset(days=math.floor(parameters['TimeInfectedNoSymptoms']+parameters['TimeExposed']))) + & (df['Date'] <= parameters['end_date'] + pd.DateOffset(days=math.floor(parameters['TimeInfectedNoSymptoms']+parameters['TimeExposed'])))].to_numpy() + help_newE = help_newE-(1-fmod)*df['Confirmed'][(df['Date'] >= parameters['start_date']+pd.DateOffset(days=math.floor(parameters['TimeInfectedNoSymptoms']+parameters['TimeExposed']-1))) + & (df['Date'] <= parameters['end_date'] + pd.DateOffset(days=math.floor(parameters['TimeInfectedNoSymptoms']+parameters['TimeExposed']-1)))].to_numpy() + df2['NewInfectionsDay'] = help_newE / \ + (1-parameters['RecoveredPerInfectedNoSymptoms']) + return df2 + + +def compare_compartments_real(files, datafile, legendplot, deaths=False, filename_plot="compare_real"): + """ Plots simulation results compared with real data for the compartments Deaths and InfectedSymptoms. + The simulation results should consist of accumulated numbers for subcompartments in case of an LCT model. + + @param[in] files: Paths of the hdf5-files (without file extension .h5) with the simulation results that should be compared. + Results should contain exactly 8 compartments (so use accumulated numbers for LCT models). Names can be given in form of a list. + One could compare results with eg different parameters or different models. + @param[in] datafile: Path to the RKI data file for whole Germany. Can be downloaded eg via pycode/memilio-epidata/memilio/epidata/getCaseData.py. + @param[in] legendplot: list with names for the results that should be used for the legend of the plot. + @param[in] deaths: If False, InfectedSymptoms compartment is plotted, if True, deaths are plotted. + @param[in] filename_plot: Name to use as the file name for the saved plot. + """ + # Define plot. + plt.figure(filename_plot) + + data_rki_ma = load_data(datafile) + num_days = data_rki_ma.shape[0] + + if (deaths): + plt.plot(range(num_days), data_rki_ma['Deaths'], + linestyle='dashed', color='grey', linewidth=1.2) + compartment_idx = 7 + labely = "Todesfälle" + else: + plt.plot(range(num_days), data_rki_ma['InfectedSymptoms'], + linestyle='dashed', color='grey', linewidth=1.2) + compartment_idx = 3 + labely = "Anzahl der Individuen in I" + + # Add simulation results to plot. + for file in range(len(files)): + # Load data. + h5file = h5py.File(str(files[file]) + '.h5', 'r') + + if (len(list(h5file.keys())) > 1): + raise gd.DataError("File should contain one dataset.") + if (len(list(h5file[list(h5file.keys())[0]].keys())) > 3): + raise gd.DataError("Expected only one group.") + + data = h5file[list(h5file.keys())[0]] + dates = data['Time'][:] + # As there should be only one Group, total is the simulation result. + total = data['Total'][:, :] + if (total.shape[1] != 8): + raise gd.DataError( + "Expected a different number of compartments.") + + # Plot result. + plt.plot(dates, total[:, compartment_idx], + linewidth=1.2, linestyle=linestyle_dict[legendplot[1+file]], color=color_dict[legendplot[1+file]]) + h5file.close() + + plt.xlabel('Datum', fontsize=14) + plt.ylabel(labely, fontsize=14) + plt.xlim(left=0, right=num_days-1) + # Define x-ticks. + datelist = np.array(pd.date_range(parameters["start_date"].date(), + periods=num_days, freq='D').strftime('%m-%d').tolist()) + tick_range = (np.arange(int((num_days-1) / 5) + 1) * 5) + plt.xticks(tick_range, datelist[tick_range], + rotation=45, fontsize=12) + plt.xticks(np.arange(num_days), minor=True) + + plt.legend(legendplot, fontsize=12, framealpha=0.5) + plt.grid(True, linestyle='--') + plt.tight_layout() + + # Save result. + if not os.path.isdir('Plots'): + os.makedirs('Plots') + plt.savefig('Plots/'+filename_plot+'.png', bbox_inches='tight', dpi=500) + + +def plot_new_infections_real(files, datafile, legendplot, filename_plot="compare_new_infections_real"): + """ Plots simulation results compared with real data for new infections within one day. + The simulation results should consist of accumulated numbers for subcompartments in case of an LCT model. + + @param[in] files: paths of the hdf5-files (without file extension .h5) with the simulation results that should be compared. + Results should contain exactly 8 compartments (so use accumulated numbers for LCT models). Names can be given in form of a list. + One could compare results with eg different parameters or different models. + @param[in] datafile: Path to the RKI data file for whole Germany. Can be downloaded eg via pycode/memilio-epidata/memilio/epidata/getCaseData.py. + @param[in] legendplot: list with names for the results that should be used for the legend of the plot. + @param[in] filename_plot: name to use as the file name for the saved plot. + """ + plt.figure(filename_plot) + + data_rki = load_data(datafile) + num_days = data_rki.shape[0] + + plt.plot(range(num_days), data_rki['NewInfectionsDay'], + linestyle='None', color='grey', marker='x', markersize=10) + + # Add simulation results to plot. + for file in range(len(files)): + # Load data. + h5file = h5py.File(str(files[file]) + '.h5', 'r') + + if (len(list(h5file.keys())) > 1): + raise gd.DataError("File should contain one dataset.") + if (len(list(h5file[list(h5file.keys())[0]].keys())) > 3): + raise gd.DataError("Expected only one group.") + + data = h5file[list(h5file.keys())[0]] + dates = data['Time'][:] + # As there should be only one Group, total is the simulation result. + total = data['Total'][:, :] + if (total.shape[1] != 8): + raise gd.DataError( + "Expected a different number of compartments.") + incidence = (total[:-1, 0]-total[1:, 0])/(dates[1:]-dates[:-1]) + + # Plot result. + plt.plot(dates[1:], incidence, + linewidth=1.2, linestyle=linestyle_dict[legendplot[1+file]], color=color_dict[legendplot[1+file]]) + h5file.close() + + plt.xlabel('Datum', fontsize=14) + plt.ylabel('Neuansteckungen pro Tag', fontsize=14) + plt.xlim(left=0, right=num_days-1) + # Define x-ticks. + datelist = np.array(pd.date_range(parameters["start_date"].date(), + periods=num_days, freq='D').strftime('%m-%d').tolist()) + tick_range = (np.arange(int((num_days - 1) / 5) + 1) * 5) + plt.xticks(tick_range, datelist[tick_range], + rotation=45, fontsize=12) + plt.xticks(np.arange(num_days), minor=True) + plt.legend(legendplot, fontsize=12, framealpha=0.5) + plt.grid(True, linestyle='--') + plt.tight_layout() + + # Save result. + if not os.path.isdir('Plots'): + os.makedirs('Plots') + plt.savefig('Plots/'+filename_plot+'.png', bbox_inches='tight', dpi=500) + + +def compare_all_compartments(files, legendplot, filename_plot="compare_compartments"): + """ Creates a 4x2 Plot with one subplot per compartment and one line per result one wants to compare. + + @param[in] files: paths of the files (without file extension .h5) with the simulation results that should be compared. + Results should contain exactly 8 compartments (so use accumulated numbers for LCT models). Names can be given in form of a list. + One could compare results with eg different parameters or different models. + @param[in] legendplot: list with names for the results that should be used for the legend of the plot. + @param[in] filename_plot: name to use as the file name for the saved plot. + """ + fig, axs = plt.subplots( + 4, 2, sharex='all', num=filename_plot, tight_layout=True) + + # Add simulation results to plot. + for file in range(len(files)): + # Load data. + h5file = h5py.File(str(files[file]) + '.h5', 'r') + + if (len(list(h5file.keys())) > 1): + raise gd.DataError("File should contain one dataset.") + if (len(list(h5file[list(h5file.keys())[0]].keys())) > 3): + raise gd.DataError("Expected only one group.") + + data = h5file[list(h5file.keys())[0]] + dates = data['Time'][:] + # As there should be only one Group, total is the simulation result. + total = data['Total'][:, :] + if (total.shape[1] != 8): + raise gd.DataError("Expected a different number of compartments.") + # Plot result. + if legendplot[file] in linestyle_dict: + for i in range(8): + axs[int(i/2), i % 2].plot(dates, + total[:, i], label=legendplot[file], linewidth=1.2, linestyle=linestyle_dict[legendplot[file]], color=color_dict[legendplot[file]]) + else: + for i in range(8): + axs[int(i/2), i % 2].plot(dates, + total[:, i], label=legendplot[file], linewidth=1.2) + h5file.close() + + # Define some characteristics of the plot. + for i in range(8): + axs[int(i/2), i % 2].set_title(secir_dict[i], fontsize=10) + axs[int(i/2), i % 2].set_xlim(left=0, right=dates[-1]) + axs[int(i/2), i % 2].grid(True, linestyle='--') + axs[int(i/2), i % 2].tick_params(axis='y', labelsize=8) + axs[int(i/2), i % 2].tick_params(axis='x', labelsize=8) + + fig.supxlabel('Zeit', fontsize=12) + + lines, labels = axs[0, 0].get_legend_handles_labels() + lgd = fig.legend(lines, labels, ncol=len(legendplot), loc='outside lower center', + fontsize=10, bbox_to_anchor=(0.5, - 0.06), bbox_transform=fig.transFigure) + + plt.tight_layout(pad=0, w_pad=0.5, h_pad=0.1) + plt.subplots_adjust(bottom=0.09) + + # Save result. + if not os.path.isdir('Plots'): + os.makedirs('Plots') + fig.savefig('Plots/'+filename_plot+'.png', + bbox_extra_artists=(lgd,), bbox_inches='tight', dpi=500) + + +def plot_new_infections(files, ylim, legendplot, filename_plot="compare_new_infections"): + """ Single plot to compare the incidence of different results. Incidence means the number of people leaving the susceptible class per day. + + @param[in] files: paths of the files (without file extension .h5) with the simulation results that should be compared. + Results should contain exactly 8 compartments (so use accumulated numbers for LCT models). Names can be given in form of a list. + One could compare results with eg different parameters or different models. + @param[in] ylim: upper limit for the y-axis of the plot. + @param[in] legendplot: list with names for the results that should be used for the legend of the plot. + @param[in] filename_plot: name to use as the file name for the saved plot. + """ + plt.figure(filename_plot) + + # Add simulation results to plot. + for file in range(len(files)): + # Load data. + h5file = h5py.File(str(files[file]) + '.h5', 'r') + + if (len(list(h5file.keys())) > 1): + raise gd.DataError("File should contain one dataset.") + if (len(list(h5file[list(h5file.keys())[0]].keys())) > 3): + raise gd.DataError("Expected only one group.") + + data = h5file[list(h5file.keys())[0]] + dates = data['Time'][:] + # As there should be only one Group, total is the simulation result. + total = data['Total'][:, :] + if (total.shape[1] != 8): + raise gd.DataError( + "Expected a different number of compartments.") + incidence = (total[:-1, 0]-total[1:, 0])/(dates[1:]-dates[:-1]) + if (file == 0): + incidence_ref = incidence + print("Relative deviation of the new infections at time " + + str(dates[-1])+" from the results of "+legendplot[0]) + deviation = (incidence[-1]-incidence_ref[-1])/incidence_ref[-1] + print(legendplot[file]+": "+str(deviation)) + # Plot result. + if legendplot[file] in linestyle_dict: + plt.plot(dates[1:], incidence, linewidth=1.2, + linestyle=linestyle_dict[legendplot[file]], color=color_dict[legendplot[file]]) + else: + plt.plot(dates[1:], incidence, linewidth=1.2) + + h5file.close() + + plt.xlabel('Zeit', fontsize=14) + plt.ylabel('Neuansteckungen pro Tag', fontsize=14) + plt.ylim(bottom=0, top=ylim) + plt.xlim(left=0, right=dates[-1]) + plt.legend(legendplot, fontsize=14, framealpha=0.5) + plt.grid(True, linestyle='--') + plt.tight_layout() + + # Save result. + if not os.path.isdir('Plots'): + os.makedirs('Plots') + plt.savefig('Plots/'+filename_plot+'.png', bbox_inches='tight', dpi=500) + + +if __name__ == '__main__': + # simulation results should be stored in folder "../data/simulation_lct". + data_dir = os.path.join(os.path.dirname( + __file__), "..", "data", "simulation_lct") + + # Defines which simulation case should be plotted. + case = 6 + + if case == 1: + # Compare different initalization method. + compare_all_compartments([os.path.join(data_dir, "init", "lct_init_transitions_20"), os.path.join(data_dir, "init", "lct_init_mean_20"), os.path.join(data_dir, "init", "lct_init_first_20")], + legendplot=list(["Übergänge", "Koslow", "Hurtado"]), filename_plot="compare_compartments_initialization20") + plot_new_infections([os.path.join(data_dir, "init", "lct_init_transitions_20_long"), os.path.join(data_dir, "init", "lct_init_mean_20_long"), os.path.join(data_dir, "init", "lct_init_first_20_long")], + 6000, legendplot=list(["Übergänge", "Koslow", "Hurtado"]), filename_plot="compare_new_infections_initialization_20_long") + compare_all_compartments([os.path.join(data_dir, "init", "lct_init_transitions_20_long"), os.path.join(data_dir, "init", "lct_init_mean_20_long"), os.path.join(data_dir, "init", "lct_init_first_20_long")], + legendplot=list(["Übergänge", "Koslow", "Hurtado"]), filename_plot="compare_compartments_initialization_20_long") + plot_new_infections([os.path.join(data_dir, "init", "lct_init_transitions_20"), os.path.join(data_dir, "init", "lct_init_mean_20"), os.path.join(data_dir, "init", "lct_init_first_3"), os.path.join(data_dir, "init", "lct_init_first_10"), os.path.join(data_dir, "init", "lct_init_first_20")], + 6000, legendplot=list(["Übergänge (20)", "Koslow (20)", "Hurtado 3", "Hurtado 10", "Hurtado 20"]), filename_plot="compare_new_infections_initialization_diffnums") + + elif case == 2: + # Compare simulation results of different models with a drop of R0. + plot_new_infections([os.path.join(data_dir, "dropR0", "fictional_lct_0.5_1"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_3"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_10"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_20"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_0")], + 4100, legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_new_infections_lct_drop") + compare_all_compartments([os.path.join(data_dir, "dropR0", "fictional_lct_0.5_1"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_3"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_10"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_20"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_0")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_compartments_lct_drop") + plot_new_infections([os.path.join(data_dir, "dropR0", "fictional_lct_0.5_1"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_3"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_10"), os.path.join(data_dir, "dropR0", "fictional_ide_0.5_3"), os.path.join(data_dir, "dropR0", "fictional_ide_0.5_10")], + 4100, legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_new_infections_ide_drop") + compare_all_compartments([os.path.join(data_dir, "dropR0", "fictional_lct_0.5_1"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_3"), os.path.join(data_dir, "dropR0", "fictional_lct_0.5_10"), os.path.join(data_dir, "dropR0", "fictional_ide_0.5_3"), os.path.join(data_dir, "dropR0", "fictional_ide_0.5_10")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_compartments_ide_drop") + + if case == 3: + # Compare simulation results of different models with a rise of R0. + plot_new_infections([os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_1"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_3"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_10"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_20"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_0")], + 15500, legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_new_infections_lct_rise2") + compare_all_compartments([os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_1"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_3"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_10"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_20"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_0")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_compartments_lct_rise2") + plot_new_infections([os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_1"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_3"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_10"), os.path.join(data_dir, "riseR0short", "fictional_ide_2.0_3"), os.path.join(data_dir, "riseR0short", "fictional_ide_2.0_10")], + 15500, legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_new_infections_ide_rise2") + compare_all_compartments([os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_1"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_3"), os.path.join(data_dir, "riseR0short", "fictional_lct_2.0_10"), os.path.join(data_dir, "riseR0short", "fictional_ide_2.0_3"), os.path.join(data_dir, "riseR0short", "fictional_ide_2.0_10")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_compartments_ide_rise2") + + if case == 4: + # Compare simulation results of different models with a rise of R0 and a long simulation time. + plot_new_infections([os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_20_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_0_long")], + 6e6, legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_new_infections_lct_rise4long") + compare_all_compartments([os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_20_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_0_long")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_compartments_lct_rise4long") + plot_new_infections([os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_4.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_4.0_10_long")], + 6e6, legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_new_infections_ide_rise4long") + compare_all_compartments([os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_4.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_4.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_4.0_10_long")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_compartments_ide_rise4long") + + plot_new_infections([os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_20_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_0_long")], + 2e6, legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_new_infections_lct_rise2long") + compare_all_compartments([os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_20_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_0_long")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "LCT 20", "LCT var"]), filename_plot="compare_compartments_lct_rise2long") + plot_new_infections([os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_2.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_2.0_10_long")], + 2e6, legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_new_infections_ide_rise2long") + compare_all_compartments([os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_1_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_lct_2.0_10_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_2.0_3_long"), os.path.join(data_dir, "riseR0long", "fictional_ide_2.0_10_long")], + legendplot=list(["ODE", "LCT 3", "LCT 10", "IDE 3", "IDE 10"]), filename_plot="compare_compartments_ide_rise2long") + + if case == 5: + # Attention: also change values in the dictionary parameters! + # Compare simulation results for a real situation starting on 01.06.2020. + compare_compartments_real([os.path.join(data_dir, "real", "real_lct1_2020_6_1"), os.path.join(data_dir, "real", "real_lct3_2020_6_1"), os.path.join(data_dir, "real", "real_lct10_2020_6_1")], + os.path.join(os.path.dirname( + __file__), "..", "data", "pydata", "Germany", "cases_all_germany_ma7.json"), + legendplot=list(["reale Daten", "ODE", "LCT 3", "LCT 10"]), deaths=True, filename_plot="compare_deaths_real_2020_6_1") + compare_compartments_real([os.path.join(data_dir, "real", "real_lct1_2020_6_1"), os.path.join(data_dir, "real", "real_lct3_2020_6_1"), os.path.join(data_dir, "real", "real_lct10_2020_6_1")], + os.path.join(os.path.dirname( + __file__), "..", "data", "pydata", "Germany", "cases_all_germany_ma7.json"), + legendplot=list(["reale Daten", "ODE", "LCT 3", "LCT 10"]), deaths=False, filename_plot="compare_infected_real_2020_6_1") + plot_new_infections_real([os.path.join(data_dir, "real", "real_lct1_2020_6_1"), os.path.join(data_dir, "real", "real_lct3_2020_6_1"), os.path.join(data_dir, "real", "real_lct10_2020_6_1")], + os.path.join(os.path.dirname( + __file__), "..", "data", "pydata", "Germany", "cases_all_germany_all_dates.json"), + legendplot=list(["reale Daten", "ODE", "LCT 3", "LCT 10"]), filename_plot="compare_new_infections_real_2020_6_1") + if case == 6: + # Attention: also change values in the dictionary parameters! + # Compare simulation results for a real situation starting on 01.10.2020. + compare_compartments_real([os.path.join(data_dir, "real", "real_lct1_2020_10_1"), os.path.join(data_dir, "real", "real_lct3_2020_10_1"), os.path.join(data_dir, "real", "real_lct10_2020_10_1")], + os.path.join(os.path.dirname( + __file__), "..", "data", "pydata", "Germany", "cases_all_germany_ma7.json"), + legendplot=list(["reale Daten", "ODE", "LCT 3", "LCT 10"]), deaths=True, filename_plot="compare_deaths_real_2020_10_1") + compare_compartments_real([os.path.join(data_dir, "real", "real_lct1_2020_10_1"), os.path.join(data_dir, "real", "real_lct3_2020_10_1"), os.path.join(data_dir, "real", "real_lct10_2020_10_1")], + os.path.join(os.path.dirname( + __file__), "..", "data", "pydata", "Germany", "cases_all_germany_ma7.json"), + legendplot=list(["reale Daten", "ODE", "LCT 3", "LCT 10"]), deaths=False, filename_plot="compare_infected_real_2020_10_1") + plot_new_infections_real([os.path.join(data_dir, "real", "real_lct1_2020_10_1"), os.path.join(data_dir, "real", "real_lct3_2020_10_1"), os.path.join(data_dir, "real", "real_lct10_2020_10_1")], + os.path.join(os.path.dirname( + __file__), "..", "data", "pydata", "Germany", "cases_all_germany_all_dates.json"), + legendplot=list(["reale Daten", "ODE", "LCT 3", "LCT 10"]), filename_plot="compare_new_infections_real_2020_10_1")