Skip to content

Commit 3390762

Browse files
410 Make IDE model compatible with graph (#1203)
Add graph simulation that doesn't require mobility so that IDE model can be used within graph Co-authored-by: reneSchm <[email protected]>
1 parent 12b041d commit 3390762

File tree

7 files changed

+229
-36
lines changed

7 files changed

+229
-36
lines changed

cpp/examples/CMakeLists.txt

+4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ add_executable(ide_secir_ageres_example ide_secir_ageres.cpp)
116116
target_link_libraries(ide_secir_ageres_example PRIVATE memilio ide_secir)
117117
target_compile_options(ide_secir_ageres_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS})
118118

119+
add_executable(ide_secir_graph_example ide_secir_graph.cpp)
120+
target_link_libraries(ide_secir_graph_example PRIVATE memilio ide_secir)
121+
target_compile_options(ide_secir_graph_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS})
122+
119123
add_executable(lct_secir_example lct_secir.cpp)
120124
target_link_libraries(lct_secir_example PRIVATE memilio lct_secir)
121125
target_compile_options(lct_secir_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS})

cpp/examples/ide_secir_graph.cpp

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (C) 2020-2025 MEmilio
3+
*
4+
* Authors: Anna Wendler
5+
*
6+
* Contact: Martin J. Kuehn <[email protected]>
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
#include "ide_secir/infection_state.h"
21+
#include "ide_secir/model.h"
22+
#include "ide_secir/simulation.h"
23+
#include "memilio/config.h"
24+
#include "memilio/mobility/graph.h"
25+
#include "memilio/mobility/metapopulation_mobility_instant.h"
26+
27+
int main()
28+
{
29+
// This is an example for running the IDE-SECIR model in a graph. It demonstrates a simple setup for a graph without
30+
// mobility that consists of two nodes and no edges.
31+
32+
using Vec = mio::TimeSeries<ScalarType>::Vector;
33+
34+
const ScalarType t0 = 0.;
35+
const ScalarType tmax = 10.;
36+
37+
// Set time step size of IDE solver. Note that the time step size will be constant throughout the simulation.
38+
const ScalarType dt_ide_solver = 1.;
39+
40+
size_t num_agegroups = 1;
41+
42+
mio::CustomIndexArray<ScalarType, mio::AgeGroup> N_init =
43+
mio::CustomIndexArray<ScalarType, mio::AgeGroup>(mio::AgeGroup(num_agegroups), 10000.);
44+
mio::CustomIndexArray<ScalarType, mio::AgeGroup> deaths_init =
45+
mio::CustomIndexArray<ScalarType, mio::AgeGroup>(mio::AgeGroup(num_agegroups), 13.10462213);
46+
47+
// Create TimeSeries with num_transitions * num_agegroups elements where initial transitions needed for simulation
48+
// will be stored. We require values for the transitions for a sufficient number of time points before the start of
49+
// the simulation to initialize our model.
50+
size_t num_transitions = (size_t)mio::isecir::InfectionTransition::Count;
51+
mio::TimeSeries<ScalarType> transitions_init(num_transitions * num_agegroups);
52+
53+
// Define vector of transitions that will be added to the time points of the TimeSeries transitions_init.
54+
Vec vec_init(num_transitions * num_agegroups);
55+
vec_init[(size_t)mio::isecir::InfectionTransition::SusceptibleToExposed] = 25.;
56+
vec_init[(size_t)mio::isecir::InfectionTransition::ExposedToInfectedNoSymptoms] = 15.;
57+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedNoSymptomsToInfectedSymptoms] = 8.;
58+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedNoSymptomsToRecovered] = 4.;
59+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedSymptomsToInfectedSevere] = 1.;
60+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedSymptomsToRecovered] = 4.;
61+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedSevereToInfectedCritical] = 1.;
62+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedSevereToRecovered] = 1.;
63+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedCriticalToDead] = 1.;
64+
vec_init[(size_t)mio::isecir::InfectionTransition::InfectedCriticalToRecovered] = 1.;
65+
// Multiply vec_init with dt_ide_solver so that within a time interval of length 1, always the above number of
66+
// individuals are transitioning from one compartment to another, irrespective of the chosen time step size.
67+
vec_init = vec_init * dt_ide_solver;
68+
69+
// In this example, we use the default transition distributions. For these distributions, setting the initial time
70+
// point of the TimeSeries transitions_init at time -10 will give us a sufficient number of time points before t0=0.
71+
// For more information on this, we refer to the documentation of TransitionDistributions in
72+
// models/ide_secir/parameters.h.
73+
transitions_init.add_time_point(-10, vec_init);
74+
// Add further time points with distance dt_ide_solver until time t0.
75+
while (transitions_init.get_last_time() < t0 - dt_ide_solver / 2.) {
76+
transitions_init.add_time_point(transitions_init.get_last_time() + dt_ide_solver, vec_init);
77+
}
78+
79+
// Initialize IDE model that will be used in in each node in the graph below.
80+
mio::isecir::Model model(std::move(transitions_init), N_init, deaths_init, num_agegroups);
81+
model.check_constraints(dt_ide_solver);
82+
83+
// Set up graph with two nodes and no edges. To each node, we pass an id, the above constructed IDE model as well
84+
// as the time step size that will be used by the numerical solver. For simplicity, we use the same model in
85+
// both nodes.
86+
mio::Graph<mio::SimulationNode<mio::isecir::Simulation>, mio::MobilityEdge<ScalarType>> g;
87+
g.add_node(1001, model, dt_ide_solver);
88+
g.add_node(1002, model, dt_ide_solver);
89+
90+
// We use make_no_mobilty_sim to create a GraphSimulation for the model graph we defined above. This function allows
91+
// us to create a simulation without having to define mobility between nodes. Any edges would be effectively
92+
// ignored by the simulation.
93+
auto sim = mio::make_no_mobility_sim(t0, std::move(g));
94+
// Run the simulation. This advances all graph nodes independently.
95+
sim.advance(tmax);
96+
97+
// Print results of first node.
98+
auto& node_0 = sim.get_graph().nodes()[0];
99+
auto result_0 = node_0.property.get_result();
100+
result_0.print_table({"S", "E", "C", "I", "H", "U", "R", "D "}, 16, 8);
101+
102+
return 0;
103+
}

cpp/memilio/mobility/metapopulation_mobility_instant.h

+49-15
Original file line numberDiff line numberDiff line change
@@ -403,13 +403,13 @@ class MobilityEdge
403403

404404
private:
405405
MobilityParameters<FP> m_parameters;
406-
TimeSeries<double> m_mobile_population;
407-
TimeSeries<double> m_return_times;
406+
TimeSeries<FP> m_mobile_population;
407+
TimeSeries<FP> m_return_times;
408408
bool m_return_mobile_population;
409-
double m_t_last_dynamic_npi_check = -std::numeric_limits<double>::infinity();
410-
std::pair<double, SimulationTime> m_dynamic_npi = {-std::numeric_limits<double>::max(), SimulationTime(0)};
409+
FP m_t_last_dynamic_npi_check = -std::numeric_limits<FP>::infinity();
410+
std::pair<FP, SimulationTime> m_dynamic_npi = {-std::numeric_limits<FP>::max(), SimulationTime(0)};
411411
std::vector<std::vector<size_t>> m_saved_compartment_indices; // groups of indices from compartments to save
412-
TimeSeries<double> m_mobility_results; // save results from edges + entry for the total number of commuters
412+
TimeSeries<FP> m_mobility_results; // save results from edges + entry for the total number of commuters
413413

414414
/**
415415
* @brief Computes a condensed version of `m_mobile_population` and stores it in `m_mobility_results`.
@@ -419,11 +419,11 @@ class MobilityEdge
419419
*
420420
* @param[in] t The current time.
421421
*/
422-
void add_mobility_result_time_point(const double t);
422+
void add_mobility_result_time_point(const FP t);
423423
};
424424

425425
template <typename FP>
426-
void MobilityEdge<FP>::add_mobility_result_time_point(const double t)
426+
void MobilityEdge<FP>::add_mobility_result_time_point(const FP t)
427427
{
428428
const size_t save_indices_size = this->m_saved_compartment_indices.size();
429429
if (save_indices_size > 0) {
@@ -435,7 +435,7 @@ void MobilityEdge<FP>::add_mobility_result_time_point(const double t)
435435
std::transform(this->m_saved_compartment_indices.begin(), this->m_saved_compartment_indices.end(),
436436
condensed_values.data(), [&last_value](const auto& indices) {
437437
return std::accumulate(indices.begin(), indices.end(), 0.0,
438-
[&last_value](double sum, auto i) {
438+
[&last_value](FP sum, auto i) {
439439
return sum + last_value[i];
440440
});
441441
});
@@ -563,7 +563,7 @@ template <class Sim>
563563
void MobilityEdge<FP>::apply_mobility(FP t, FP dt, SimulationNode<Sim>& node_from, SimulationNode<Sim>& node_to)
564564
{
565565
//check dynamic npis
566-
if (m_t_last_dynamic_npi_check == -std::numeric_limits<double>::infinity()) {
566+
if (m_t_last_dynamic_npi_check == -std::numeric_limits<FP>::infinity()) {
567567
m_t_last_dynamic_npi_check = node_from.get_t0();
568568
}
569569

@@ -574,8 +574,8 @@ void MobilityEdge<FP>::apply_mobility(FP t, FP dt, SimulationNode<Sim>& node_fro
574574
auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel);
575575
if (exceeded_threshold != dyn_npis.get_thresholds().end() &&
576576
(exceeded_threshold->first > m_dynamic_npi.first ||
577-
t > double(m_dynamic_npi.second))) { //old NPI was weaker or is expired
578-
auto t_end = SimulationTime(t + double(dyn_npis.get_duration()));
577+
t > FP(m_dynamic_npi.second))) { //old NPI was weaker or is expired
578+
auto t_end = SimulationTime(t + FP(dyn_npis.get_duration()));
579579
m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end);
580580
implement_dynamic_npis(
581581
m_parameters.get_coefficients(), exceeded_threshold->second, SimulationTime(t), t_end, [this](auto& g) {
@@ -673,8 +673,8 @@ void apply_mobility(FP t, FP dt, MobilityEdge<FP>& mobilityEdge, SimulationNode<
673673
*/
674674
template <typename FP, class Sim>
675675
GraphSimulation<Graph<SimulationNode<Sim>, MobilityEdge<FP>>, FP, FP,
676-
void (*)(double, double, mio::MobilityEdge<>&, mio::SimulationNode<Sim>&, mio::SimulationNode<Sim>&),
677-
void (*)(double, double, mio::SimulationNode<Sim>&)>
676+
void (*)(FP, FP, mio::MobilityEdge<FP>&, mio::SimulationNode<Sim>&, mio::SimulationNode<Sim>&),
677+
void (*)(FP, FP, mio::SimulationNode<Sim>&)>
678678
make_mobility_sim(FP t0, FP dt, const Graph<SimulationNode<Sim>, MobilityEdge<FP>>& graph)
679679
{
680680
return make_graph_sim(t0, dt, graph, static_cast<void (*)(FP, FP, SimulationNode<Sim>&)>(&advance_model<Sim>),
@@ -684,8 +684,8 @@ make_mobility_sim(FP t0, FP dt, const Graph<SimulationNode<Sim>, MobilityEdge<FP
684684

685685
template <typename FP, class Sim>
686686
GraphSimulation<Graph<SimulationNode<Sim>, MobilityEdge<FP>>, FP, FP,
687-
void (*)(double, double, mio::MobilityEdge<>&, mio::SimulationNode<Sim>&, mio::SimulationNode<Sim>&),
688-
void (*)(double, double, mio::SimulationNode<Sim>&)>
687+
void (*)(FP, FP, mio::MobilityEdge<FP>&, mio::SimulationNode<Sim>&, mio::SimulationNode<Sim>&),
688+
void (*)(FP, FP, mio::SimulationNode<Sim>&)>
689689
make_mobility_sim(FP t0, FP dt, Graph<SimulationNode<Sim>, MobilityEdge<FP>>&& graph)
690690
{
691691
return make_graph_sim(t0, dt, std::move(graph),
@@ -696,6 +696,40 @@ make_mobility_sim(FP t0, FP dt, Graph<SimulationNode<Sim>, MobilityEdge<FP>>&& g
696696

697697
/** @} */
698698

699+
/**
700+
* Create a graph simulation without mobility.
701+
*
702+
* Note that we set the time step of the graph simulation to infinity since we do not require any exchange between the
703+
* nodes. Hence, in each node, the simulation runs until tmax when advancing the simulation without interruption.
704+
*
705+
* @param t0 Start time of the simulation.
706+
* @param graph Set up for graph-based simulation.
707+
* @{
708+
*/
709+
template <typename FP, class Sim>
710+
auto make_no_mobility_sim(FP t0, Graph<SimulationNode<Sim>, MobilityEdge<FP>>& graph)
711+
{
712+
using GraphSim =
713+
GraphSimulation<Graph<SimulationNode<Sim>, MobilityEdge<FP>>, FP, FP,
714+
void (*)(FP, FP, mio::MobilityEdge<FP>&, mio::SimulationNode<Sim>&, mio::SimulationNode<Sim>&),
715+
void (*)(FP, FP, mio::SimulationNode<Sim>&)>;
716+
return GraphSim(t0, std::numeric_limits<FP>::infinity(), graph, &advance_model<Sim>,
717+
[](FP, FP, MobilityEdge<FP>&, SimulationNode<Sim>&, SimulationNode<Sim>&) {});
718+
}
719+
720+
template <typename FP, class Sim>
721+
auto make_no_mobility_sim(FP t0, Graph<SimulationNode<Sim>, MobilityEdge<FP>>&& graph)
722+
{
723+
using GraphSim =
724+
GraphSimulation<Graph<SimulationNode<Sim>, MobilityEdge<FP>>, FP, FP,
725+
void (*)(FP, FP, mio::MobilityEdge<FP>&, mio::SimulationNode<Sim>&, mio::SimulationNode<Sim>&),
726+
void (*)(FP, FP, mio::SimulationNode<Sim>&)>;
727+
return GraphSim(t0, std::numeric_limits<FP>::infinity(), std::move(graph), &advance_model<Sim>,
728+
[](FP, FP, MobilityEdge<FP>&, SimulationNode<Sim>&, SimulationNode<Sim>&) {});
729+
}
730+
731+
/** @} */
732+
699733
} // namespace mio
700734

701735
#endif //METAPOPULATION_MOBILITY_INSTANT_H

cpp/models/ide_secir/parameters.h

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ namespace isecir
4747
/**
4848
* @brief Transition distribution for each transition in #InfectionTransition.
4949
*
50+
* For each transition, the corresponding transition distribution can be chosen independently.
51+
* The choice of distributions determines how many initial time points are required to initialize the model, see
52+
* get_global_support_max() in models/ide_secir/model.h.
53+
*
5054
* As a default we use SmootherCosine functions for all transitions with m_parameter=2.
5155
*/
5256
struct TransitionDistributions {

cpp/models/ide_secir/simulation.cpp

+1-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ void Simulation::advance(ScalarType tmax)
3232
{
3333
mio::log_info("Simulating IDE-SECIR from t0 = {} until tmax = {} with dt = {}.",
3434
m_model->transitions.get_last_time(), tmax, m_dt);
35-
m_model->set_transitiondistributions_support_max(m_dt);
36-
m_model->set_transitiondistributions_derivative(m_dt);
37-
m_model->set_transitiondistributions_in_forceofinfection(m_dt);
35+
3836
m_model->initial_compute_compartments(m_dt);
3937

4038
// For every time step:

cpp/models/ide_secir/simulation.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ class Simulation
4141
/**
4242
* @brief setup the Simulation for an IDE model.
4343
* @param[in] model An instance of the IDE model.
44-
* @param[in] dt Step size of numerical solver.
44+
* @param[in] dt Step size of numerical solver. Throughout the simulation, the step size will be constant.
4545
*/
4646
Simulation(Model const& model, ScalarType dt = 0.1)
4747
: m_model(std::make_unique<Model>(model))
4848
, m_dt(dt)
4949
{
50+
assert(m_dt > 0);
51+
m_model->set_transitiondistributions_support_max(m_dt);
52+
m_model->set_transitiondistributions_derivative(m_dt);
53+
m_model->set_transitiondistributions_in_forceofinfection(m_dt);
5054
}
5155

5256
/**

0 commit comments

Comments
 (0)