From 1679386c505a8db77fed99226e3e09b4ca843f5c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:54:45 +0200 Subject: [PATCH 001/169] CHG: Docstring improveent --- cpp/memilio/mobility/graph.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index eb8bd35990..56cc820022 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -211,7 +211,9 @@ class Graph } /** - * @brief range of edges going out from a specific node + * @brief Range of edges going pout from a specific node + * + * @param node_idx ID of node */ auto out_edges(size_t node_idx) { @@ -220,6 +222,8 @@ class Graph /** * @brief range of edges going out from a specific node + * + * @param node_idx ID of node */ auto out_edges(size_t node_idx) const { From 83e789a1efd9a508680b1f210e158654b3589fc4 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:56:07 +0200 Subject: [PATCH 002/169] CHG: Docs trings update --- .../metapopulation_mobility_instant.h | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_instant.h b/cpp/memilio/mobility/metapopulation_mobility_instant.h index 19aa85cf65..b6d877e3cf 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_instant.h +++ b/cpp/memilio/mobility/metapopulation_mobility_instant.h @@ -434,10 +434,9 @@ void MobilityEdge::add_mobility_result_time_point(const FP t) // sum up the values of m_saved_compartment_indices for each group (e.g. Age groups) std::transform(this->m_saved_compartment_indices.begin(), this->m_saved_compartment_indices.end(), condensed_values.data(), [&last_value](const auto& indices) { - return std::accumulate(indices.begin(), indices.end(), 0.0, - [&last_value](FP sum, auto i) { - return sum + last_value[i]; - }); + return std::accumulate(indices.begin(), indices.end(), 0.0, [&last_value](FP sum, auto i) { + return sum + last_value[i]; + }); }); // the last value is the sum of commuters @@ -641,7 +640,7 @@ void MobilityEdge::apply_mobility(FP t, FP dt, SimulationNode& node_fro } /** - * edge functor for mobility-based simulation. + * node functor for mobility-based simulation. * @see SimulationNode::advance */ template @@ -662,7 +661,8 @@ void apply_mobility(FP t, FP dt, MobilityEdge& mobilityEdge, SimulationNode< } /** - * create a mobility-based simulation. + * @brief create a mobility-based simulation. + * * After every second time step, for each edge a portion of the population corresponding to the coefficients of the edge * changes from one node to the other. In the next timestep, the mobile population returns to their "home" node. * Returns are adjusted based on the development in the target node. @@ -697,7 +697,7 @@ make_mobility_sim(FP t0, FP dt, Graph, MobilityEdge>&& g /** @} */ /** - * Create a graph simulation without mobility. + * @brief Create a graph simulation without mobility. * * Note that we set the time step of the graph simulation to infinity since we do not require any exchange between the * nodes. Hence, in each node, the simulation runs until tmax when advancing the simulation without interruption. @@ -706,16 +706,16 @@ make_mobility_sim(FP t0, FP dt, Graph, MobilityEdge>&& g * @param graph Set up for graph-based simulation. * @{ */ - template - auto make_no_mobility_sim(FP t0, Graph, MobilityEdge>& graph) - { - using GraphSim = - GraphSimulation, MobilityEdge>, FP, FP, - void (*)(FP, FP, mio::MobilityEdge&, mio::SimulationNode&, mio::SimulationNode&), - void (*)(FP, FP, mio::SimulationNode&)>; - return GraphSim(t0, std::numeric_limits::infinity(), graph, &advance_model, - [](FP, FP, MobilityEdge&, SimulationNode&, SimulationNode&) {}); - } +template +auto make_no_mobility_sim(FP t0, Graph, MobilityEdge>& graph) +{ + using GraphSim = + GraphSimulation, MobilityEdge>, FP, FP, + void (*)(FP, FP, mio::MobilityEdge&, mio::SimulationNode&, mio::SimulationNode&), + void (*)(FP, FP, mio::SimulationNode&)>; + return GraphSim(t0, std::numeric_limits::infinity(), graph, &advance_model, + [](FP, FP, MobilityEdge&, SimulationNode&, SimulationNode&) {}); +} template auto make_no_mobility_sim(FP t0, Graph, MobilityEdge>&& graph) From 027358e079df2fb6b833f83133d354e2f0f58b4d Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:56:40 +0200 Subject: [PATCH 003/169] CHG: asymetric mobility introduction --- .../metapopulation_mobility_asymmetric.cpp | 24 +++ .../metapopulation_mobility_asymmetric.h | 203 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 cpp/memilio/mobility/metapopulation_mobility_asymmetric.cpp create mode 100644 cpp/memilio/mobility/metapopulation_mobility_asymmetric.h diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.cpp b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.cpp new file mode 100644 index 0000000000..eb6bd917fc --- /dev/null +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.cpp @@ -0,0 +1,24 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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/mobility/metapopulation_mobility_asymmetric.h" + +namespace mio +{ +} // namespace mio diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h new file mode 100644 index 0000000000..6cadd83264 --- /dev/null +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -0,0 +1,203 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 METAPOPULATION_MOBILITY_STOCHASTIC_H +#define METAPOPULATION_MOBILITY_STOCHASTIC_H + +#include "memilio/compartments/simulation.h" +#include "memilio/utils/time_series.h" +#include "memilio/epidemiology/contact_matrix.h" +#include "memilio/epidemiology/age_group.h" +#include "memilio/mobility/graph_simulation.h" +#include "memilio/mobility/metapopulation_mobility_instant.h" + +#include "boost/filesystem.hpp" + +#include +#include +#include +#include +#include + +namespace mio +{ + +class MobilityParametersTimed +{ + +public: + /** + * @brief Construct a new Mobility Parameters Timed object + * + */ + MobilityParametersTimed() + : _exchanges{} {}; + MobilityParametersTimed(std::filebuf& input_data) + : _exchanges{} + { + insert_input_data(input_data); + }; + + /** + * @brief Return the number of exchanged items in the next exchange event. + * + * @return auto + */ + auto next_event_number() + { + return _exchanges.top().number; + }; + /** + * @brief Return the timepoint of the next exchange event. + * + * @return auto + */ + auto next_event_time() + { + return _exchanges.top(); + }; + + /** + * @brief Return the ExchangeData for the next exchange event and delete it from the list. + * + * @return auto + */ + auto process_next_event() + { + auto next_event = _exchanges.top(); + _exchanges.pop(); + return next_event; + }; + +private: + void insert_input_data(std::filebuf& input_data) { + //... + }; + + /** + * @brief Stores Timepoint and number of exchanged items for an exchange process. + * + * @param time Timepoint of the exchange process + * @param number Number of exchanged items + */ + struct ExchangeData { + double time; + int number; + }; + + struct CompareExchangeData { + bool operator()(const ExchangeData& left, const ExchangeData& right) + { + return left.time > right.time; + }; + }; + +private: + std::priority_queue, CompareExchangeData> _exchanges; +}; + +// /** +// * represents the mobility between two nodes. +// */ +// class MobilityEdgeDirected +// { +// public: +// /** +// * create edge with coefficients. +// * @param coeffs mobility rate for each group and compartment +// */ +// MobilityEdgeDirected(const MobilityParametersTimed& params) +// : m_parameters(params) +// { +// } + +// /** +// * get the mobility parameters. +// */ +// const MobilityParametersTimed& get_parameters() const +// { +// return m_parameters; +// } + +// /** +// * compute mobility from node_from to node_to for a given event +// * @param[in] event index specifying which compartment and age group change nodes +// * @param node_from node that people changed from +// * @param node_to node that people changed to +// */ +// template +// void apply_mobility(size_t event, SimulationNode& node_from, SimulationNode& node_to); + +// private: +// MobilityParametersTimed m_parameters; +// }; + +// template +// void MobilityEdgeDirected::apply_mobility(size_t event, SimulationNode& node_from, SimulationNode& node_to) +// { +// node_from.get_result().get_last_value()[event] -= 1; +// node_to.get_result().get_last_value()[event] += 1; +// } + +// /** +// * edge functor for mobility-based simulation. +// * @see MobilityEdgeDirected::apply_mobility +// */ +// template +// void apply_mobility(StochasticEdge& mobilityEdge, size_t event, SimulationNode& node_from, +// SimulationNode& node_to) +// { +// mobilityEdge.apply_mobility(event, node_from, node_to); +// } + +// /** +// * create a mobility-based simulation. +// * After every second time step, for each edge a portion of the population corresponding to the coefficients of the edge +// * changes from one node to the other. In the next timestep, the mobile population returns to their "home" node. +// * Returns are adjusted based on the development in the target node. +// * @param t0 start time of the simulation +// * @param dt time step between mobility +// * @param graph set up for mobility-based simulation +// * @{ +// */ +// template +// GraphSimulationStochastic, MobilityEdgeDirected>> +// make_mobility_sim(double t0, double dt, const Graph, MobilityEdgeDirected>& graph) +// { +// return make_graph_sim_stochastic( +// t0, dt, graph, &advance_model, +// static_cast&, SimulationNode&)>( +// &apply_mobility)); +// } + +// template +// GraphSimulationStochastic, MobilityEdgeDirected>> +// make_mobility_sim(double t0, double dt, Graph, MobilityEdgeDirected>&& graph) +// { +// return make_graph_sim_stochastic( +// t0, dt, std::move(graph), &advance_model, +// static_cast&, SimulationNode&)>( +// &apply_mobility)); +// } + +// /** @} */ + +} // namespace mio + +#endif //METAPOPULATION_MOBILITY_STOCHASTIC_H From bf0aeabf1b81f285f73cd3fdd8c97591f7cbb656 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:27:40 +0200 Subject: [PATCH 004/169] FIX: typo --- cpp/memilio/mobility/metapopulation_mobility_instant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_instant.h b/cpp/memilio/mobility/metapopulation_mobility_instant.h index b6d877e3cf..9c5bd99b0a 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_instant.h +++ b/cpp/memilio/mobility/metapopulation_mobility_instant.h @@ -189,7 +189,7 @@ class MobilityParameters //@} /** - * Get/Setthe mobility coefficients. + * Get/Set the mobility coefficients. * The coefficients represent the (time-dependent) percentage of people moving * from one node to another by age and infection compartment. * @{ From e03d661ab61654f2f1cb48348fe8392b4f192029 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:27:56 +0200 Subject: [PATCH 005/169] CHG: Add functions for parameter lookups --- .../metapopulation_mobility_asymmetric.h | 206 ++++++++++-------- 1 file changed, 118 insertions(+), 88 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 6cadd83264..179b636707 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -64,15 +64,41 @@ class MobilityParametersTimed return _exchanges.top().number; }; /** - * @brief Return the timepoint of the next exchange event. + * @brief Return the timepoint of the next exchangeclass EdgePropertyT event. * * @return auto */ auto next_event_time() { - return _exchanges.top(); + return _exchanges.top().time; }; - + /** + * @brief Return the destination node id of the next exchange + * + * @return auto + */ + auto next_event_node_to() + { + return _exchanges.top().node_to; + } + /** + * @brief Return a const reference to the next event + * + * @return auto + */ + auto next_event() + { + return _exchanges.top(); + } + /** + * @brief Delete the next event from the heap + * + * @return auto + */ + auto pop_next_event() + { + return _exchanges.pop() + } /** * @brief Return the ExchangeData for the next exchange event and delete it from the list. * @@ -99,6 +125,7 @@ class MobilityParametersTimed struct ExchangeData { double time; int number; + int node_to; }; struct CompareExchangeData { @@ -112,91 +139,94 @@ class MobilityParametersTimed std::priority_queue, CompareExchangeData> _exchanges; }; -// /** -// * represents the mobility between two nodes. -// */ -// class MobilityEdgeDirected -// { -// public: -// /** -// * create edge with coefficients. -// * @param coeffs mobility rate for each group and compartment -// */ -// MobilityEdgeDirected(const MobilityParametersTimed& params) -// : m_parameters(params) -// { -// } - -// /** -// * get the mobility parameters. -// */ -// const MobilityParametersTimed& get_parameters() const -// { -// return m_parameters; -// } - -// /** -// * compute mobility from node_from to node_to for a given event -// * @param[in] event index specifying which compartment and age group change nodes -// * @param node_from node that people changed from -// * @param node_to node that people changed to -// */ -// template -// void apply_mobility(size_t event, SimulationNode& node_from, SimulationNode& node_to); - -// private: -// MobilityParametersTimed m_parameters; -// }; - -// template -// void MobilityEdgeDirected::apply_mobility(size_t event, SimulationNode& node_from, SimulationNode& node_to) -// { -// node_from.get_result().get_last_value()[event] -= 1; -// node_to.get_result().get_last_value()[event] += 1; -// } - -// /** -// * edge functor for mobility-based simulation. -// * @see MobilityEdgeDirected::apply_mobility -// */ -// template -// void apply_mobility(StochasticEdge& mobilityEdge, size_t event, SimulationNode& node_from, -// SimulationNode& node_to) -// { -// mobilityEdge.apply_mobility(event, node_from, node_to); -// } - -// /** -// * create a mobility-based simulation. -// * After every second time step, for each edge a portion of the population corresponding to the coefficients of the edge -// * changes from one node to the other. In the next timestep, the mobile population returns to their "home" node. -// * Returns are adjusted based on the development in the target node. -// * @param t0 start time of the simulation -// * @param dt time step between mobility -// * @param graph set up for mobility-based simulation -// * @{ -// */ -// template -// GraphSimulationStochastic, MobilityEdgeDirected>> -// make_mobility_sim(double t0, double dt, const Graph, MobilityEdgeDirected>& graph) -// { -// return make_graph_sim_stochastic( -// t0, dt, graph, &advance_model, -// static_cast&, SimulationNode&)>( -// &apply_mobility)); -// } - -// template -// GraphSimulationStochastic, MobilityEdgeDirected>> -// make_mobility_sim(double t0, double dt, Graph, MobilityEdgeDirected>&& graph) -// { -// return make_graph_sim_stochastic( -// t0, dt, std::move(graph), &advance_model, -// static_cast&, SimulationNode&)>( -// &apply_mobility)); -// } - -// /** @} */ +/** + * represents the mobility between two nodes. + */ +class MobilityEdgeDirected +{ +public: + /** + * create edge with timed movement parameters. + * @param params mobility rate for each group and compartment + */ + MobilityEdgeDirected(const MobilityParametersTimed& params) + : m_parameters(params) + { + } + + /** + * get the mobility parameters. + */ + const MobilityParametersTimed& get_parameters() const + { + return m_parameters; + } + + /** + * compute mobility from node_from to node_to for a given event + * @param[in] event index specifying which compartment and age group change nodes + * @param node_from node that people changed from + * @param node_to node that people changed to + */ + template + void apply_mobility(const Graph, MobilityEdgeDirected>& graph); + +private: + MobilityParametersTimed m_parameters; +}; + +template +void MobilityEdgeDirected::apply_mobility(const Graph, MobilityEdgeDirected>& graph) +{ + auto node_to_id = m_parameters.next_event_node_to(); + auto number = m_parameters.next_event_number(); + auto time = m_parameters.next_event_time(); + m_parameters.pop_next_event(); + auto node_to = graph.nodes()[node_to_id]; +} + +/**get_last_value + * edge functor for mobility-based simulation. + * @see MobilityEdgeDirected::apply_mobility + */ +template +void apply_mobility(StochasticEdge& mobilityEdge, size_t event, SimulationNode& node_from, + SimulationNode& node_to) +{ + mobilityEdge.apply_mobility(event, node_from, node_to); +} + +/** + * create a mobility-based simulation. + * After every second time step, for each edge a portion of the population corresponding to the coefficients of the edge + * changes from one node to the other. In the next timestep, the mobile population returns to their "home" node. + * Returns are adjusted based on the development in the target node. + * @param t0 start time of the simulation + * @param dt time step between mobility + * @param graph set up for mobility-based simulation + * @{ + */ +template +GraphSimulationStochastic, MobilityEdgeDirected>> +make_mobility_sim(double t0, double dt, const Graph, MobilityEdgeDirected>& graph) +{ + return make_graph_sim_stochastic( + t0, dt, graph, &advance_model, + static_cast&, SimulationNode&)>( + &apply_mobility)); +} + +template +GraphSimulationStochastic, MobilityEdgeDirected>> +make_mobility_sim(double t0, double dt, Graph, MobilityEdgeDirected>&& graph) +{ + return make_graph_sim_stochastic( + t0, dt, std::move(graph), &advance_model, + static_cast&, SimulationNode&)>( + &apply_mobility)); +} + +/** @} */ } // namespace mio From 80ef21a2db8b8862d944b290fec3d18533a0c67c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:29:32 +0200 Subject: [PATCH 006/169] NEW: asymmetric graph example --- cpp/examples/CMakeLists.txt | 4 ++ cpp/examples/asymmetric_graph.cpp | 112 ++++++++++++++++++++++++++++++ cpp/memilio/CMakeLists.txt | 2 + 3 files changed, 118 insertions(+) create mode 100644 cpp/examples/asymmetric_graph.cpp diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index fea58a84cf..c3a82ea186 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -100,6 +100,10 @@ add_executable(graph_stochastic_mobility_example graph_stochastic_mobility.cpp) target_link_libraries(graph_stochastic_mobility_example PRIVATE memilio ode_secir) target_compile_options(graph_stochastic_mobility_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +add_executable(asymmetric_graph_example asymmetric_graph.cpp) +target_link_libraries(asymmetric_graph_example PRIVATE memilio ode_secir) +target_compile_options(asymmetric_graph_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) + add_executable(abm_minimal_example abm_minimal.cpp) target_link_libraries(abm_minimal_example PRIVATE memilio abm) target_compile_options(abm_minimal_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp new file mode 100644 index 0000000000..e507444e46 --- /dev/null +++ b/cpp/examples/asymmetric_graph.cpp @@ -0,0 +1,112 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Daniel Abele +* +* 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/mobility/metapopulation_mobility_asymmetric.h" +#include "ode_secir/model.h" +#include "ode_secir/infection_state.h" +#include "ode_secir/parameters.h" +#include "memilio/compartments/simulation.h" +#include "memilio/mobility/graph.h" +#include "memilio/mobility/metapopulation_mobility_stochastic.h" +#include "memilio/mobility/metapopulation_mobility_instant.h" + +#include + +int main(int /*argc*/, char** /*argv*/) +{ + const auto t0 = 0.; + const auto tmax = 10.; + const auto dt = 0.1; //initial time step + + //total compartment sizes + double num_total = 10000, num_exp = 200, num_ins = 50, num_is = 50, num_isev = 10, num_icri = 5, num_rec = 0, + num_dead = 0; + + //model with 3 age group + mio::osecir::Model model(3); + + auto& params = model.parameters; + + auto num_age_groups = params.get_num_groups(); + double fact = 1.0 / (double)(size_t)num_age_groups; + + //set initialization and model parameters + for (auto i = mio::AgeGroup(0); i < num_age_groups; i++) { + params.get>()[i] = 3.2; + params.get>()[i] = 2.; + params.get>()[i] = 6.; + params.get>()[i] = 12.; + params.get>()[i] = 8.; + + //initial populations is equally distributed among age groups + model.populations[{i, mio::osecir::InfectionState::Exposed}] = fact * num_exp; + model.populations[{i, mio::osecir::InfectionState::InfectedNoSymptoms}] = fact * num_ins; + model.populations[{i, mio::osecir::InfectionState::InfectedSymptoms}] = fact * num_is; + model.populations[{i, mio::osecir::InfectionState::InfectedSevere}] = fact * num_isev; + model.populations[{i, mio::osecir::InfectionState::InfectedCritical}] = fact * num_icri; + model.populations[{i, mio::osecir::InfectionState::Recovered}] = fact * num_rec; + model.populations[{i, mio::osecir::InfectionState::Dead}] = fact * num_dead; + model.populations.set_difference_from_group_total({i, mio::osecir::InfectionState::Susceptible}, + fact * num_total); + + params.get>()[i] = 0.05; + params.get>()[i] = 0.67; + params.get>()[i] = 0.09; + params.get>()[i] = 0.25; + params.get>()[i] = 0.2; + params.get>()[i] = 0.25; + params.get>()[i] = 0.3; + } + + //add contact pattern and contact damping + mio::ContactMatrixGroup& contact_matrix = params.get>(); + contact_matrix[0] = + mio::ContactMatrix(Eigen::MatrixXd::Constant((size_t)num_age_groups, (size_t)num_age_groups, fact * 10)); + contact_matrix.add_damping(Eigen::MatrixXd::Constant((size_t)num_age_groups, (size_t)num_age_groups, 0.6), + mio::SimulationTime(5.)); + + model.apply_constraints(); + + //modify model for second node + auto model2 = model; + + for (auto i = mio::AgeGroup(0); i < num_age_groups; i++) { + + model2.populations[{i, mio::osecir::InfectionState::Exposed}] = fact * 100; + model2.populations[{i, mio::osecir::InfectionState::InfectedNoSymptoms}] = fact * 100; + model2.populations[{i, mio::osecir::InfectionState::InfectedCritical}] = fact * 10; + model2.populations.set_difference_from_group_total({i, mio::osecir::InfectionState::Susceptible}, + fact * num_total); + } + + mio::Graph>>, mio::MobilityEdgeDirected> + graph; + graph.add_node(1001, model, t0); + graph.add_node(1002, model2, t0); + + auto param = mio::MobilityParametersTimed(2, 10, 1); + + graph.add_edge(0, 1, param); + + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); + + sim.advance(tmax); + + return 0; +} diff --git a/cpp/memilio/CMakeLists.txt b/cpp/memilio/CMakeLists.txt index 8898d32efb..68949d3238 100644 --- a/cpp/memilio/CMakeLists.txt +++ b/cpp/memilio/CMakeLists.txt @@ -68,6 +68,8 @@ add_library(memilio mobility/metapopulation_mobility_instant.cpp mobility/metapopulation_mobility_stochastic.h mobility/metapopulation_mobility_stochastic.cpp + mobility/metapopulation_mobility_asymmetric.h + mobility/metapopulation_mobility_asymmetric.cpp mobility/graph_simulation.h mobility/graph_simulation.cpp mobility/graph.h From bc05c3c8a4a26c23a8806d12c3669fc0bd414e59 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:30:10 +0200 Subject: [PATCH 007/169] CHG: Add example MobilityEdge constructor --- cpp/memilio/mobility/metapopulation_mobility_asymmetric.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 179b636707..d82aed9134 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -53,6 +53,11 @@ class MobilityParametersTimed { insert_input_data(input_data); }; + MobilityParametersTimed(int time, int number, int to) + : _exchanges{} + { + _exchanges.push(ExchangeData(time, number, to)); + }; /** * @brief Return the number of exchanged items in the next exchange event. From 0581191508c859a9b3fb52c8f5b23023f63e50f9 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:30:39 +0200 Subject: [PATCH 008/169] CHG: Update directed edge implementation --- cpp/memilio/mobility/metapopulation_mobility_asymmetric.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index d82aed9134..77364d74f7 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -102,7 +102,7 @@ class MobilityParametersTimed */ auto pop_next_event() { - return _exchanges.pop() + return _exchanges.pop(); } /** * @brief Return the ExchangeData for the next exchange event and delete it from the list. @@ -215,7 +215,7 @@ template GraphSimulationStochastic, MobilityEdgeDirected>> make_mobility_sim(double t0, double dt, const Graph, MobilityEdgeDirected>& graph) { - return make_graph_sim_stochastic( + return make_graph_sim( t0, dt, graph, &advance_model, static_cast&, SimulationNode&)>( &apply_mobility)); @@ -225,7 +225,7 @@ template GraphSimulationStochastic, MobilityEdgeDirected>> make_mobility_sim(double t0, double dt, Graph, MobilityEdgeDirected>&& graph) { - return make_graph_sim_stochastic( + return make_graph_sim( t0, dt, std::move(graph), &advance_model, static_cast&, SimulationNode&)>( &apply_mobility)); From 97fc970e3c7904c83791519bfbd97db22e38947a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:46:31 +0200 Subject: [PATCH 009/169] CHG: Change parameters of apply_mobility --- .../metapopulation_mobility_asymmetric.h | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 77364d74f7..0665420cdd 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -190,16 +190,16 @@ void MobilityEdgeDirected::apply_mobility(const Graph, Mobil auto node_to = graph.nodes()[node_to_id]; } -/**get_last_value - * edge functor for mobility-based simulation. - * @see MobilityEdgeDirected::apply_mobility - */ -template -void apply_mobility(StochasticEdge& mobilityEdge, size_t event, SimulationNode& node_from, - SimulationNode& node_to) -{ - mobilityEdge.apply_mobility(event, node_from, node_to); -} +// /**get_last_value +// * edge functor for mobility-based simulation. +// * @see MobilityEdgeDirected::apply_mobility +// */ +// template +// void apply_mobility(StochasticEdge& mobilityEdge, size_t event, SimulationNode& node_from, +// SimulationNode& node_to) +// { +// mobilityEdge.apply_mobility(event, node_from, node_to); +// } /** * create a mobility-based simulation. @@ -212,23 +212,20 @@ void apply_mobility(StochasticEdge& mobilityEdge, size_t event, SimulationNode -GraphSimulationStochastic, MobilityEdgeDirected>> +AsymmetricGraphSimulation, MobilityEdgeDirected>> make_mobility_sim(double t0, double dt, const Graph, MobilityEdgeDirected>& graph) { - return make_graph_sim( - t0, dt, graph, &advance_model, - static_cast&, SimulationNode&)>( - &apply_mobility)); + return make_graph_sim(t0, dt, graph, &advance_model, + + &apply_mobility, MobilityEdgeDirected>>); } template -GraphSimulationStochastic, MobilityEdgeDirected>> +AsymmetricGraphSimulation, MobilityEdgeDirected>> make_mobility_sim(double t0, double dt, Graph, MobilityEdgeDirected>&& graph) { - return make_graph_sim( - t0, dt, std::move(graph), &advance_model, - static_cast&, SimulationNode&)>( - &apply_mobility)); + return make_graph_sim(t0, dt, std::move(graph), &advance_model, + &apply_mobility, MobilityEdgeDirected>>); } /** @} */ From 5cc5213fff63ab2da3236558a861672961f2e173 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:06:23 +0200 Subject: [PATCH 010/169] CHG: add asymmetric graph --- cpp/memilio/mobility/graph_simulation.h | 43 +++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index b911b43fbd..aa472f83a8 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -124,9 +124,9 @@ class GraphSimulationStochastic std::function> { using Base = GraphSimulationBase, - std::function>; + std::function>; using node_function = typename Base::node_function; using edge_function = typename Base::edge_function; @@ -252,6 +252,38 @@ class GraphSimulationStochastic RandomNumberGenerator m_rng; }; +template +class AsymmetricGraphSimulation : public GraphSimulationBase +{ + using Base = GraphSimulationBase; + using Base::GraphSimulationBase; + +public: + void advance(Timepoint t_max = 1.0) + { + auto dt = Base::m_dt; + while (Base::m_t < t_max) { + if (Base::m_t + dt > t_max) { + dt = t_max - Base::m_t; + } + + for (auto& n : Base::m_graph.nodes()) { + Base::m_node_func(Base::m_t, dt, n.property); + } + + Base::m_t += dt; + + for (auto& e : Base::m_graph.edges()) { + Base::m_edge_func(Base::m_t, dt, e.property, Base::m_graph.nodes()[e.start_node_idx].property, + Base::m_graph.nodes()[e.end_node_idx].property); + } + } + } +}; + template auto make_graph_sim(Timepoint t0, Timespan dt, Graph&& g, NodeF&& node_func, EdgeF&& edge_func) { @@ -266,5 +298,12 @@ auto make_graph_sim_stochastic(FP t0, FP dt, Graph&& g, NodeF&& node_func, EdgeF t0, dt, std::forward(g), std::forward(node_func), std::forward(edge_func)); } +template +auto make_asymmetric_graph_sim(Timepoint t0, Timespan dt, Graph&& g, NodeF&& node_func, EdgeF&& edge_func) +{ + return AsymmetricGraphSimulation, Timepoint, Timespan, EdgeF, NodeF>( + t0, dt, std::forward(g), std::forward(node_func), std::forward(edge_func)); +} + } // namespace mio #endif //MIO_MOBILITY_GRAPH_SIMULATION_H From 8e4948893e0fcf5006380e383f256d1d46f93c54 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:06:49 +0200 Subject: [PATCH 011/169] CHG: update TimedEdge --- .../metapopulation_mobility_asymmetric.h | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 0665420cdd..f74358ad35 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -73,7 +73,7 @@ class MobilityParametersTimed * * @return auto */ - auto next_event_time() + auto next_event_time() const { return _exchanges.top().time; }; @@ -167,6 +167,11 @@ class MobilityEdgeDirected return m_parameters; } + auto next_event_time() const + { + return m_parameters.next_event_time(); + } + /** * compute mobility from node_from to node_to for a given event * @param[in] event index specifying which compartment and age group change nodes @@ -174,22 +179,30 @@ class MobilityEdgeDirected * @param node_to node that people changed to */ template - void apply_mobility(const Graph, MobilityEdgeDirected>& graph); + void apply_mobility(SimulationNode& node_from, SimulationNode& node_to); private: MobilityParametersTimed m_parameters; }; template -void MobilityEdgeDirected::apply_mobility(const Graph, MobilityEdgeDirected>& graph) +void MobilityEdgeDirected::apply_mobility(SimulationNode& node_from, SimulationNode& node_to) { - auto node_to_id = m_parameters.next_event_node_to(); - auto number = m_parameters.next_event_number(); - auto time = m_parameters.next_event_time(); - m_parameters.pop_next_event(); - auto node_to = graph.nodes()[node_to_id]; + auto next_event = m_parameters.process_next_event(); + auto num_moving = next_event.number; + node_from.get_result().get_last_value()[0] -= num_moving; + node_to.get_result().get_last_value()[0] += num_moving; } +template +void apply_timed_mobility(double t, double dt, MobilityEdgeDirected& edge, SimulationNode& node_from, + SimulationNode& node_to) +{ + if (edge.next_event_time() >= t + dt) { + return; + } + edge.apply_mobility(node_from, node_to); +} // /**get_last_value // * edge functor for mobility-based simulation. // * @see MobilityEdgeDirected::apply_mobility @@ -211,21 +224,24 @@ void MobilityEdgeDirected::apply_mobility(const Graph, Mobil * @param graph set up for mobility-based simulation * @{ */ -template +template AsymmetricGraphSimulation, MobilityEdgeDirected>> -make_mobility_sim(double t0, double dt, const Graph, MobilityEdgeDirected>& graph) +make_mobility_sim(FP t0, FP dt, const Graph, MobilityEdgeDirected>& graph) { - return make_graph_sim(t0, dt, graph, &advance_model, - - &apply_mobility, MobilityEdgeDirected>>); + return make_asymmetric_graph_sim( + t0, dt, graph, &advance_model, + static_cast&, SimulationNode&)>(apply_mobility)); } +// static_cast&, SimulationNode&)>(&apply_mobility) -template +template AsymmetricGraphSimulation, MobilityEdgeDirected>> -make_mobility_sim(double t0, double dt, Graph, MobilityEdgeDirected>&& graph) +make_mobility_sim(FP t0, FP dt, Graph, MobilityEdgeDirected>&& graph) { - return make_graph_sim(t0, dt, std::move(graph), &advance_model, - &apply_mobility, MobilityEdgeDirected>>); + return make_asymmetric_graph_sim( + t0, dt, std::move(graph), &advance_model, + static_cast&, SimulationNode&)>( + apply_timed_mobility)); } /** @} */ From ee131532f559eda73d4878c49998a7ed88c006ed Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:06:59 +0200 Subject: [PATCH 012/169] CHG: Add smm to example --- cpp/examples/asymmetric_graph.cpp | 91 +++++++++++-------------------- 1 file changed, 32 insertions(+), 59 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index e507444e46..1bdf15b22a 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -17,17 +17,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" -#include "ode_secir/model.h" -#include "ode_secir/infection_state.h" -#include "ode_secir/parameters.h" -#include "memilio/compartments/simulation.h" #include "memilio/mobility/graph.h" -#include "memilio/mobility/metapopulation_mobility_stochastic.h" -#include "memilio/mobility/metapopulation_mobility_instant.h" +#include "smm/simulation.h" +#include "smm/parameters.h" #include +enum class InfectionState +{ + S, + E, + I, + R, + Count +}; + int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; @@ -35,67 +41,34 @@ int main(int /*argc*/, char** /*argv*/) const auto dt = 0.1; //initial time step //total compartment sizes - double num_total = 10000, num_exp = 200, num_ins = 50, num_is = 50, num_isev = 10, num_icri = 5, num_rec = 0, - num_dead = 0; - - //model with 3 age group - mio::osecir::Model model(3); - - auto& params = model.parameters; + double num_total = 10000, num_exp = 200, num_ins = 50, num_rec = 0; - auto num_age_groups = params.get_num_groups(); - double fact = 1.0 / (double)(size_t)num_age_groups; + using Model = mio::smm::Model<1, InfectionState>; + Model model; - //set initialization and model parameters - for (auto i = mio::AgeGroup(0); i < num_age_groups; i++) { - params.get>()[i] = 3.2; - params.get>()[i] = 2.; - params.get>()[i] = 6.; - params.get>()[i] = 12.; - params.get>()[i] = 8.; + auto home = mio::regions::Region(1); - //initial populations is equally distributed among age groups - model.populations[{i, mio::osecir::InfectionState::Exposed}] = fact * num_exp; - model.populations[{i, mio::osecir::InfectionState::InfectedNoSymptoms}] = fact * num_ins; - model.populations[{i, mio::osecir::InfectionState::InfectedSymptoms}] = fact * num_is; - model.populations[{i, mio::osecir::InfectionState::InfectedSevere}] = fact * num_isev; - model.populations[{i, mio::osecir::InfectionState::InfectedCritical}] = fact * num_icri; - model.populations[{i, mio::osecir::InfectionState::Recovered}] = fact * num_rec; - model.populations[{i, mio::osecir::InfectionState::Dead}] = fact * num_dead; - model.populations.set_difference_from_group_total({i, mio::osecir::InfectionState::Susceptible}, - fact * num_total); + model.populations[{home, InfectionState::E}] = num_exp; + model.populations[{home, InfectionState::I}] = num_ins; + model.populations[{home, InfectionState::R}] = num_rec; + model.populations[{home, InfectionState::S}] = num_total - num_exp - num_ins - num_rec; - params.get>()[i] = 0.05; - params.get>()[i] = 0.67; - params.get>()[i] = 0.09; - params.get>()[i] = 0.25; - params.get>()[i] = 0.2; - params.get>()[i] = 0.25; - params.get>()[i] = 0.3; - } - - //add contact pattern and contact damping - mio::ContactMatrixGroup& contact_matrix = params.get>(); - contact_matrix[0] = - mio::ContactMatrix(Eigen::MatrixXd::Constant((size_t)num_age_groups, (size_t)num_age_groups, fact * 10)); - contact_matrix.add_damping(Eigen::MatrixXd::Constant((size_t)num_age_groups, (size_t)num_age_groups, 0.6), - mio::SimulationTime(5.)); - - model.apply_constraints(); + std::vector> adoption_rates; + adoption_rates.push_back({InfectionState::E, InfectionState::I, home, 0.2, {}}); + adoption_rates.push_back({InfectionState::I, InfectionState::R, home, 0.333, {}}); + adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.5}}}); + model.parameters.get>() = adoption_rates; + // std::vector> transition_rates; + // for (size_t s = 0; s < static_cast(InfectionState::Count); ++s) { + // transition_rates.push_back({InfectionState(s), home, home, 0}); + // } + // model.parameters.get>() = transition_rates; //modify model for second node auto model2 = model; - for (auto i = mio::AgeGroup(0); i < num_age_groups; i++) { - - model2.populations[{i, mio::osecir::InfectionState::Exposed}] = fact * 100; - model2.populations[{i, mio::osecir::InfectionState::InfectedNoSymptoms}] = fact * 100; - model2.populations[{i, mio::osecir::InfectionState::InfectedCritical}] = fact * 10; - model2.populations.set_difference_from_group_total({i, mio::osecir::InfectionState::Susceptible}, - fact * num_total); - } - - mio::Graph>>, mio::MobilityEdgeDirected> + mio::Graph>>, + mio::MobilityEdgeDirected> graph; graph.add_node(1001, model, t0); graph.add_node(1002, model2, t0); From 5a498b4664d2bf7e3279aaa7d3abb1c1b49551ab Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:28:37 +0200 Subject: [PATCH 013/169] FIX: dimensions error --- cpp/examples/asymmetric_graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 1bdf15b22a..925dd2d675 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -46,7 +46,7 @@ int main(int /*argc*/, char** /*argv*/) using Model = mio::smm::Model<1, InfectionState>; Model model; - auto home = mio::regions::Region(1); + auto home = mio::regions::Region(0); model.populations[{home, InfectionState::E}] = num_exp; model.populations[{home, InfectionState::I}] = num_ins; From c246556b847109fe7f8d887ee6a7dfa5ed83fdcf Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:01:05 +0200 Subject: [PATCH 014/169] FIX: link smm in CMake --- cpp/examples/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index c3a82ea186..cad69d32bf 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -101,7 +101,7 @@ target_link_libraries(graph_stochastic_mobility_example PRIVATE memilio ode_seci target_compile_options(graph_stochastic_mobility_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) add_executable(asymmetric_graph_example asymmetric_graph.cpp) -target_link_libraries(asymmetric_graph_example PRIVATE memilio ode_secir) +target_link_libraries(asymmetric_graph_example PRIVATE memilio smm) target_compile_options(asymmetric_graph_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) add_executable(abm_minimal_example abm_minimal.cpp) From 1ded562d2180178e79be73969cfe4631224601f1 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:02:08 +0200 Subject: [PATCH 015/169] CHG: update asymmetric graph --- .../metapopulation_mobility_asymmetric.h | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index f74358ad35..06e7fa1c40 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -21,14 +21,15 @@ #define METAPOPULATION_MOBILITY_STOCHASTIC_H #include "memilio/compartments/simulation.h" +#include "memilio/utils/random_number_generator.h" #include "memilio/utils/time_series.h" -#include "memilio/epidemiology/contact_matrix.h" -#include "memilio/epidemiology/age_group.h" + #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_instant.h" #include "boost/filesystem.hpp" +#include #include #include #include @@ -53,12 +54,17 @@ class MobilityParametersTimed { insert_input_data(input_data); }; - MobilityParametersTimed(int time, int number, int to) + MobilityParametersTimed(double time, int number, int to) : _exchanges{} { _exchanges.push(ExchangeData(time, number, to)); }; + void add_exchange(double time, int number, int to) + { + _exchanges.push(ExchangeData{time, number, to}); + } + /** * @brief Return the number of exchanged items in the next exchange event. * @@ -75,6 +81,9 @@ class MobilityParametersTimed */ auto next_event_time() const { + if (_exchanges.empty()) { + return std::numeric_limits::max(); + } return _exchanges.top().time; }; /** @@ -159,6 +168,11 @@ class MobilityEdgeDirected { } + void add_exchange(double time, int number, int to) + { + m_parameters.add_exchange(time, number, to); + } + /** * get the mobility parameters. */ @@ -190,8 +204,15 @@ void MobilityEdgeDirected::apply_mobility(SimulationNode& node_from, Simula { auto next_event = m_parameters.process_next_event(); auto num_moving = next_event.number; - node_from.get_result().get_last_value()[0] -= num_moving; - node_to.get_result().get_last_value()[0] += num_moving; + // auto num_available = boost::numeric::ublas::sum(node_from.get_result().get_last_value()); + auto rng = mio::RandomNumberGenerator(); + auto distribution = DiscreteDistributionInPlace(); + + for (int i = 0; i < num_moving; ++i) { + auto group = distribution(rng, {node_from.get_result().get_last_value()}); + node_from.get_result().get_last_value()[group] -= 1; + node_to.get_result().get_last_value()[group] += 1; + } } template From fcf84e5796a1bc8204f3e412c0adf37194f33340 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:03:51 +0200 Subject: [PATCH 016/169] FIX: Use Smm simulation --- cpp/examples/asymmetric_graph.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 925dd2d675..6b3ec27fca 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -38,7 +38,7 @@ int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; const auto tmax = 10.; - const auto dt = 0.1; //initial time step + const auto dt = 1.; //initial time step //total compartment sizes double num_total = 10000, num_exp = 200, num_ins = 50, num_rec = 0; @@ -67,11 +67,8 @@ int main(int /*argc*/, char** /*argv*/) //modify model for second node auto model2 = model; - mio::Graph>>, - mio::MobilityEdgeDirected> - graph; - graph.add_node(1001, model, t0); - graph.add_node(1002, model2, t0); + mio::Graph>, mio::MobilityEdgeDirected> graph; + graph.add_node(0, model, t0); auto param = mio::MobilityParametersTimed(2, 10, 1); From 302beacc725a4377ea53d3bdba23ff2bdb8d72a8 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:05:22 +0200 Subject: [PATCH 017/169] CHG: functional minimal asymmetric graph example --- cpp/examples/asymmetric_graph.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 6b3ec27fca..1568ca5a22 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -69,14 +69,21 @@ int main(int /*argc*/, char** /*argv*/) mio::Graph>, mio::MobilityEdgeDirected> graph; graph.add_node(0, model, t0); + graph.add_node(1, model2, t0); - auto param = mio::MobilityParametersTimed(2, 10, 1); - + auto param = mio::MobilityParametersTimed(2.0, 10, 1); graph.add_edge(0, 1, param); + graph.edges()[0].property.add_exchange(5.0, 5, 1); + graph.edges()[0].property.add_exchange(6.0, 500, 1); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); sim.advance(tmax); + std::cout << "First table" << std::endl; + sim.get_graph().nodes()[0].property.get_result().print_table({"S", "E", "I", "R"}); + std::cout << "Second Table" << std::endl; + sim.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R"}); + return 0; } From 301beef4017a965cd7ac536c21890f0049832ead Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:42:07 +0200 Subject: [PATCH 018/169] CHG: full example code --- cpp/examples/asymmetric_graph.cpp | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 1568ca5a22..dd9bce84cc 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -20,6 +20,7 @@ #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" +#include "memilio/utils/logging.h" #include "smm/simulation.h" #include "smm/parameters.h" @@ -37,7 +38,7 @@ enum class InfectionState int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; - const auto tmax = 10.; + const auto tmax = 100.; const auto dt = 1.; //initial time step //total compartment sizes @@ -70,20 +71,40 @@ int main(int /*argc*/, char** /*argv*/) mio::Graph>, mio::MobilityEdgeDirected> graph; graph.add_node(0, model, t0); graph.add_node(1, model2, t0); + size_t num_nodes = 200000; + for (size_t i = 2; i < num_nodes; i++) { + auto local_model = model; + graph.add_node(i, local_model, t0); + } auto param = mio::MobilityParametersTimed(2.0, 10, 1); graph.add_edge(0, 1, param); graph.edges()[0].property.add_exchange(5.0, 5, 1); graph.edges()[0].property.add_exchange(6.0, 500, 1); + auto rng = mio::RandomNumberGenerator(); + auto distribution = mio::DiscreteDistributionInPlace(); + std::vector uniform_vector(num_nodes, 1.0); + mio::log_info("Nodes generated"); + for (size_t i = 0; i < 1 * num_nodes; ++i) { + auto to = distribution(rng, {uniform_vector}); + auto from = distribution(rng, {uniform_vector}); + auto local_param = mio::MobilityParametersTimed(distribution(rng, {std::vector(100, 1)}) + 1, 10, to); + graph.add_edge(from, to, local_param); + graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); + graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); + } + + mio::log_info("Graph generated"); + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); sim.advance(tmax); - std::cout << "First table" << std::endl; - sim.get_graph().nodes()[0].property.get_result().print_table({"S", "E", "I", "R"}); - std::cout << "Second Table" << std::endl; - sim.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R"}); + // std::cout << "First table" << std::endl; + // sim.get_graph().nodes()[0].property.get_result().print_table({"S", "E", "I", "R"}); + // std::cout << "Second Table" << std::endl; + // sim.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R"}); return 0; } From d176a05e0178516c7311538e8f8077b43b21f9bd Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:43:28 +0200 Subject: [PATCH 019/169] CHG: Move heap from edges into graph --- cpp/examples/asymmetric_graph.cpp | 27 ++- cpp/memilio/mobility/graph_simulation.h | 145 ++++++++++++++- .../metapopulation_mobility_asymmetric.h | 167 +++--------------- 3 files changed, 181 insertions(+), 158 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index dd9bce84cc..b969b464b1 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -78,27 +78,36 @@ int main(int /*argc*/, char** /*argv*/) } auto param = mio::MobilityParametersTimed(2.0, 10, 1); - graph.add_edge(0, 1, param); - graph.edges()[0].property.add_exchange(5.0, 5, 1); - graph.edges()[0].property.add_exchange(6.0, 500, 1); + graph.add_edge(0, 1); + // graph.edges()[0].property.add_exchange(5.0, 5, 1); + // graph.edges()[0].property.add_exchange(6.0, 500, 1); auto rng = mio::RandomNumberGenerator(); auto distribution = mio::DiscreteDistributionInPlace(); std::vector uniform_vector(num_nodes, 1.0); mio::log_info("Nodes generated"); for (size_t i = 0; i < 1 * num_nodes; ++i) { - auto to = distribution(rng, {uniform_vector}); - auto from = distribution(rng, {uniform_vector}); - auto local_param = mio::MobilityParametersTimed(distribution(rng, {std::vector(100, 1)}) + 1, 10, to); - graph.add_edge(from, to, local_param); - graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); - graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); + auto to = distribution(rng, {uniform_vector}); + auto from = distribution(rng, {uniform_vector}); + graph.add_edge(from, to); + + // graph.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i) + // graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); + // graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); } mio::log_info("Graph generated"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); + for (size_t i = 0; i < 1 * num_nodes; i++) { + sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + } + + mio::log_info("Number of exchanges: {}", sim.get_parameters().size()); + + mio::log_info("Exchanges added"); + sim.advance(tmax); // std::cout << "First table" << std::endl; diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index aa472f83a8..ddd9eaf7fe 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -22,10 +22,130 @@ #include "memilio/mobility/graph.h" #include "memilio/utils/random_number_generator.h" +#include namespace mio { +class MobilityParametersTimed +{ + +public: + /** + * @brief Construct a new Mobility Parameters Timed object + * + */ + MobilityParametersTimed() + : _exchanges{} {}; + MobilityParametersTimed(std::filebuf& input_data) + : _exchanges{} + { + insert_input_data(input_data); + }; + MobilityParametersTimed(double time, double number, int edge) + : _exchanges{} + { + _exchanges.push(ExchangeData(time, number, edge)); + }; + + void add_exchange(double time, double number, int edge) + { + _exchanges.push(ExchangeData{time, number, edge}); + } + + /** + * @brief Return the number of exchanged items in the next exchange event. + * + * @return auto + */ + auto next_event_number() + { + return _exchanges.top().number; + }; + /** + * @brief Return the timepoint of the next exchangeclass EdgePropertyT event. + * + * @return auto + */ + auto next_event_time() const + { + if (_exchanges.empty()) { + return std::numeric_limits::max(); + } + return _exchanges.top().time; + }; + /** + * @brief Return the destination node id of the next exchange + * + * @return auto + */ + auto next_event_edge_id() + { + return _exchanges.top().edge_id; + } + /** + * @brief Return a const reference to the next event + * + * @return auto + */ + auto next_event() + { + return _exchanges.top(); + } + /** + * @brief Delete the next event from the heap + * + * @return auto + */ + auto pop_next_event() + { + return _exchanges.pop(); + } + /** + * @brief Return the ExchangeData for the next exchange event and delete it from the list. + * + * @return auto + */ + auto process_next_event() + { + auto next_event = _exchanges.top(); + _exchanges.pop(); + return next_event; + }; + + auto size() const + { + return _exchanges.size(); + } + +private: + void insert_input_data(std::filebuf& input_data) { + //... + }; + + /** + * @brief Stores Timepoint and number of exchanged items for an exchange process. + * + * @param time Timepoint of the exchange process + * @param number Number of exchanged items + */ + struct ExchangeData { + double time; + double number; + int edge_id; + }; + + struct CompareExchangeData { + bool operator()(const ExchangeData& left, const ExchangeData& right) + { + return left.time > right.time; + }; + }; + +private: + std::priority_queue, CompareExchangeData> _exchanges; +}; + /** * @brief abstract simulation on a graph with alternating node and edge actions */ @@ -264,7 +384,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase t_max) { dt = t_max - Base::m_t; @@ -276,12 +396,31 @@ class AsymmetricGraphSimulation : public GraphSimulationBase diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 06e7fa1c40..07ab9d89a4 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -32,127 +32,12 @@ #include #include #include -#include #include #include namespace mio { -class MobilityParametersTimed -{ - -public: - /** - * @brief Construct a new Mobility Parameters Timed object - * - */ - MobilityParametersTimed() - : _exchanges{} {}; - MobilityParametersTimed(std::filebuf& input_data) - : _exchanges{} - { - insert_input_data(input_data); - }; - MobilityParametersTimed(double time, int number, int to) - : _exchanges{} - { - _exchanges.push(ExchangeData(time, number, to)); - }; - - void add_exchange(double time, int number, int to) - { - _exchanges.push(ExchangeData{time, number, to}); - } - - /** - * @brief Return the number of exchanged items in the next exchange event. - * - * @return auto - */ - auto next_event_number() - { - return _exchanges.top().number; - }; - /** - * @brief Return the timepoint of the next exchangeclass EdgePropertyT event. - * - * @return auto - */ - auto next_event_time() const - { - if (_exchanges.empty()) { - return std::numeric_limits::max(); - } - return _exchanges.top().time; - }; - /** - * @brief Return the destination node id of the next exchange - * - * @return auto - */ - auto next_event_node_to() - { - return _exchanges.top().node_to; - } - /** - * @brief Return a const reference to the next event - * - * @return auto - */ - auto next_event() - { - return _exchanges.top(); - } - /** - * @brief Delete the next event from the heap - * - * @return auto - */ - auto pop_next_event() - { - return _exchanges.pop(); - } - /** - * @brief Return the ExchangeData for the next exchange event and delete it from the list. - * - * @return auto - */ - auto process_next_event() - { - auto next_event = _exchanges.top(); - _exchanges.pop(); - return next_event; - }; - -private: - void insert_input_data(std::filebuf& input_data) { - //... - }; - - /** - * @brief Stores Timepoint and number of exchanged items for an exchange process. - * - * @param time Timepoint of the exchange process - * @param number Number of exchanged items - */ - struct ExchangeData { - double time; - int number; - int node_to; - }; - - struct CompareExchangeData { - bool operator()(const ExchangeData& left, const ExchangeData& right) - { - return left.time > right.time; - }; - }; - -private: - std::priority_queue, CompareExchangeData> _exchanges; -}; - /** * represents the mobility between two nodes. */ @@ -163,28 +48,17 @@ class MobilityEdgeDirected * create edge with timed movement parameters. * @param params mobility rate for each group and compartment */ - MobilityEdgeDirected(const MobilityParametersTimed& params) - : m_parameters(params) - { - } + // MobilityEdgeDirected(const MobilityParametersTimed& params) + // : m_parameters(params) + // { + // } - void add_exchange(double time, int number, int to) - { - m_parameters.add_exchange(time, number, to); - } + // auto next_event_time() const + // { + // return m_parameters.next_event_time(); + // } - /** - * get the mobility parameters. - */ - const MobilityParametersTimed& get_parameters() const - { - return m_parameters; - } - - auto next_event_time() const - { - return m_parameters.next_event_time(); - } + MobilityEdgeDirected() = default; /** * compute mobility from node_from to node_to for a given event @@ -193,17 +67,18 @@ class MobilityEdgeDirected * @param node_to node that people changed to */ template - void apply_mobility(SimulationNode& node_from, SimulationNode& node_to); + void apply_mobility(double num_moving, SimulationNode& node_from, SimulationNode& node_to); -private: - MobilityParametersTimed m_parameters; + // private: + // MobilityParametersTimed m_parameters; }; template -void MobilityEdgeDirected::apply_mobility(SimulationNode& node_from, SimulationNode& node_to) +void MobilityEdgeDirected::apply_mobility(double num_moving, SimulationNode& node_from, + SimulationNode& node_to) { - auto next_event = m_parameters.process_next_event(); - auto num_moving = next_event.number; + // auto next_event = m_parameters.process_next_event(); + // auto num_moving = next_event.number; // auto num_available = boost::numeric::ublas::sum(node_from.get_result().get_last_value()); auto rng = mio::RandomNumberGenerator(); auto distribution = DiscreteDistributionInPlace(); @@ -216,13 +91,13 @@ void MobilityEdgeDirected::apply_mobility(SimulationNode& node_from, Simula } template -void apply_timed_mobility(double t, double dt, MobilityEdgeDirected& edge, SimulationNode& node_from, +void apply_timed_mobility(double t, double num_moving, MobilityEdgeDirected& edge, SimulationNode& node_from, SimulationNode& node_to) { - if (edge.next_event_time() >= t + dt) { - return; - } - edge.apply_mobility(node_from, node_to); + // if (edge.next_event_time() >= t + dt) { + // return; + // } + edge.apply_mobility(num_moving, node_from, node_to); } // /**get_last_value // * edge functor for mobility-based simulation. From 5798064c3698251e99956513691feaef8e76ec66 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:36:10 +0200 Subject: [PATCH 020/169] CHG: Move geographical location to location file in memilio/geography --- cpp/memilio/geography/locations.h | 81 ++++++++++++++++++++++++++++ cpp/models/abm/common_abm_loggers.h | 2 +- cpp/models/abm/location.h | 32 ++--------- cpp/simulations/abm_braunschweig.cpp | 4 +- cpp/tests/test_abm_location.cpp | 2 +- 5 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 cpp/memilio/geography/locations.h diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h new file mode 100644 index 0000000000..4b871961ef --- /dev/null +++ b/cpp/memilio/geography/locations.h @@ -0,0 +1,81 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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/io/default_serialize.h" +#include +#include +#include +#include "memilio/utils/logging.h" +namespace mio +{ +namespace geo +{ + +const double earth_radius = 6371; + +class GeographicalLocation +{ +public: + double latitude; + double longitude; + + /** + * @brief Compare two GeographicalLocation%s. + */ + bool operator==(const GeographicalLocation& other) const + { + return (latitude == other.latitude && longitude == other.longitude); + } + + bool operator!=(const GeographicalLocation& other) const + { + return !(latitude == other.latitude && longitude == other.longitude); + } + + /// This method is used by the default serialization feature. + auto default_serialize() + { + return Members("GeographicalLocation").add("latitude", latitude).add("longitude", longitude); + } + + /* + * @brief Calculate the distance between two geographical locations + * @param other The other geographical location. + * @return The distance between the two locations in kilometers. + * + * Uses the haversine formula (https://en.wikipedia.org/wiki/Haversine_formula) to calculate the distance between + * two geographical locations. Uses an earth radius of 6375 km (https://en.wikipedia.org/wiki/Earth_radius). + * The math functions use radians, whereas coordinates are given in degrees. + */ + double distance(const GeographicalLocation& other) const + { + double delta_latitude = (latitude - other.latitude) / 180 * M_PI; + double delta_longitude = (longitude - other.longitude) / 180 * M_PI; + double first_part = sin(delta_latitude / 2) * sin(delta_latitude / 2); + double second_part = cos(latitude / 180 * M_PI) * cos(other.latitude / 180 * M_PI) * sin(delta_longitude / 2) * + sin(delta_longitude / 2); + double distance = 2.0 * earth_radius * asin(sqrt(first_part + second_part)); + return distance; + } +}; + +} // namespace geo + +} // namespace mio \ No newline at end of file diff --git a/cpp/models/abm/common_abm_loggers.h b/cpp/models/abm/common_abm_loggers.h index a453d9554b..242dfe659c 100644 --- a/cpp/models/abm/common_abm_loggers.h +++ b/cpp/models/abm/common_abm_loggers.h @@ -80,7 +80,7 @@ constexpr mio::abm::ActivityType guess_activity_type(mio::abm::LocationType curr */ struct LogLocationInformation : mio::LogOnce { using Type = std::vector< - std::tuple>; + std::tuple>; /** * @brief Log the LocationInformation of the simulation. * @param[in] sim The simulation of the abm. diff --git a/cpp/models/abm/location.h b/cpp/models/abm/location.h index cd18d1b361..61349ae830 100644 --- a/cpp/models/abm/location.h +++ b/cpp/models/abm/location.h @@ -25,6 +25,7 @@ #include "abm/parameters.h" #include "abm/location_type.h" +#include "memilio/geography/locations.h" #include "memilio/io/default_serialize.h" #include "boost/atomic/atomic.hpp" @@ -33,30 +34,6 @@ namespace mio namespace abm { -struct GeographicalLocation { - double latitude; - double longitude; - - /** - * @brief Compare two GeographicalLocation%s. - */ - bool operator==(const GeographicalLocation& other) const - { - return (latitude == other.latitude && longitude == other.longitude); - } - - bool operator!=(const GeographicalLocation& other) const - { - return !(latitude == other.latitude && longitude == other.longitude); - } - - /// This method is used by the default serialization feature. - auto default_serialize() - { - return Members("GraphicalLocation").add("latitude", latitude).add("longitude", longitude); - } -}; - struct CellIndex : public mio::Index { CellIndex(size_t i) : mio::Index(i) @@ -248,7 +225,7 @@ class Location * @brief Get the geographical location of the Location. * @return The geographical location of the Location. */ - GeographicalLocation get_geographical_location() const + mio::geo::GeographicalLocation get_geographical_location() const { return m_geographical_location; } @@ -257,7 +234,7 @@ class Location * @brief Set the geographical location of the Location. * @param[in] location The geographical location of the Location. */ - void set_geographical_location(GeographicalLocation location) + void set_geographical_location(mio::geo::GeographicalLocation location) { m_geographical_location = location; } @@ -292,7 +269,8 @@ class Location LocalInfectionParameters m_parameters; ///< Infection parameters for the Location. std::vector m_cells{}; ///< A vector of all Cell%s that the Location is divided in. MaskType m_required_mask; ///< Least secure type of Mask that is needed to enter the Location. - GeographicalLocation m_geographical_location; ///< Geographical location (longitude and latitude) of the Location. + mio::geo::GeographicalLocation + m_geographical_location; ///< Geographical location (longitude and latitude) of the Location. int m_model_id; ///< Model id the location is in. Only used for ABM graph model or hybrid graph model. }; diff --git a/cpp/simulations/abm_braunschweig.cpp b/cpp/simulations/abm_braunschweig.cpp index 5f1e1d1394..a6d9be1668 100644 --- a/cpp/simulations/abm_braunschweig.cpp +++ b/cpp/simulations/abm_braunschweig.cpp @@ -310,14 +310,14 @@ void create_model_from_data(mio::abm::Model& model, const std::string& filename, uint32_t home_id = row[index["huid"]]; uint32_t target_location_id = std::abs(row[index["loc_id_end"]]); uint32_t activity_end = row[index["activity_end"]]; - mio::abm::GeographicalLocation location_long_lat = {(double)row[index["lon_end"]] / 1e+5, + mio::geo::GeographicalLocation location_long_lat = {(double)row[index["lon_end"]] / 1e+5, (double)row[index["lat_end"]] / 1e+5}; mio::abm::LocationId home; auto it_home = locations.find(home_id); if (it_home == locations.end()) { home = model.add_location(mio::abm::LocationType::Home, 1); locations.insert({home_id, home}); - mio::abm::GeographicalLocation location_long_lat_home = {(double)row[index["lon_start"]] / 1e+5, + mio::geo::GeographicalLocation location_long_lat_home = {(double)row[index["lon_start"]] / 1e+5, (double)row[index["lat_start"]] / 1e+5}; model.get_location(home).set_geographical_location(location_long_lat_home); } diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index 0d57d18fa6..55d4c0fad6 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -167,7 +167,7 @@ TEST_F(TestLocation, getGeographicalLocation) // Create a location of type Home. auto location = mio::abm::Location(mio::abm::LocationType::Home, 0); // Set a geographical location for the location. - mio::abm::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; location.set_geographical_location(geographical_location); // Verify that the set geographical location matches the expected values. EXPECT_EQ(location.get_geographical_location(), geographical_location); From 122abea6b77c5063e6b3fc123957423a01b300da Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:36:35 +0200 Subject: [PATCH 021/169] CHG: Make node params protected --- cpp/memilio/mobility/metapopulation_mobility_instant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_instant.h b/cpp/memilio/mobility/metapopulation_mobility_instant.h index 9c5bd99b0a..1480e3b610 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_instant.h +++ b/cpp/memilio/mobility/metapopulation_mobility_instant.h @@ -98,7 +98,7 @@ class SimulationNode m_last_state = m_simulation.get_result().get_last_value(); } -private: +protected: Sim m_simulation; Eigen::VectorXd m_last_state; double m_t0; From 89991f133f7d91ada2ae257e3ec88cb64917986d Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:37:25 +0200 Subject: [PATCH 022/169] CHG: add mio::unused --- cpp/memilio/mobility/graph_simulation.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index b25a5d055e..33c69ea967 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -21,11 +21,12 @@ #define MIO_MOBILITY_GRAPH_SIMULATION_H #include "memilio/mobility/graph.h" +#include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/random_number_generator.h" #include #include "memilio/compartments/feedback_simulation.h" #include "memilio/geography/regions.h" - +#include namespace mio { @@ -121,8 +122,10 @@ class MobilityParametersTimed } private: - void insert_input_data(std::filebuf& input_data) { + void insert_input_data(std::filebuf& input_data) + { //... + mio::unused(input_data); }; /** @@ -224,6 +227,10 @@ class GraphSimulation : public GraphSimulationBase Date: Tue, 5 Aug 2025 17:37:44 +0200 Subject: [PATCH 023/169] CHG: remove omp --- cpp/memilio/mobility/graph_simulation.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 33c69ea967..26b06f4535 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -26,7 +26,7 @@ #include #include "memilio/compartments/feedback_simulation.h" #include "memilio/geography/regions.h" -#include + namespace mio { @@ -227,10 +227,6 @@ class GraphSimulation : public GraphSimulationBase Date: Tue, 5 Aug 2025 18:00:45 +0200 Subject: [PATCH 024/169] CHG: --- cpp/examples/asymmetric_graph.cpp | 38 +++++++----- .../metapopulation_mobility_asymmetric.h | 58 ++++++++++++------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index b969b464b1..9282c2041a 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -24,8 +24,6 @@ #include "smm/simulation.h" #include "smm/parameters.h" -#include - enum class InfectionState { S, @@ -37,9 +35,9 @@ enum class InfectionState int main(int /*argc*/, char** /*argv*/) { - const auto t0 = 0.; - const auto tmax = 100.; - const auto dt = 1.; //initial time step + const auto t0 = 0.; + // const auto tmax = 100.; + const auto dt = 1.; //initial time step //total compartment sizes double num_total = 10000, num_exp = 200, num_ins = 50, num_rec = 0; @@ -68,10 +66,10 @@ int main(int /*argc*/, char** /*argv*/) //modify model for second node auto model2 = model; - mio::Graph>, mio::MobilityEdgeDirected> graph; + mio::Graph>, mio::MobilityEdgeDirected> graph; graph.add_node(0, model, t0); graph.add_node(1, model2, t0); - size_t num_nodes = 200000; + size_t num_nodes = 100; for (size_t i = 2; i < num_nodes; i++) { auto local_model = model; graph.add_node(i, local_model, t0); @@ -86,7 +84,7 @@ int main(int /*argc*/, char** /*argv*/) auto distribution = mio::DiscreteDistributionInPlace(); std::vector uniform_vector(num_nodes, 1.0); mio::log_info("Nodes generated"); - for (size_t i = 0; i < 1 * num_nodes; ++i) { + for (size_t i = 0; i < 3 * num_nodes; ++i) { auto to = distribution(rng, {uniform_vector}); auto from = distribution(rng, {uniform_vector}); graph.add_edge(from, to); @@ -100,15 +98,27 @@ int main(int /*argc*/, char** /*argv*/) auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - for (size_t i = 0; i < 1 * num_nodes; i++) { - sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); - } + // for (size_t i = 0; i < 3 * num_nodes; i++) { + // sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + // } + // for (size_t i = 0; i < 1 * num_nodes; i++) { + // sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + // } + // for (size_t i = 0; i < 1 * num_nodes; i++) { + // sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + // } + + // mio::log_info("Number of exchanges: {}", sim.get_parameters().size()); - mio::log_info("Number of exchanges: {}", sim.get_parameters().size()); + // mio::log_info("Exchanges added"); - mio::log_info("Exchanges added"); + // sim.advance(tmax); + // mio::log_info("Simulation finished"); - sim.advance(tmax); + // auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); + // auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); + // mio::log_info("Distance from Bonn to Berlin: {}", bonn.distance(berlin)); + // mio::log_info("Distance from Berlin to Bonn: {}", berlin.distance(bonn)); // std::cout << "First table" << std::endl; // sim.get_graph().nodes()[0].property.get_result().print_table({"S", "E", "I", "R"}); diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 07ab9d89a4..7aa04564c6 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -20,24 +20,42 @@ #ifndef METAPOPULATION_MOBILITY_STOCHASTIC_H #define METAPOPULATION_MOBILITY_STOCHASTIC_H -#include "memilio/compartments/simulation.h" +#include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/random_number_generator.h" -#include "memilio/utils/time_series.h" +#include "memilio/geography/locations.h" #include "memilio/mobility/graph_simulation.h" +#include "memilio/mobility/graph.h" #include "memilio/mobility/metapopulation_mobility_instant.h" -#include "boost/filesystem.hpp" - #include #include -#include -#include -#include namespace mio { +template +class LocationNode : public SimulationNode +{ + using Base = SimulationNode; + +public: + template ::value, void>> + LocationNode(Args&&... args) + : Base(std::forward(args)...) + , m_location(0.000, 0.000) + { + } + + auto get_location() const + { + return m_location; + } + +private: + mio::geo::GeographicalLocation m_location; // location of the node +}; + /** * represents the mobility between two nodes. */ @@ -67,15 +85,14 @@ class MobilityEdgeDirected * @param node_to node that people changed to */ template - void apply_mobility(double num_moving, SimulationNode& node_from, SimulationNode& node_to); + void apply_mobility(double num_moving, LocationNode& node_from, LocationNode& node_to); // private: // MobilityParametersTimed m_parameters; }; template -void MobilityEdgeDirected::apply_mobility(double num_moving, SimulationNode& node_from, - SimulationNode& node_to) +void MobilityEdgeDirected::apply_mobility(double num_moving, LocationNode& node_from, LocationNode& node_to) { // auto next_event = m_parameters.process_next_event(); // auto num_moving = next_event.number; @@ -91,12 +108,13 @@ void MobilityEdgeDirected::apply_mobility(double num_moving, SimulationNode } template -void apply_timed_mobility(double t, double num_moving, MobilityEdgeDirected& edge, SimulationNode& node_from, - SimulationNode& node_to) +void apply_timed_mobility(double t, double num_moving, MobilityEdgeDirected& edge, LocationNode& node_from, + LocationNode& node_to) { // if (edge.next_event_time() >= t + dt) { // return; // } + mio::unused(t); edge.apply_mobility(num_moving, node_from, node_to); } // /**get_last_value @@ -121,23 +139,19 @@ void apply_timed_mobility(double t, double num_moving, MobilityEdgeDirected& edg * @{ */ template -AsymmetricGraphSimulation, MobilityEdgeDirected>> -make_mobility_sim(FP t0, FP dt, const Graph, MobilityEdgeDirected>& graph) +AsymmetricGraphSimulation, MobilityEdgeDirected>> +make_mobility_sim(FP t0, FP dt, const Graph, MobilityEdgeDirected>& graph) { return make_asymmetric_graph_sim( t0, dt, graph, &advance_model, - static_cast&, SimulationNode&)>(apply_mobility)); + static_cast&, LocationNode&)>(apply_timed_mobility)); } -// static_cast&, SimulationNode&)>(&apply_mobility) template -AsymmetricGraphSimulation, MobilityEdgeDirected>> -make_mobility_sim(FP t0, FP dt, Graph, MobilityEdgeDirected>&& graph) +AsymmetricGraphSimulation, MobilityEdgeDirected>> +make_mobility_sim(FP t0, FP dt, Graph, MobilityEdgeDirected>&& graph) { - return make_asymmetric_graph_sim( - t0, dt, std::move(graph), &advance_model, - static_cast&, SimulationNode&)>( - apply_timed_mobility)); + return make_asymmetric_graph_sim(t0, dt, std::move(graph), &advance_model, &apply_timed_mobility); } /** @} */ From 41839e563edfbb942403d9f2c51395610d3a696d Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:52:59 +0200 Subject: [PATCH 025/169] FIX: asymmetric graph simulation using LocationNode --- .../metapopulation_mobility_asymmetric.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 7aa04564c6..41b722cf9a 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -56,6 +56,16 @@ class LocationNode : public SimulationNode mio::geo::GeographicalLocation m_location; // location of the node }; +/** + * node functor for mobility-based simulation. + * @see SimulationNode::advance + */ +template +void advance_model(double t, double dt, LocationNode& node) +{ + node.advance(t, dt); +} + /** * represents the mobility between two nodes. */ @@ -151,7 +161,11 @@ template AsymmetricGraphSimulation, MobilityEdgeDirected>> make_mobility_sim(FP t0, FP dt, Graph, MobilityEdgeDirected>&& graph) { - return make_asymmetric_graph_sim(t0, dt, std::move(graph), &advance_model, &apply_timed_mobility); + using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, + void (*)(FP, FP, mio::MobilityEdgeDirected&, mio::LocationNode&, + mio::LocationNode&), + void (*)(FP, FP, mio::LocationNode&)>; + return GraphSim(t0, dt, std::move(graph), &advance_model, &apply_timed_mobility); } /** @} */ From 0fd3f11ab84291387890299782ebcdc5b36e370f Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:53:39 +0200 Subject: [PATCH 026/169] CHG: multiplications instead of divisions --- cpp/memilio/geography/locations.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h index 4b871961ef..fd99b216d9 100644 --- a/cpp/memilio/geography/locations.h +++ b/cpp/memilio/geography/locations.h @@ -29,6 +29,7 @@ namespace geo { const double earth_radius = 6371; +const double radians = M_PI / 180.0; class GeographicalLocation { @@ -66,11 +67,11 @@ class GeographicalLocation */ double distance(const GeographicalLocation& other) const { - double delta_latitude = (latitude - other.latitude) / 180 * M_PI; - double delta_longitude = (longitude - other.longitude) / 180 * M_PI; - double first_part = sin(delta_latitude / 2) * sin(delta_latitude / 2); - double second_part = cos(latitude / 180 * M_PI) * cos(other.latitude / 180 * M_PI) * sin(delta_longitude / 2) * - sin(delta_longitude / 2); + double delta_latitude = (latitude - other.latitude) * radians; + double delta_longitude = (longitude - other.longitude) * radians; + double first_part = sin(delta_latitude * 0.5) * sin(delta_latitude * 0.5); + double second_part = cos(latitude * radians) * cos(other.latitude * radians) * sin(delta_longitude * 0.5) * + sin(delta_longitude * 0.5); double distance = 2.0 * earth_radius * asin(sqrt(first_part + second_part)); return distance; } From 837de0423d0d24a2d6999869c1fa5b7d2f69080d Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:31:09 +0200 Subject: [PATCH 027/169] CHG: Add tests for GeographicalLocation --- cpp/tests/test_abm_location.cpp | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index 55d4c0fad6..bbf4777027 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -173,6 +173,42 @@ TEST_F(TestLocation, getGeographicalLocation) EXPECT_EQ(location.get_geographical_location(), geographical_location); } +/** + * @brief Test comparing geographical locations for equality. + */ +TEST_F(TestLocation, compareGeographicalLocation) +{ + // Set a geographical location for the location. + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location2 = {10.5100470359749, 52.2672785559812}; + // Verify that the set geographical location matches the expected values. + EXPECT_TRUE(geographical_location == geographical_location2); +} + +/** + * @brief Test comparing geographical locations for inequality. + */ +TEST_F(TestLocation, compareGeographicalLocation2) +{ + // Set a geographical location for the location. + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location2 = {10.5100470309749, 52.2672785559812}; + // Verify that the set geographical location matches the expected values. + EXPECT_FALSE(geographical_location == geographical_location2); +} + +/** + * @brief Test calculating the distance between two locations + */ +TEST_F(TestLocation, Distance) +{ + auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); + auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); + EXPECT_DOUBLE_EQ(bonn.distance(berlin), berlin.distance(bonn)); + auto distance = 478.2; + EXPECT_LT(abs(bonn.distance(berlin) - distance), 0.1); +} + /** * @brief Test whether the contact rates of a location are reduced if the total contacts in that location exceed the maximum number of contacts. */ From b5415b8dff9f4d0058aa863bd56150cc41cc081a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:45:32 +0200 Subject: [PATCH 028/169] CHG: remove make_asymmetric_mobility_sim --- cpp/memilio/mobility/graph_simulation.h | 7 ------- cpp/memilio/mobility/metapopulation_mobility_asymmetric.h | 8 +++++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 26b06f4535..efc593afc1 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -442,13 +442,6 @@ auto make_graph_sim_stochastic(FP t0, FP dt, Graph&& g, NodeF&& node_func, EdgeF t0, dt, std::forward(g), std::forward(node_func), std::forward(edge_func)); } -template -auto make_asymmetric_graph_sim(Timepoint t0, Timespan dt, Graph&& g, NodeF&& node_func, EdgeF&& edge_func) -{ - return AsymmetricGraphSimulation, Timepoint, Timespan, EdgeF, NodeF>( - t0, dt, std::forward(g), std::forward(node_func), std::forward(edge_func)); -} - // FeedbackGraphSimulation is only allowed to be used with local FeedbackSimulation. // Therefore, we use type traits to check if the type is a specialization of FeedbackSimulation template diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 41b722cf9a..1f45e2e7bc 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -152,9 +152,11 @@ template AsymmetricGraphSimulation, MobilityEdgeDirected>> make_mobility_sim(FP t0, FP dt, const Graph, MobilityEdgeDirected>& graph) { - return make_asymmetric_graph_sim( - t0, dt, graph, &advance_model, - static_cast&, LocationNode&)>(apply_timed_mobility)); + using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, + void (*)(FP, FP, mio::MobilityEdgeDirected&, mio::LocationNode&, + mio::LocationNode&), + void (*)(FP, FP, mio::LocationNode&)>; + return GraphSim(t0, dt, graph, &advance_model, &apply_timed_mobility); } template From 5213747afbccc132fc152f04b378e04c137428d1 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:16:27 +0200 Subject: [PATCH 029/169] CHG: Update LocationNode Constructor --- cpp/examples/asymmetric_graph.cpp | 57 +++++++------------ .../metapopulation_mobility_asymmetric.h | 11 +++- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 9282c2041a..55d82b62c4 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -35,9 +35,9 @@ enum class InfectionState int main(int /*argc*/, char** /*argv*/) { - const auto t0 = 0.; - // const auto tmax = 100.; - const auto dt = 1.; //initial time step + const auto t0 = 0.; + const auto tmax = 100.; + const auto dt = 1.; //initial time step //total compartment sizes double num_total = 10000, num_exp = 200, num_ins = 50, num_rec = 0; @@ -58,27 +58,19 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.5}}}); model.parameters.get>() = adoption_rates; - // std::vector> transition_rates; - // for (size_t s = 0; s < static_cast(InfectionState::Count); ++s) { - // transition_rates.push_back({InfectionState(s), home, home, 0}); - // } - // model.parameters.get>() = transition_rates; - //modify model for second node auto model2 = model; mio::Graph>, mio::MobilityEdgeDirected> graph; - graph.add_node(0, model, t0); - graph.add_node(1, model2, t0); - size_t num_nodes = 100; + graph.add_node(0, 12.0, 21.0, model, t0); + graph.add_node(1, 12.0, 21.0, model2, t0); + size_t num_nodes = 999; for (size_t i = 2; i < num_nodes; i++) { auto local_model = model; - graph.add_node(i, local_model, t0); + graph.add_node(i, 12.0, 21.0, local_model, t0); } auto param = mio::MobilityParametersTimed(2.0, 10, 1); graph.add_edge(0, 1); - // graph.edges()[0].property.add_exchange(5.0, 5, 1); - // graph.edges()[0].property.add_exchange(6.0, 500, 1); auto rng = mio::RandomNumberGenerator(); auto distribution = mio::DiscreteDistributionInPlace(); @@ -88,37 +80,28 @@ int main(int /*argc*/, char** /*argv*/) auto to = distribution(rng, {uniform_vector}); auto from = distribution(rng, {uniform_vector}); graph.add_edge(from, to); - - // graph.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i) - // graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); - // graph.edges().back().property.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 5, to); } mio::log_info("Graph generated"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - // for (size_t i = 0; i < 3 * num_nodes; i++) { - // sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); - // } - // for (size_t i = 0; i < 1 * num_nodes; i++) { - // sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); - // } - // for (size_t i = 0; i < 1 * num_nodes; i++) { - // sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); - // } - - // mio::log_info("Number of exchanges: {}", sim.get_parameters().size()); + for (size_t i = 0; i < 3 * num_nodes; i++) { + sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + } + for (size_t i = 0; i < 3 * num_nodes; i++) { + sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + } + for (size_t i = 0; i < 3 * num_nodes; i++) { + sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + } - // mio::log_info("Exchanges added"); + mio::log_info("Number of exchanges: {}", sim.get_parameters().size()); - // sim.advance(tmax); - // mio::log_info("Simulation finished"); + mio::log_info("Exchanges added"); - // auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); - // auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); - // mio::log_info("Distance from Bonn to Berlin: {}", bonn.distance(berlin)); - // mio::log_info("Distance from Berlin to Bonn: {}", berlin.distance(bonn)); + sim.advance(tmax); + mio::log_info("Simulation finished"); // std::cout << "First table" << std::endl; // sim.get_graph().nodes()[0].property.get_result().print_table({"S", "E", "I", "R"}); diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 1f45e2e7bc..21d0de6672 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -41,9 +41,10 @@ class LocationNode : public SimulationNode public: template ::value, void>> - LocationNode(Args&&... args) + LocationNode(double latitude, double longitude, Args&&... args) : Base(std::forward(args)...) - , m_location(0.000, 0.000) + , m_location(latitude, longitude) + , regional_neighbor_indices{} { } @@ -52,8 +53,14 @@ class LocationNode : public SimulationNode return m_location; } + void set_location(double latitude, double longitude) + { + m_location = mio::geo::GeographicalLocation(latitude, longitude); + } + private: mio::geo::GeographicalLocation m_location; // location of the node + std::vector> regional_neighbor_indices; }; /** From 2af6d2c24b3192261f268f0464f169acdf4e57fb Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:36:10 +0200 Subject: [PATCH 030/169] CHG: Move geographical location to location file in memilio/geography --- cpp/memilio/geography/locations.h | 81 ++++++++++++++++++++++++++++ cpp/models/abm/common_abm_loggers.h | 2 +- cpp/models/abm/location.h | 32 ++--------- cpp/simulations/abm_braunschweig.cpp | 4 +- cpp/tests/test_abm_location.cpp | 2 +- 5 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 cpp/memilio/geography/locations.h diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h new file mode 100644 index 0000000000..4b871961ef --- /dev/null +++ b/cpp/memilio/geography/locations.h @@ -0,0 +1,81 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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/io/default_serialize.h" +#include +#include +#include +#include "memilio/utils/logging.h" +namespace mio +{ +namespace geo +{ + +const double earth_radius = 6371; + +class GeographicalLocation +{ +public: + double latitude; + double longitude; + + /** + * @brief Compare two GeographicalLocation%s. + */ + bool operator==(const GeographicalLocation& other) const + { + return (latitude == other.latitude && longitude == other.longitude); + } + + bool operator!=(const GeographicalLocation& other) const + { + return !(latitude == other.latitude && longitude == other.longitude); + } + + /// This method is used by the default serialization feature. + auto default_serialize() + { + return Members("GeographicalLocation").add("latitude", latitude).add("longitude", longitude); + } + + /* + * @brief Calculate the distance between two geographical locations + * @param other The other geographical location. + * @return The distance between the two locations in kilometers. + * + * Uses the haversine formula (https://en.wikipedia.org/wiki/Haversine_formula) to calculate the distance between + * two geographical locations. Uses an earth radius of 6375 km (https://en.wikipedia.org/wiki/Earth_radius). + * The math functions use radians, whereas coordinates are given in degrees. + */ + double distance(const GeographicalLocation& other) const + { + double delta_latitude = (latitude - other.latitude) / 180 * M_PI; + double delta_longitude = (longitude - other.longitude) / 180 * M_PI; + double first_part = sin(delta_latitude / 2) * sin(delta_latitude / 2); + double second_part = cos(latitude / 180 * M_PI) * cos(other.latitude / 180 * M_PI) * sin(delta_longitude / 2) * + sin(delta_longitude / 2); + double distance = 2.0 * earth_radius * asin(sqrt(first_part + second_part)); + return distance; + } +}; + +} // namespace geo + +} // namespace mio \ No newline at end of file diff --git a/cpp/models/abm/common_abm_loggers.h b/cpp/models/abm/common_abm_loggers.h index a453d9554b..242dfe659c 100644 --- a/cpp/models/abm/common_abm_loggers.h +++ b/cpp/models/abm/common_abm_loggers.h @@ -80,7 +80,7 @@ constexpr mio::abm::ActivityType guess_activity_type(mio::abm::LocationType curr */ struct LogLocationInformation : mio::LogOnce { using Type = std::vector< - std::tuple>; + std::tuple>; /** * @brief Log the LocationInformation of the simulation. * @param[in] sim The simulation of the abm. diff --git a/cpp/models/abm/location.h b/cpp/models/abm/location.h index cd18d1b361..61349ae830 100644 --- a/cpp/models/abm/location.h +++ b/cpp/models/abm/location.h @@ -25,6 +25,7 @@ #include "abm/parameters.h" #include "abm/location_type.h" +#include "memilio/geography/locations.h" #include "memilio/io/default_serialize.h" #include "boost/atomic/atomic.hpp" @@ -33,30 +34,6 @@ namespace mio namespace abm { -struct GeographicalLocation { - double latitude; - double longitude; - - /** - * @brief Compare two GeographicalLocation%s. - */ - bool operator==(const GeographicalLocation& other) const - { - return (latitude == other.latitude && longitude == other.longitude); - } - - bool operator!=(const GeographicalLocation& other) const - { - return !(latitude == other.latitude && longitude == other.longitude); - } - - /// This method is used by the default serialization feature. - auto default_serialize() - { - return Members("GraphicalLocation").add("latitude", latitude).add("longitude", longitude); - } -}; - struct CellIndex : public mio::Index { CellIndex(size_t i) : mio::Index(i) @@ -248,7 +225,7 @@ class Location * @brief Get the geographical location of the Location. * @return The geographical location of the Location. */ - GeographicalLocation get_geographical_location() const + mio::geo::GeographicalLocation get_geographical_location() const { return m_geographical_location; } @@ -257,7 +234,7 @@ class Location * @brief Set the geographical location of the Location. * @param[in] location The geographical location of the Location. */ - void set_geographical_location(GeographicalLocation location) + void set_geographical_location(mio::geo::GeographicalLocation location) { m_geographical_location = location; } @@ -292,7 +269,8 @@ class Location LocalInfectionParameters m_parameters; ///< Infection parameters for the Location. std::vector m_cells{}; ///< A vector of all Cell%s that the Location is divided in. MaskType m_required_mask; ///< Least secure type of Mask that is needed to enter the Location. - GeographicalLocation m_geographical_location; ///< Geographical location (longitude and latitude) of the Location. + mio::geo::GeographicalLocation + m_geographical_location; ///< Geographical location (longitude and latitude) of the Location. int m_model_id; ///< Model id the location is in. Only used for ABM graph model or hybrid graph model. }; diff --git a/cpp/simulations/abm_braunschweig.cpp b/cpp/simulations/abm_braunschweig.cpp index 5f1e1d1394..a6d9be1668 100644 --- a/cpp/simulations/abm_braunschweig.cpp +++ b/cpp/simulations/abm_braunschweig.cpp @@ -310,14 +310,14 @@ void create_model_from_data(mio::abm::Model& model, const std::string& filename, uint32_t home_id = row[index["huid"]]; uint32_t target_location_id = std::abs(row[index["loc_id_end"]]); uint32_t activity_end = row[index["activity_end"]]; - mio::abm::GeographicalLocation location_long_lat = {(double)row[index["lon_end"]] / 1e+5, + mio::geo::GeographicalLocation location_long_lat = {(double)row[index["lon_end"]] / 1e+5, (double)row[index["lat_end"]] / 1e+5}; mio::abm::LocationId home; auto it_home = locations.find(home_id); if (it_home == locations.end()) { home = model.add_location(mio::abm::LocationType::Home, 1); locations.insert({home_id, home}); - mio::abm::GeographicalLocation location_long_lat_home = {(double)row[index["lon_start"]] / 1e+5, + mio::geo::GeographicalLocation location_long_lat_home = {(double)row[index["lon_start"]] / 1e+5, (double)row[index["lat_start"]] / 1e+5}; model.get_location(home).set_geographical_location(location_long_lat_home); } diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index 0d57d18fa6..55d4c0fad6 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -167,7 +167,7 @@ TEST_F(TestLocation, getGeographicalLocation) // Create a location of type Home. auto location = mio::abm::Location(mio::abm::LocationType::Home, 0); // Set a geographical location for the location. - mio::abm::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; location.set_geographical_location(geographical_location); // Verify that the set geographical location matches the expected values. EXPECT_EQ(location.get_geographical_location(), geographical_location); From 02d6a374b6baec311659ce9088f8abe58e83bda7 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:53:39 +0200 Subject: [PATCH 031/169] CHG: multiplications instead of divisions --- cpp/memilio/geography/locations.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h index 4b871961ef..fd99b216d9 100644 --- a/cpp/memilio/geography/locations.h +++ b/cpp/memilio/geography/locations.h @@ -29,6 +29,7 @@ namespace geo { const double earth_radius = 6371; +const double radians = M_PI / 180.0; class GeographicalLocation { @@ -66,11 +67,11 @@ class GeographicalLocation */ double distance(const GeographicalLocation& other) const { - double delta_latitude = (latitude - other.latitude) / 180 * M_PI; - double delta_longitude = (longitude - other.longitude) / 180 * M_PI; - double first_part = sin(delta_latitude / 2) * sin(delta_latitude / 2); - double second_part = cos(latitude / 180 * M_PI) * cos(other.latitude / 180 * M_PI) * sin(delta_longitude / 2) * - sin(delta_longitude / 2); + double delta_latitude = (latitude - other.latitude) * radians; + double delta_longitude = (longitude - other.longitude) * radians; + double first_part = sin(delta_latitude * 0.5) * sin(delta_latitude * 0.5); + double second_part = cos(latitude * radians) * cos(other.latitude * radians) * sin(delta_longitude * 0.5) * + sin(delta_longitude * 0.5); double distance = 2.0 * earth_radius * asin(sqrt(first_part + second_part)); return distance; } From abdb2f96fb07c22bfd485500f66ca15de14a07dc Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:31:09 +0200 Subject: [PATCH 032/169] CHG: Add tests for GeographicalLocation --- cpp/tests/test_abm_location.cpp | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index 55d4c0fad6..bbf4777027 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -173,6 +173,42 @@ TEST_F(TestLocation, getGeographicalLocation) EXPECT_EQ(location.get_geographical_location(), geographical_location); } +/** + * @brief Test comparing geographical locations for equality. + */ +TEST_F(TestLocation, compareGeographicalLocation) +{ + // Set a geographical location for the location. + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location2 = {10.5100470359749, 52.2672785559812}; + // Verify that the set geographical location matches the expected values. + EXPECT_TRUE(geographical_location == geographical_location2); +} + +/** + * @brief Test comparing geographical locations for inequality. + */ +TEST_F(TestLocation, compareGeographicalLocation2) +{ + // Set a geographical location for the location. + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location2 = {10.5100470309749, 52.2672785559812}; + // Verify that the set geographical location matches the expected values. + EXPECT_FALSE(geographical_location == geographical_location2); +} + +/** + * @brief Test calculating the distance between two locations + */ +TEST_F(TestLocation, Distance) +{ + auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); + auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); + EXPECT_DOUBLE_EQ(bonn.distance(berlin), berlin.distance(bonn)); + auto distance = 478.2; + EXPECT_LT(abs(bonn.distance(berlin) - distance), 0.1); +} + /** * @brief Test whether the contact rates of a location are reduced if the total contacts in that location exceed the maximum number of contacts. */ From 7888062c842f3b482647d7543830e3f0816dfadd Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:28:19 +0200 Subject: [PATCH 033/169] CHG: Add data_types entry for GeographicLocation --- docs/source/cpp/data_types.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/cpp/data_types.rst b/docs/source/cpp/data_types.rst index 006397a460..10db1e4225 100644 --- a/docs/source/cpp/data_types.rst +++ b/docs/source/cpp/data_types.rst @@ -49,3 +49,5 @@ The following list explains the nonstandard data types that are used throughout - Represents a coefficient-wise matrix expression :math:`B - D \odot (B - M)`, where :math:`B` is a baseline matrix, :math:`M` is a minimum matrix, :math:`D` is a time-dependent complex damping factor, and :math:`\odot` is element wise multiplication. Used as the base for time-dependent contact matrices. * - :code:`DampingMatrixExpressionGroup` - Represents a collection of ``DampingMatrixExpression``\s that are summed up. Used for representing multiple sources of contacts or mobility. + * - :code:`GeographicLocation` + - Stores coordinates in (Latitude, Longitude) format. Allows for comparisons and realtistic distance calculations. From 2905b844768111c9f5b3e2d15d263888578b653f Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:52:54 +0200 Subject: [PATCH 034/169] NEW: add r-Tree to mio::geo --- cpp/memilio/geography/tree.h | 223 ++++++++++++++++++++++++++++++++ cpp/tests/CMakeLists.txt | 1 + cpp/tests/test_abm_location.cpp | 54 ++------ cpp/tests/test_geography.cpp | 144 +++++++++++++++++++++ 4 files changed, 377 insertions(+), 45 deletions(-) create mode 100644 cpp/memilio/geography/tree.h create mode 100644 cpp/tests/test_geography.cpp diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h new file mode 100644 index 0000000000..56322313ed --- /dev/null +++ b/cpp/memilio/geography/tree.h @@ -0,0 +1,223 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 "math.h" +#include "locations.h" +#include +#include +#include +#include +#include + +#include "boost/geometry/geometry.hpp" + +#include +#include +#include + +namespace bg = boost::geometry; +namespace bgi = bg::index; +namespace bgsb = bg::strategy::buffer; + +namespace mio +{ +namespace geo +{ +using Point = bg::model::point>; +typedef std::pair Node; + +template +concept IsSphericalLocation = requires(const Location& loc) { + std::is_floating_point_v && std::is_floating_point_v; +}; + +template +concept IsSphericalLocationIterator = std::input_iterator && IsSphericalLocation())>; + +/** + * @brief R-Tree for spatial queries of geographical locations on the sphere + * + * Wraps the Boost::geometry::index::rtree. Can be initialized with a vector of geographical location data or a range. + * The provided location data needs to expose a latitude and a longitude parameter. + */ + +class RTree +{ +public: + /** + * @brief Construct a new RTree object without data + * + */ + RTree() = default; + + /** + * @brief Construct a new RTree object with data given in a vector + * + * @param locations A vector of geographical locations, they need to expose a latitude and a longitude parameter. + */ + template + RTree(const std::vector& locations) + : rtree{} + { + for (size_t index = 0; index < locations.size(); index++) { + Point point(locations[index].longitude, locations[index].latitude); + rtree.insert(Node(point, index)); + } + } + + /** + * @brief Construct a new RTree object with data given in a range + * + * @param first The beginning of the range + * @param last The end of the range + * The provided location data needs to expose a latitude and a longitude parameter. + */ + template + RTree(Iter first, Iter last) + : rtree{} + { + size_t index = 0; + while (first != last) { + Point point(first->longitude, first->latitude); + rtree.insert(Node(point, index)); + ++first; + ++index; + } + } + /** + * @brief Return the number of data points stored in the RTree + * + * @return size of the tree + */ + auto size() const + { + return rtree.size(); + } + + /** + * @brief Return the indices of the k nearest neighbours of a given location + * + * @param location Midpoint for the query, exposes latitude and longitude + * @param number The number of nearest neighbours to find + * @return Vector with indices of the nearest neighbours + */ + auto nearest_neighbor_indices(const IsSphericalLocation auto& location, size_t number) const + { + Point point(location.longitude, location.latitude); + std::vector indices; + bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element(indices)); + return indices; + } + + /** + * @brief Return the indices of the points within a given radius where the circle is approximated by a polygon + * + * @param location Midpoint for the query, exposes latitude and longitude + * @param radius The radius of the query + * @return Vector with indices of the points found + */ + auto inrange_indices_approximate(const IsSphericalLocation auto& location, double radius) const + { + auto radius_in_meter = 1000 * radius; + auto circle = create_circle_approximation(location, radius_in_meter); + std::vector indices; + bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element(indices)); + return indices; + } + + /** + * @brief Return the indices of the points within a given radius + * + * @param location Midpoint for the query, exposes latitude and longitude + * @param radius The radius of the query + * @return Vector with indices of the points found + * + * Basically the same as \ref inrange_indices_approximate, but filters the result to make sure the points are within the radius. + */ + auto inrange_indices(const IsSphericalLocation auto& location, double radius) const + { + auto radius_in_meter = 1000 * radius; + + auto circle = create_circle_approximation(location, radius_in_meter); + std::vector nodes; + bgi::query(rtree, bgi::covered_by(circle), std::back_inserter(nodes)); + auto midpoint = Point(location.longitude, location.latitude); + std::vector indices; + for (auto& node : nodes) { + mio::log_info("Distance: {}", bg::distance(midpoint, node.first)); + if (bg::distance(midpoint, node.first) < radius_in_meter) { + indices.push_back(node.second); + } + } + return indices; + } + +private: + /** + * @brief Create a circle approximation object + * + * @param location Midpoint, needs to expose latitude and longitude + * @param radius in meters + * @return auto + */ + auto create_circle_approximation(const IsSphericalLocation auto& location, double radius) const + { + bgsb::geographic_point_circle<> point_strategy(36); + bgsb::distance_symmetric distance_strategy(radius); + bgsb::join_round join_strategy; + bgsb::end_round end_strategy; + bgsb::side_straight side_strategy; + + Point midpoint(location.longitude, location.latitude); + + bg::model::multi_polygon> circle; + bg::buffer(midpoint, circle, distance_strategy, side_strategy, join_strategy, end_strategy, point_strategy); + return circle; + } + + /** + * @brief Back inserter that ignores the first element of pairs given to it + */ + template + struct back_inserter_second_element { + Container& container; + back_inserter_second_element& operator*() + { + return *this; + } + back_inserter_second_element& operator++() + { + return *this; + } + back_inserter_second_element operator++(int) + { + return *this; + } + back_inserter_second_element operator=(const Node& node) + { + container.push_back(node.second); + return *this; + } + }; + bgi::rtree> rtree; +}; + +} // namespace geo +} // namespace mio diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 126e17ff2c..31e9906071 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -74,6 +74,7 @@ set(TESTSOURCES test_glct_secir.cpp test_ad.cpp test_smm_model.cpp + test_geography.cpp abm_helpers.h abm_helpers.cpp actions.h diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index bbf4777027..44bc755f1f 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -26,12 +26,12 @@ #include "memilio/utils/parameter_distributions.h" #include "random_number_test.h" -using TestLocation = RandomNumberTest; +using TestGeography = RandomNumberTest; /** * @brief Test that initializing a location with cells correctly creates the given number of cells. */ -TEST_F(TestLocation, initCell) +TEST_F(TestGeography, initCell) { // Create a location of type PublicTransport with 2 cells. mio::abm::Location location(mio::abm::LocationType::PublicTransport, 0, 6, 0, 2); @@ -42,7 +42,7 @@ TEST_F(TestLocation, initCell) /** * @brief Test that a location correctly returns its ID. */ -TEST_F(TestLocation, getId) +TEST_F(TestGeography, getId) { // Create a location of type Home with an ID of 0. mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); @@ -53,7 +53,7 @@ TEST_F(TestLocation, getId) /** * @brief Test that the computation of space per person relative to capacity works correctly. */ -TEST_F(TestLocation, computeSpacePerPersonRelative) +TEST_F(TestGeography, computeSpacePerPersonRelative) { using testing::Return; @@ -74,7 +74,7 @@ TEST_F(TestLocation, computeSpacePerPersonRelative) /** * @brief Test the interaction between infected and susceptible persons at a location. */ -TEST_F(TestLocation, interact) +TEST_F(TestGeography, interact) { using testing::Return; @@ -134,7 +134,7 @@ TEST_F(TestLocation, interact) /** * @brief Test setting and getting the capacity of a location. */ -TEST_F(TestLocation, setCapacity) +TEST_F(TestGeography, setCapacity) { // Create a location of type Home. mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); @@ -147,7 +147,7 @@ TEST_F(TestLocation, setCapacity) /** * @brief Test setting and getting the required mask type at a location. */ -TEST_F(TestLocation, setRequiredMask) +TEST_F(TestGeography, setRequiredMask) { // Create a location of type Home. mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); @@ -162,7 +162,7 @@ TEST_F(TestLocation, setRequiredMask) /** * @brief Test setting and getting the geographical location of a location. */ -TEST_F(TestLocation, getGeographicalLocation) +TEST_F(TestGeography, getGeographicalLocation) { // Create a location of type Home. auto location = mio::abm::Location(mio::abm::LocationType::Home, 0); @@ -173,46 +173,10 @@ TEST_F(TestLocation, getGeographicalLocation) EXPECT_EQ(location.get_geographical_location(), geographical_location); } -/** - * @brief Test comparing geographical locations for equality. - */ -TEST_F(TestLocation, compareGeographicalLocation) -{ - // Set a geographical location for the location. - mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; - mio::geo::GeographicalLocation geographical_location2 = {10.5100470359749, 52.2672785559812}; - // Verify that the set geographical location matches the expected values. - EXPECT_TRUE(geographical_location == geographical_location2); -} - -/** - * @brief Test comparing geographical locations for inequality. - */ -TEST_F(TestLocation, compareGeographicalLocation2) -{ - // Set a geographical location for the location. - mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; - mio::geo::GeographicalLocation geographical_location2 = {10.5100470309749, 52.2672785559812}; - // Verify that the set geographical location matches the expected values. - EXPECT_FALSE(geographical_location == geographical_location2); -} - -/** - * @brief Test calculating the distance between two locations - */ -TEST_F(TestLocation, Distance) -{ - auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); - auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); - EXPECT_DOUBLE_EQ(bonn.distance(berlin), berlin.distance(bonn)); - auto distance = 478.2; - EXPECT_LT(abs(bonn.distance(berlin) - distance), 0.1); -} - /** * @brief Test whether the contact rates of a location are reduced if the total contacts in that location exceed the maximum number of contacts. */ -TEST_F(TestLocation, adjustContactRates) +TEST_F(TestGeography, adjustContactRates) { mio::abm::Location loc(mio::abm::LocationType::SocialEvent, mio::abm::LocationId(0)); //Set the maximum contacts smaller than the contact rates diff --git a/cpp/tests/test_geography.cpp b/cpp/tests/test_geography.cpp new file mode 100644 index 0000000000..b83eeded3d --- /dev/null +++ b/cpp/tests/test_geography.cpp @@ -0,0 +1,144 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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/geography/tree.h" +#include "random_number_test.h" + +using TestLocation = RandomNumberTest; + +/** + * @brief Test comparing geographical locations for equality. + */ +TEST_F(TestLocation, compareGeographicalLocation) +{ + // Set a geographical location for the location. + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location2 = {10.5100470359749, 52.2672785559812}; + // Verify that the set geographical location matches the expected values. + EXPECT_TRUE(geographical_location == geographical_location2); +} + +/** + * @brief Test comparing geographical locations for inequality. + */ +TEST_F(TestLocation, compareGeographicalLocation2) +{ + // Set a geographical location for the location. + mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; + mio::geo::GeographicalLocation geographical_location2 = {10.5100470309749, 52.2672785559812}; + // Verify that the set geographical location matches the expected values. + EXPECT_FALSE(geographical_location == geographical_location2); +} + +/** + * @brief Test calculating the distance between two locations + */ +TEST_F(TestLocation, Distance) +{ + auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); + auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); + EXPECT_DOUBLE_EQ(bonn.distance(berlin), berlin.distance(bonn)); + auto distance = 478.2; + EXPECT_LT(abs(bonn.distance(berlin) - distance), 0.1); +} + +/** + * @brief Test the default r-Tree Constructor + */ +TEST_F(TestLocation, rtreeConstructionNoData) +{ + EXPECT_NO_THROW(mio::geo::RTree()); +} + +/** + * @brief Test the r-Tree Constructor given a vector + */ +TEST_F(TestLocation, rtreeConstructionVector) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + EXPECT_NO_THROW(mio::geo::RTree tree(locations)); +} + +/** + * @brief Test the r-Tree Constructor given a range + */ +TEST_F(TestLocation, rtreeConstructionRange) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + EXPECT_NO_THROW(mio::geo::RTree(locations.begin(), locations.end())); +} + +/** + * @brief Test the size function of an r-Tree + */ +TEST_F(TestLocation, rtreesize) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + auto rtree = mio::geo::RTree(locations.begin(), locations.end()); + EXPECT_EQ(rtree.size(), 3); +} + +/** + * @brief Test the nearest neighbours query of an r-Tree + */ +TEST_F(TestLocation, rtreeNN) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + auto rtree = mio::geo::RTree(locations.begin(), locations.end()); + EXPECT_EQ(rtree.nearest_neighbor_indices(mio::geo::GeographicalLocation(50.781, 6.080), 1)[0], 0); +} + +/** + * @brief Test the in-range query of an r-Tree + */ +TEST_F(TestLocation, rtreeinrange_approx) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + auto rtree = mio::geo::RTree(locations.begin(), locations.end()); + EXPECT_EQ(rtree.inrange_indices_approximate(mio::geo::GeographicalLocation(50.933501, 6.875124), 150).size(), 2); +} + +/** + * @brief Test the exact in-range query of an r-Tree + */ +TEST_F(TestLocation, rtreeinrange) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + auto rtree = mio::geo::RTree(locations.begin(), locations.end()); + EXPECT_EQ(rtree.inrange_indices(mio::geo::GeographicalLocation(50.933501, 6.875124), 150).size(), 2); +} \ No newline at end of file From 2e27acd3c1a36961012f3b532048b6fd19671201 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:15:24 +0200 Subject: [PATCH 035/169] CHG: add getter_functions for lat, long to Location Node --- .../mobility/metapopulation_mobility_asymmetric.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 21d0de6672..055f02ba0e 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -58,6 +58,16 @@ class LocationNode : public SimulationNode m_location = mio::geo::GeographicalLocation(latitude, longitude); } + double get_longitude() const + { + return m_location.longitude; + } + + double get_latitude() const + { + return m_location.latitude; + } + private: mio::geo::GeographicalLocation m_location; // location of the node std::vector> regional_neighbor_indices; From ca6646f16f52e09b239326488dd8134af6df7f9e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:50:22 +0200 Subject: [PATCH 036/169] CHG: unnecessary changes for CI --- cpp/memilio/geography/locations.h | 13 +++++++++++++ cpp/memilio/geography/tree.h | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h index fd99b216d9..b3dc09ce9c 100644 --- a/cpp/memilio/geography/locations.h +++ b/cpp/memilio/geography/locations.h @@ -37,6 +37,19 @@ class GeographicalLocation double latitude; double longitude; + GeographicalLocation(double lat, double lon) + : latitude(lat) + , longitude(lon) + { + } + GeographicalLocation(std::pair coordinates) + : latitude(coordinates.first) + , longitude(coordinates.second) + { + } + + GeographicalLocation() = default; + /** * @brief Compare two GeographicalLocation%s. */ diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 56322313ed..2ada7d68ba 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -122,7 +122,7 @@ class RTree { Point point(location.longitude, location.latitude); std::vector indices; - bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element(indices)); + bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element>(indices)); return indices; } @@ -138,7 +138,7 @@ class RTree auto radius_in_meter = 1000 * radius; auto circle = create_circle_approximation(location, radius_in_meter); std::vector indices; - bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element(indices)); + bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element>(indices)); return indices; } From ad9afc5b6b7e8a989c29078efce036430e407bf2 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:01:33 +0200 Subject: [PATCH 037/169] CHG: Make latitude and longitude privat --- cpp/memilio/geography/locations.h | 28 +++++++++++++++++++++++++--- cpp/memilio/geography/tree.h | 28 ++++++++++++++-------------- cpp/simulations/abm_braunschweig.cpp | 4 ++-- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h index b3dc09ce9c..8e69366b31 100644 --- a/cpp/memilio/geography/locations.h +++ b/cpp/memilio/geography/locations.h @@ -33,10 +33,8 @@ const double radians = M_PI / 180.0; class GeographicalLocation { -public: - double latitude; - double longitude; +public: GeographicalLocation(double lat, double lon) : latitude(lat) , longitude(lon) @@ -88,6 +86,30 @@ class GeographicalLocation double distance = 2.0 * earth_radius * asin(sqrt(first_part + second_part)); return distance; } + + auto get_latitude() const -> double + { + return latitude; + } + + auto get_longitude() const -> double + { + return longitude; + } + + auto set_latitude(double lat) -> void + { + latitude = lat; + } + + auto set_longitude(double lon) -> void + { + longitude = lon; + } + +private: + double latitude; + double longitude; }; } // namespace geo diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 2ada7d68ba..619abd9b85 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -45,7 +45,7 @@ typedef std::pair Node; template concept IsSphericalLocation = requires(const Location& loc) { - std::is_floating_point_v && std::is_floating_point_v; + std::is_floating_point_v && std::is_floating_point_v; }; template @@ -55,7 +55,7 @@ concept IsSphericalLocationIterator = std::input_iterator && IsSphericalLo * @brief R-Tree for spatial queries of geographical locations on the sphere * * Wraps the Boost::geometry::index::rtree. Can be initialized with a vector of geographical location data or a range. - * The provided location data needs to expose a latitude and a longitude parameter. + * The provided location data needs to provide get_latitude() and get_longitude(). */ class RTree @@ -70,14 +70,14 @@ class RTree /** * @brief Construct a new RTree object with data given in a vector * - * @param locations A vector of geographical locations, they need to expose a latitude and a longitude parameter. + * @param locations A vector of geographical locations, they need to provide get_latitude() and get_longitude(). */ template RTree(const std::vector& locations) : rtree{} { for (size_t index = 0; index < locations.size(); index++) { - Point point(locations[index].longitude, locations[index].latitude); + Point point(locations[index].get_longitude(), locations[index].get_latitude()); rtree.insert(Node(point, index)); } } @@ -87,7 +87,7 @@ class RTree * * @param first The beginning of the range * @param last The end of the range - * The provided location data needs to expose a latitude and a longitude parameter. + * The provided location data needs to provide get_latitude() and get_longitude(). */ template RTree(Iter first, Iter last) @@ -95,7 +95,7 @@ class RTree { size_t index = 0; while (first != last) { - Point point(first->longitude, first->latitude); + Point point(first->get_longitude(), first->get_latitude()); rtree.insert(Node(point, index)); ++first; ++index; @@ -114,13 +114,13 @@ class RTree /** * @brief Return the indices of the k nearest neighbours of a given location * - * @param location Midpoint for the query, exposes latitude and longitude + * @param location Midpoint for the query, provides get_latitude() and get_longitude() * @param number The number of nearest neighbours to find * @return Vector with indices of the nearest neighbours */ auto nearest_neighbor_indices(const IsSphericalLocation auto& location, size_t number) const { - Point point(location.longitude, location.latitude); + Point point(location.get_longitude(), location.get_latitude()); std::vector indices; bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element>(indices)); return indices; @@ -129,7 +129,7 @@ class RTree /** * @brief Return the indices of the points within a given radius where the circle is approximated by a polygon * - * @param location Midpoint for the query, exposes latitude and longitude + * @param location Midpoint for the query, provides get_latitude() and get_longitude() * @param radius The radius of the query * @return Vector with indices of the points found */ @@ -145,7 +145,7 @@ class RTree /** * @brief Return the indices of the points within a given radius * - * @param location Midpoint for the query, exposes latitude and longitude + * @param location Midpoint for the query, provides get_latitude() and get_longitude() * @param radius The radius of the query * @return Vector with indices of the points found * @@ -158,7 +158,7 @@ class RTree auto circle = create_circle_approximation(location, radius_in_meter); std::vector nodes; bgi::query(rtree, bgi::covered_by(circle), std::back_inserter(nodes)); - auto midpoint = Point(location.longitude, location.latitude); + auto midpoint = Point(location.get_longitude(), location.get_latitude()); std::vector indices; for (auto& node : nodes) { mio::log_info("Distance: {}", bg::distance(midpoint, node.first)); @@ -172,8 +172,8 @@ class RTree private: /** * @brief Create a circle approximation object - * - * @param location Midpoint, needs to expose latitude and longitude + * + * @param location Midpoint, needs to provide get_latitude() and get_longitude() * @param radius in meters * @return auto */ @@ -185,7 +185,7 @@ class RTree bgsb::end_round end_strategy; bgsb::side_straight side_strategy; - Point midpoint(location.longitude, location.latitude); + Point midpoint(location.get_longitude(), location.get_latitude()); bg::model::multi_polygon> circle; bg::buffer(midpoint, circle, distance_strategy, side_strategy, join_strategy, end_strategy, point_strategy); diff --git a/cpp/simulations/abm_braunschweig.cpp b/cpp/simulations/abm_braunschweig.cpp index a6d9be1668..0f2c484608 100644 --- a/cpp/simulations/abm_braunschweig.cpp +++ b/cpp/simulations/abm_braunschweig.cpp @@ -879,8 +879,8 @@ void write_log_to_file_person_and_location_data(const T& history) for (uint32_t loc_id_index = 0; loc_id_index < loc_id.size(); ++loc_id_index) { auto id = std::get<0>(loc_id[loc_id_index]); auto location_type = (int)std::get<1>(loc_id[loc_id_index]); - auto id_longitute = std::get<2>(loc_id[loc_id_index]).longitude; - auto id_latitude = std::get<2>(loc_id[loc_id_index]).latitude; + auto id_longitute = std::get<2>(loc_id[loc_id_index]).get_longitude(); + auto id_latitude = std::get<2>(loc_id[loc_id_index]).get_latitude(); myfile << id << ", " << location_type << ", " << id_longitute << ", " << id_latitude << "\n"; } myfile.close(); From 711a25f4d1cb68deb072d56b6b21e632daf8ed1e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:31:29 +0200 Subject: [PATCH 038/169] CHG: remove duplicate tests --- cpp/tests/test_abm_location.cpp | 36 --------------------------------- 1 file changed, 36 deletions(-) diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index ee1dfbba6e..44bc755f1f 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -173,42 +173,6 @@ TEST_F(TestGeography, getGeographicalLocation) EXPECT_EQ(location.get_geographical_location(), geographical_location); } -/** - * @brief Test comparing geographical locations for equality. - */ -TEST_F(TestLocation, compareGeographicalLocation) -{ - // Set a geographical location for the location. - mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; - mio::geo::GeographicalLocation geographical_location2 = {10.5100470359749, 52.2672785559812}; - // Verify that the set geographical location matches the expected values. - EXPECT_TRUE(geographical_location == geographical_location2); -} - -/** - * @brief Test comparing geographical locations for inequality. - */ -TEST_F(TestLocation, compareGeographicalLocation2) -{ - // Set a geographical location for the location. - mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; - mio::geo::GeographicalLocation geographical_location2 = {10.5100470309749, 52.2672785559812}; - // Verify that the set geographical location matches the expected values. - EXPECT_FALSE(geographical_location == geographical_location2); -} - -/** - * @brief Test calculating the distance between two locations - */ -TEST_F(TestLocation, Distance) -{ - auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); - auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); - EXPECT_DOUBLE_EQ(bonn.distance(berlin), berlin.distance(bonn)); - auto distance = 478.2; - EXPECT_LT(abs(bonn.distance(berlin) - distance), 0.1); -} - /** * @brief Test whether the contact rates of a location are reduced if the total contacts in that location exceed the maximum number of contacts. */ From c5edf45196e79fe9633d79be81de617e2545db82 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:05:35 +0200 Subject: [PATCH 039/169] CHG: use ifndefs --- cpp/memilio/geography/locations.h | 6 +++++- cpp/memilio/geography/tree.h | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h index 8e69366b31..8f7774d1a5 100644 --- a/cpp/memilio/geography/locations.h +++ b/cpp/memilio/geography/locations.h @@ -17,6 +17,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#ifndef LOCATIONS_H +#define LOCATIONS_H #include "memilio/io/default_serialize.h" #include @@ -114,4 +116,6 @@ class GeographicalLocation } // namespace geo -} // namespace mio \ No newline at end of file +} // namespace mio + +#endif // LOCATIONS_H diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 619abd9b85..7367209cfb 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -17,6 +17,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#ifndef TREE_H +#define TREE_H #include "math.h" #include "locations.h" @@ -221,3 +223,5 @@ class RTree } // namespace geo } // namespace mio + +#endif // TREE_H From 3ce4001b52cc8088e57f6769126abf8ab77db462 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:30:07 +0200 Subject: [PATCH 040/169] CHG: try fix CI as CI compiler does not support our c++ language standard --- cpp/memilio/geography/tree.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 7367209cfb..c1262ae380 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -124,7 +124,7 @@ class RTree { Point point(location.get_longitude(), location.get_latitude()); std::vector indices; - bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element>(indices)); + bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element{indices}); return indices; } @@ -140,7 +140,7 @@ class RTree auto radius_in_meter = 1000 * radius; auto circle = create_circle_approximation(location, radius_in_meter); std::vector indices; - bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element>(indices)); + bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element{indices}); return indices; } From 5a213de24465d20079e5110aa0f39be9bf3742ec Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:06:19 +0200 Subject: [PATCH 041/169] CHG: Try fix CI --- cpp/memilio/geography/tree.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index c1262ae380..2a0fce7231 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -124,7 +124,7 @@ class RTree { Point point(location.get_longitude(), location.get_latitude()); std::vector indices; - bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element{indices}); + bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element>{indices}); return indices; } @@ -140,7 +140,7 @@ class RTree auto radius_in_meter = 1000 * radius; auto circle = create_circle_approximation(location, radius_in_meter); std::vector indices; - bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element{indices}); + bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element>{indices}); return indices; } From 91b066d8fd8914a1ad4702a10188811b4b61b35c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:30:07 +0200 Subject: [PATCH 042/169] CHG: switch names of Location and geography tests --- cpp/tests/test_abm_location.cpp | 18 +++++++++--------- cpp/tests/test_geography.cpp | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index 44bc755f1f..55d4c0fad6 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -26,12 +26,12 @@ #include "memilio/utils/parameter_distributions.h" #include "random_number_test.h" -using TestGeography = RandomNumberTest; +using TestLocation = RandomNumberTest; /** * @brief Test that initializing a location with cells correctly creates the given number of cells. */ -TEST_F(TestGeography, initCell) +TEST_F(TestLocation, initCell) { // Create a location of type PublicTransport with 2 cells. mio::abm::Location location(mio::abm::LocationType::PublicTransport, 0, 6, 0, 2); @@ -42,7 +42,7 @@ TEST_F(TestGeography, initCell) /** * @brief Test that a location correctly returns its ID. */ -TEST_F(TestGeography, getId) +TEST_F(TestLocation, getId) { // Create a location of type Home with an ID of 0. mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); @@ -53,7 +53,7 @@ TEST_F(TestGeography, getId) /** * @brief Test that the computation of space per person relative to capacity works correctly. */ -TEST_F(TestGeography, computeSpacePerPersonRelative) +TEST_F(TestLocation, computeSpacePerPersonRelative) { using testing::Return; @@ -74,7 +74,7 @@ TEST_F(TestGeography, computeSpacePerPersonRelative) /** * @brief Test the interaction between infected and susceptible persons at a location. */ -TEST_F(TestGeography, interact) +TEST_F(TestLocation, interact) { using testing::Return; @@ -134,7 +134,7 @@ TEST_F(TestGeography, interact) /** * @brief Test setting and getting the capacity of a location. */ -TEST_F(TestGeography, setCapacity) +TEST_F(TestLocation, setCapacity) { // Create a location of type Home. mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); @@ -147,7 +147,7 @@ TEST_F(TestGeography, setCapacity) /** * @brief Test setting and getting the required mask type at a location. */ -TEST_F(TestGeography, setRequiredMask) +TEST_F(TestLocation, setRequiredMask) { // Create a location of type Home. mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); @@ -162,7 +162,7 @@ TEST_F(TestGeography, setRequiredMask) /** * @brief Test setting and getting the geographical location of a location. */ -TEST_F(TestGeography, getGeographicalLocation) +TEST_F(TestLocation, getGeographicalLocation) { // Create a location of type Home. auto location = mio::abm::Location(mio::abm::LocationType::Home, 0); @@ -176,7 +176,7 @@ TEST_F(TestGeography, getGeographicalLocation) /** * @brief Test whether the contact rates of a location are reduced if the total contacts in that location exceed the maximum number of contacts. */ -TEST_F(TestGeography, adjustContactRates) +TEST_F(TestLocation, adjustContactRates) { mio::abm::Location loc(mio::abm::LocationType::SocialEvent, mio::abm::LocationId(0)); //Set the maximum contacts smaller than the contact rates diff --git a/cpp/tests/test_geography.cpp b/cpp/tests/test_geography.cpp index b83eeded3d..5df3c06bbc 100644 --- a/cpp/tests/test_geography.cpp +++ b/cpp/tests/test_geography.cpp @@ -21,12 +21,12 @@ #include "memilio/geography/tree.h" #include "random_number_test.h" -using TestLocation = RandomNumberTest; +using TestGeography = RandomNumberTest; /** * @brief Test comparing geographical locations for equality. */ -TEST_F(TestLocation, compareGeographicalLocation) +TEST_F(TestGeography, compareGeographicalLocation) { // Set a geographical location for the location. mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; @@ -38,7 +38,7 @@ TEST_F(TestLocation, compareGeographicalLocation) /** * @brief Test comparing geographical locations for inequality. */ -TEST_F(TestLocation, compareGeographicalLocation2) +TEST_F(TestGeography, compareGeographicalLocation2) { // Set a geographical location for the location. mio::geo::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; @@ -50,7 +50,7 @@ TEST_F(TestLocation, compareGeographicalLocation2) /** * @brief Test calculating the distance between two locations */ -TEST_F(TestLocation, Distance) +TEST_F(TestGeography, Distance) { auto bonn = mio::geo::GeographicalLocation(50.7333, 7.1000); auto berlin = mio::geo::GeographicalLocation(52.5200, 13.4050); @@ -62,7 +62,7 @@ TEST_F(TestLocation, Distance) /** * @brief Test the default r-Tree Constructor */ -TEST_F(TestLocation, rtreeConstructionNoData) +TEST_F(TestGeography, rtreeConstructionNoData) { EXPECT_NO_THROW(mio::geo::RTree()); } @@ -70,7 +70,7 @@ TEST_F(TestLocation, rtreeConstructionNoData) /** * @brief Test the r-Tree Constructor given a vector */ -TEST_F(TestLocation, rtreeConstructionVector) +TEST_F(TestGeography, rtreeConstructionVector) { std::vector locations; locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); @@ -82,7 +82,7 @@ TEST_F(TestLocation, rtreeConstructionVector) /** * @brief Test the r-Tree Constructor given a range */ -TEST_F(TestLocation, rtreeConstructionRange) +TEST_F(TestGeography, rtreeConstructionRange) { std::vector locations; locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); @@ -94,7 +94,7 @@ TEST_F(TestLocation, rtreeConstructionRange) /** * @brief Test the size function of an r-Tree */ -TEST_F(TestLocation, rtreesize) +TEST_F(TestGeography, rtreesize) { std::vector locations; locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); @@ -107,7 +107,7 @@ TEST_F(TestLocation, rtreesize) /** * @brief Test the nearest neighbours query of an r-Tree */ -TEST_F(TestLocation, rtreeNN) +TEST_F(TestGeography, rtreeNN) { std::vector locations; locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); @@ -120,7 +120,7 @@ TEST_F(TestLocation, rtreeNN) /** * @brief Test the in-range query of an r-Tree */ -TEST_F(TestLocation, rtreeinrange_approx) +TEST_F(TestGeography, rtreeinrange_approx) { std::vector locations; locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); @@ -133,7 +133,7 @@ TEST_F(TestLocation, rtreeinrange_approx) /** * @brief Test the exact in-range query of an r-Tree */ -TEST_F(TestLocation, rtreeinrange) +TEST_F(TestGeography, rtreeinrange) { std::vector locations; locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); From 07aeafeff0650bf63c76bc2949d640e73412ff9c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:01:15 +0200 Subject: [PATCH 043/169] CHG: update inlcludes and use numbers constants --- cpp/memilio/geography/locations.h | 7 +++---- cpp/memilio/geography/tree.h | 8 ++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h index 8f7774d1a5..d395ac39ca 100644 --- a/cpp/memilio/geography/locations.h +++ b/cpp/memilio/geography/locations.h @@ -21,17 +21,16 @@ #define LOCATIONS_H #include "memilio/io/default_serialize.h" -#include -#include #include -#include "memilio/utils/logging.h" +#include +#include namespace mio { namespace geo { const double earth_radius = 6371; -const double radians = M_PI / 180.0; +const double radians = std::numbers::pi / 180.0; class GeographicalLocation { diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 2a0fce7231..7ea2624236 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -20,16 +20,12 @@ #ifndef TREE_H #define TREE_H -#include "math.h" -#include "locations.h" #include #include #include #include -#include - -#include "boost/geometry/geometry.hpp" - +#include +#include #include #include #include From 298e62e85dee40c9c92199f97b8214524f77874b Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:17:05 +0200 Subject: [PATCH 044/169] CHG: Update includes --- cpp/memilio/geography/tree.h | 1 - cpp/tests/test_geography.cpp | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 7ea2624236..905618fcd8 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -159,7 +159,6 @@ class RTree auto midpoint = Point(location.get_longitude(), location.get_latitude()); std::vector indices; for (auto& node : nodes) { - mio::log_info("Distance: {}", bg::distance(midpoint, node.first)); if (bg::distance(midpoint, node.first) < radius_in_meter) { indices.push_back(node.second); } diff --git a/cpp/tests/test_geography.cpp b/cpp/tests/test_geography.cpp index 5df3c06bbc..a49ef11407 100644 --- a/cpp/tests/test_geography.cpp +++ b/cpp/tests/test_geography.cpp @@ -19,6 +19,7 @@ */ #include "memilio/geography/tree.h" +#include "memilio/geography/locations.h" #include "random_number_test.h" using TestGeography = RandomNumberTest; From 38e6d4cd1ba80f008c53a1547142703adc89e66e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:56:53 +0200 Subject: [PATCH 045/169] CHG: explicitly denote pi for memilio.simulation to work --- cpp/memilio/geography/locations.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h index d395ac39ca..8cbfcd6b62 100644 --- a/cpp/memilio/geography/locations.h +++ b/cpp/memilio/geography/locations.h @@ -23,14 +23,14 @@ #include "memilio/io/default_serialize.h" #include #include -#include namespace mio { namespace geo { +const double pi = 3.141592653589793238462643383279502884; const double earth_radius = 6371; -const double radians = std::numbers::pi / 180.0; +const double radians = pi / 180.0; class GeographicalLocation { From bac5307f6028ff3b6182699007cce19c9d93f70a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:57:26 +0200 Subject: [PATCH 046/169] CHG: Make tree available in Timed Graph --- cpp/examples/asymmetric_graph.cpp | 36 ++++++++++++++++--- cpp/memilio/geography/tree.h | 25 +++++++++++++ .../metapopulation_mobility_asymmetric.h | 9 +++-- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 55d82b62c4..1e4629429b 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -17,12 +17,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "memilio/geography/locations.h" +#include "memilio/geography/tree.h" #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" #include "memilio/utils/logging.h" +#include "memilio/utils/parameter_distributions.h" +#include "memilio/utils/random_number_generator.h" #include "smm/simulation.h" #include "smm/parameters.h" +#include enum class InfectionState { @@ -59,21 +64,25 @@ int main(int /*argc*/, char** /*argv*/) model.parameters.get>() = adoption_rates; auto model2 = model; + auto rng = mio::RandomNumberGenerator(); mio::Graph>, mio::MobilityEdgeDirected> graph; - graph.add_node(0, 12.0, 21.0, model, t0); - graph.add_node(1, 12.0, 21.0, model2, t0); - size_t num_nodes = 999; + graph.add_node(0, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), + mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model, t0); + graph.add_node(1, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), + mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); + size_t num_nodes = 200000; for (size_t i = 2; i < num_nodes; i++) { auto local_model = model; - graph.add_node(i, 12.0, 21.0, local_model, t0); + graph.add_node(i, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), + mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), local_model, t0); } auto param = mio::MobilityParametersTimed(2.0, 10, 1); graph.add_edge(0, 1); - auto rng = mio::RandomNumberGenerator(); auto distribution = mio::DiscreteDistributionInPlace(); + std::vector uniform_vector(num_nodes, 1.0); mio::log_info("Nodes generated"); for (size_t i = 0; i < 3 * num_nodes; ++i) { @@ -84,6 +93,23 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("Graph generated"); + auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + // mio::log_info("Value: {}", nodes.begin()->get_latitude()); + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + mio::log_info("RTree generated"); + + for (auto& node : graph.nodes()) { + std::vector> neighbors; + neighbors.push_back(tree.inrange_indices_approximate(node.property.get_location(), 3)); + neighbors.push_back(tree.inrange_indices_approximate(node.property.get_location(), 5)); + neighbors.push_back(tree.inrange_indices_approximate(node.property.get_location(), 10)); + node.property.set_regional_neighbors(neighbors); + } + + mio::log_info("Neighbors set"); + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); for (size_t i = 0; i < 3 * num_nodes; i++) { diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 905618fcd8..d5aa46d14f 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -49,6 +49,12 @@ concept IsSphericalLocation = requires(const Location& loc) { template concept IsSphericalLocationIterator = std::input_iterator && IsSphericalLocation())>; +template +concept IsSphericalLocationIt = std::indirectly_readable && requires(const Iter& loc) { + std::is_floating_point_vget_latitude())> && + std::is_floating_point_vget_longitude())>; +}; + /** * @brief R-Tree for spatial queries of geographical locations on the sphere * @@ -99,6 +105,25 @@ class RTree ++index; } } + /** + * @brief Construct a new RTree object with data given in a range + * + * @param first The beginning of the range + * @param last The end of the range + * The provided location data needs to provide get_latitude() and get_longitude(). + */ + template + RTree(Iter first, Iter last) + : rtree{} + { + size_t index = 0; + while (first != last) { + Point point((*first)->get_longitude(), (*first)->get_latitude()); + rtree.insert(Node(point, index)); + ++first; + ++index; + } + } /** * @brief Return the number of data points stored in the RTree * diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 055f02ba0e..7b75c2b96f 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -60,12 +60,17 @@ class LocationNode : public SimulationNode double get_longitude() const { - return m_location.longitude; + return m_location.get_longitude(); } double get_latitude() const { - return m_location.latitude; + return m_location.get_latitude(); + } + + void set_regional_neighbors(const std::vector>& neighbors) + { + regional_neighbor_indices = neighbors; } private: From 51ab550dbe3a019077e021c440dab16dbec2f6eb Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:26:28 +0200 Subject: [PATCH 047/169] CHG: functional smm example with agegroups, rest is broken --- cpp/examples/smm.cpp | 51 +++++++++++++++--------- cpp/memilio/epidemiology/adoption_rate.h | 8 ++-- cpp/models/smm/model.h | 33 ++++++++------- cpp/models/smm/parameters.h | 16 ++++---- cpp/models/smm/simulation.h | 34 +++++++++------- 5 files changed, 84 insertions(+), 58 deletions(-) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index 1e7f5d285e..5ee794e6d5 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -18,6 +18,7 @@ * limitations under the License. */ +#include "memilio/epidemiology/age_group.h" #include "smm/simulation.h" #include "smm/model.h" #include "smm/parameters.h" @@ -36,41 +37,53 @@ enum class InfectionState }; +enum class AgeGroup +{ + All, + Count +}; + int main() { //Example how to run the stochastic metapopulation models with four regions const size_t num_regions = 4; - using Model = mio::smm::Model; + using Model = mio::smm::Model; double numE = 12, numC = 4, numI = 12, numR = 0, numD = 0; Model model; //Population are distributed uniformly to the four regions for (size_t r = 0; r < num_regions; ++r) { - model.populations[{mio::regions::Region(r), InfectionState::S}] = + model.populations[{mio::regions::Region(r), InfectionState::S, AgeGroup::All}] = (1000 - numE - numC - numI - numR - numD) / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::E}] = numE / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::C}] = numC / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::I}] = numI / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::R}] = numR / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::D}] = numD / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::E, AgeGroup::All}] = numE / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::C, AgeGroup::All}] = numC / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::I, AgeGroup::All}] = numI / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::R, AgeGroup::All}] = numR / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::D, AgeGroup::All}] = numD / num_regions; } //Set infection state adoption and spatial transition rates - std::vector> adoption_rates; - std::vector> transition_rates; + std::vector> adoption_rates; + std::vector> transition_rates; for (size_t r = 0; r < num_regions; ++r) { adoption_rates.push_back({InfectionState::S, InfectionState::E, + AgeGroup::All, mio::regions::Region(r), 0.1, - {{InfectionState::C, 1}, {InfectionState::I, 0.5}}}); - adoption_rates.push_back({InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}}); - adoption_rates.push_back({InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}}); - adoption_rates.push_back({InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}}); - adoption_rates.push_back({InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}}); - adoption_rates.push_back({InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}}); + {{InfectionState::C, AgeGroup::All, 1}, {InfectionState::I, AgeGroup::All, 0.5}}}); + adoption_rates.push_back( + {InfectionState::E, InfectionState::C, AgeGroup::All, mio::regions::Region(r), 1.0 / 5., {}}); + adoption_rates.push_back( + {InfectionState::C, InfectionState::R, AgeGroup::All, mio::regions::Region(r), 0.2 / 3., {}}); + adoption_rates.push_back( + {InfectionState::C, InfectionState::I, AgeGroup::All, mio::regions::Region(r), 0.8 / 3., {}}); + adoption_rates.push_back( + {InfectionState::I, InfectionState::R, AgeGroup::All, mio::regions::Region(r), 0.99 / 5., {}}); + adoption_rates.push_back( + {InfectionState::I, InfectionState::D, AgeGroup::All, mio::regions::Region(r), 0.01 / 5., {}}); } //Agents in infection state D do not transition @@ -79,15 +92,15 @@ int main() for (size_t j = 0; j < num_regions; ++j) if (i != j) { transition_rates.push_back( - {InfectionState(s), mio::regions::Region(i), mio::regions::Region(j), 0.01}); + {InfectionState(s), AgeGroup::All, mio::regions::Region(i), mio::regions::Region(j), 0.01}); transition_rates.push_back( - {InfectionState(s), mio::regions::Region(j), mio::regions::Region(i), 0.01}); + {InfectionState(s), AgeGroup::All, mio::regions::Region(j), mio::regions::Region(i), 0.01}); } } } - model.parameters.get>() = adoption_rates; - model.parameters.get>() = transition_rates; + model.parameters.get>() = adoption_rates; + model.parameters.get>() = transition_rates; double dt = 0.1; double tmax = 30.; diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index a56dfcdc54..43fc44aedf 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -32,9 +32,10 @@ namespace mio * The population having "status" is multiplied with "factor." * @tparam Status An infection state enum. */ -template +template struct Influence { Status status; + AgeGroup age_group; ScalarType factor; }; @@ -45,13 +46,14 @@ struct Influence { * the sum over all "influences", which scale their "status" with the respective "factor". * @tparam Status An infection state enum. */ -template +template struct AdoptionRate { Status from; // i Status to; // j + AgeGroup age_group; mio::regions::Region region; // k ScalarType factor; // gammahat_{ij}^k - std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) + std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) }; } // namespace mio diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 4066667455..4c41732fc8 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -37,17 +37,18 @@ namespace smm * @tparam regions Number of regions. * @tparam Status An infection state enum. */ -template -class Model - : public mio::CompartmentalModel, - ParametersBase> +template +class Model : public mio::CompartmentalModel, + ParametersBase> { - using Base = mio::CompartmentalModel, - ParametersBase>; + using Base = mio::CompartmentalModel, + ParametersBase>; public: Model() - : Base(typename Base::Populations({static_cast(regions), Status::Count}, 0.), + : Base(typename Base::Populations({static_cast(regions), Status::Count, AgeGroup::Count}), typename Base::ParameterSet()) { } @@ -58,10 +59,11 @@ class Model * @param[in] x The current state of the model. * @return Current value of the adoption rate. */ - ScalarType evaluate(const AdoptionRate& rate, const Eigen::VectorXd& x) const + ScalarType evaluate(const AdoptionRate& rate, const Eigen::VectorXd& x) const { - const auto& pop = this->populations; - const auto source = pop.get_flat_index({rate.region, rate.from}); + const auto& pop = this->populations; + const auto source = + pop.get_flat_index({rate.region, rate.from, rate.age_group}); // Why is here rate.from used? KV // determine order and calculate rate if (rate.influences.size() == 0) { // first order adoption return rate.factor * x[source]; @@ -69,13 +71,16 @@ class Model else { // second order adoption ScalarType N = 0; for (size_t s = 0; s < static_cast(Status::Count); ++s) { - N += x[pop.get_flat_index({rate.region, Status(s)})]; + for (size_t age = 0; age < static_cast(AgeGroup::Count); ++age) { + N += x[pop.get_flat_index({rate.region, Status(s), AgeGroup(age)})]; + } } // accumulate influences ScalarType influences = 0.0; for (size_t i = 0; i < rate.influences.size(); i++) { influences += - rate.influences[i].factor * x[pop.get_flat_index({rate.region, rate.influences[i].status})]; + rate.influences[i].factor * + x[pop.get_flat_index({rate.region, rate.influences[i].status, rate.influences[i].age_group})]; } return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; } @@ -87,9 +92,9 @@ class Model * @param[in] x The current state of the model. * @return Current value of the transition rate. */ - ScalarType evaluate(const TransitionRate& rate, const Eigen::VectorXd& x) const + ScalarType evaluate(const TransitionRate& rate, const Eigen::VectorXd& x) const { - const auto source = this->populations.get_flat_index({rate.from, rate.status}); + const auto source = this->populations.get_flat_index({rate.from, rate.status, rate.age_group}); return rate.factor * x[source]; } diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index 70b8212291..cc6e734851 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -35,9 +35,9 @@ namespace smm * @brief A vector of AdoptionRate%s, see mio::AdoptionRate * @tparam Status An infection state enum. */ -template +template struct AdoptionRates { - using Type = std::vector>; + using Type = std::vector>; const static std::string name() { return "AdoptionRates"; @@ -48,25 +48,25 @@ struct AdoptionRates { * @brief Struct defining a possible regional transition in a Model based on Poisson Processes. * @tparam Status An infection state enum. */ -template +template struct TransitionRate { Status status; // i + AgeGroup age_group; mio::regions::Region from; // k mio::regions::Region to; // l ScalarType factor; // lambda_i^{kl} }; - -template +template struct TransitionRates { - using Type = std::vector>; + using Type = std::vector>; const static std::string name() { return "TransitionRates"; } }; -template -using ParametersBase = mio::ParameterSet, TransitionRates>; +template +using ParametersBase = mio::ParameterSet, TransitionRates>; } // namespace smm diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index b63e77fc99..4cf13abff5 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -37,12 +37,12 @@ namespace smm * @tparam regions The number of regions. * @tparam Status An infection state enum. */ -template +template class Simulation { public: public: - using Model = smm::Model; + using Model = smm::Model; /** * @brief Set up the simulation for a Stochastic Metapopulation Model. @@ -100,18 +100,24 @@ class Simulation if (next_event < adoption_rates().size()) { // perform adoption event const auto& rate = adoption_rates()[next_event]; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from})] -= 1; - m_model->populations[{rate.region, rate.from}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to})] += 1; - m_model->populations[{rate.region, rate.to}] += 1; + m_result + .get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from, rate.age_group})] -= + 1; + m_model->populations[{rate.region, rate.from, rate.age_group}] -= 1; + m_result + .get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to, rate.age_group})] += 1; + m_model->populations[{rate.region, rate.to, rate.age_group}] += 1; } else { // perform transition event const auto& rate = transition_rates()[next_event - adoption_rates().size()]; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status})] -= 1; - m_model->populations[{rate.from, rate.status}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status})] += 1; - m_model->populations[{rate.to, rate.status}] += 1; + m_result + .get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status, rate.age_group})] -= + 1; + m_model->populations[{rate.from, rate.status, rate.age_group}] -= 1; + m_result + .get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status, rate.age_group})] += 1; + m_model->populations[{rate.to, rate.status, rate.age_group}] += 1; } // update internal times for (size_t i = 0; i < m_internal_time.size(); i++) { @@ -161,17 +167,17 @@ class Simulation /** * @brief Returns the model's transition rates. */ - inline constexpr const typename smm::TransitionRates::Type& transition_rates() + inline constexpr const typename smm::TransitionRates::Type& transition_rates() { - return m_model->parameters.template get>(); + return m_model->parameters.template get>(); } /** * @brief Returns the model's adoption rates. */ - inline constexpr const typename smm::AdoptionRates::Type& adoption_rates() + inline constexpr const typename smm::AdoptionRates::Type& adoption_rates() { - return m_model->parameters.template get>(); + return m_model->parameters.template get>(); } /** From 6ae71d53edb05867da6e288522ef2d156608f700 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:10:11 +0200 Subject: [PATCH 048/169] CHG: Speed up multiple neighbour queries --- cpp/examples/asymmetric_graph.cpp | 7 ++----- cpp/memilio/geography/tree.h | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 1e4629429b..f282ca096e 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -101,11 +101,8 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { - std::vector> neighbors; - neighbors.push_back(tree.inrange_indices_approximate(node.property.get_location(), 3)); - neighbors.push_back(tree.inrange_indices_approximate(node.property.get_location(), 5)); - neighbors.push_back(tree.inrange_indices_approximate(node.property.get_location(), 10)); - node.property.set_regional_neighbors(neighbors); + node.property.set_regional_neighbors( + tree.inrange_indices_query(node.property.get_location(), {3.0, 5.0, 10.0})); } mio::log_info("Neighbors set"); diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index d5aa46d14f..27de9a4579 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -165,6 +166,34 @@ class RTree return indices; } + /** + * @brief Return the indices of the points within a given radius for multiple radii at the same time + * + * @param location Midpoint for the query, provides get_latitude() and get_longitude() + * @param radii Vector containing the radii of the query + * @return Vector of vectors with indices of the points found + */ + auto inrange_indices_query(const IsSphericalLocation auto& location, std::vector radii) const + { + auto max_radius = std::max_element(radii.begin(), radii.end()); + auto radius_in_meter = 1000 * (*max_radius); + auto circle = create_circle_approximation(location, radius_in_meter); + std::vector nodes; + bgi::query(rtree, bgi::covered_by(circle), std::back_inserter(nodes)); + auto midpoint = Point(location.get_longitude(), location.get_latitude()); + std::vector> result; + for (const auto& radius : radii) { + std::vector indices; + for (auto& node : nodes) { + if (bg::distance(midpoint, node.first) < radius) { + indices.push_back(node.second); + } + } + result.push_back(indices); + } + return result; + } + /** * @brief Return the indices of the points within a given radius * From 3b9d953a7632a9503fd61c7e8280be53c1f97eb7 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:58:08 +0200 Subject: [PATCH 049/169] FIX: inrange query km conversion, added test --- cpp/memilio/geography/tree.h | 3 ++- cpp/tests/test_geography.cpp | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 27de9a4579..3166bdb6bd 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -183,9 +183,10 @@ class RTree auto midpoint = Point(location.get_longitude(), location.get_latitude()); std::vector> result; for (const auto& radius : radii) { + radius_in_meter = 1000 * radius; std::vector indices; for (auto& node : nodes) { - if (bg::distance(midpoint, node.first) < radius) { + if (bg::distance(midpoint, node.first) < radius_in_meter) { indices.push_back(node.second); } } diff --git a/cpp/tests/test_geography.cpp b/cpp/tests/test_geography.cpp index a49ef11407..c2b731bceb 100644 --- a/cpp/tests/test_geography.cpp +++ b/cpp/tests/test_geography.cpp @@ -142,4 +142,20 @@ TEST_F(TestGeography, rtreeinrange) locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); auto rtree = mio::geo::RTree(locations.begin(), locations.end()); EXPECT_EQ(rtree.inrange_indices(mio::geo::GeographicalLocation(50.933501, 6.875124), 150).size(), 2); -} \ No newline at end of file +} + +/** + * @brief Test the in-range query of an r-Tree with multiple radii + */ +TEST_F(TestGeography, rtreeinrange_multiple_radii) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + auto rtree = mio::geo::RTree(locations.begin(), locations.end()); + auto result = rtree.inrange_indices_query(mio::geo::GeographicalLocation(51.492599, 7.451810), {130, 310, 80}); + EXPECT_EQ(result[0].size(), 2); + EXPECT_EQ(result[1].size(), 3); + EXPECT_EQ(result[2].size(), 1); +} From 9de25843dd1ac2d5c4b912e66fc2a6c5e425dc0a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:00:05 +0200 Subject: [PATCH 050/169] CHG: add inrange query --- cpp/memilio/geography/tree.h | 29 +++++++++++++++++++++++++++++ cpp/tests/test_geography.cpp | 18 +++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h index 905618fcd8..e0fd847850 100644 --- a/cpp/memilio/geography/tree.h +++ b/cpp/memilio/geography/tree.h @@ -140,6 +140,35 @@ class RTree return indices; } + /** + * @brief Return the indices of the points within a given radius for multiple radii at the same time + * + * @param location Midpoint for the query, provides get_latitude() and get_longitude() + * @param radii Vector containing the radii of the query + * @return Vector of vectors with indices of the points found + */ + auto inrange_indices_query(const IsSphericalLocation auto& location, std::vector radii) const + { + auto max_radius = std::max_element(radii.begin(), radii.end()); + auto radius_in_meter = 1000 * (*max_radius); + auto circle = create_circle_approximation(location, radius_in_meter); + std::vector nodes; + bgi::query(rtree, bgi::covered_by(circle), std::back_inserter(nodes)); + auto midpoint = Point(location.get_longitude(), location.get_latitude()); + std::vector> result; + for (const auto& radius : radii) { + radius_in_meter = 1000 * radius; + std::vector indices; + for (auto& node : nodes) { + if (bg::distance(midpoint, node.first) < radius_in_meter) { + indices.push_back(node.second); + } + } + result.push_back(indices); + } + return result; + } + /** * @brief Return the indices of the points within a given radius * diff --git a/cpp/tests/test_geography.cpp b/cpp/tests/test_geography.cpp index a49ef11407..c2b731bceb 100644 --- a/cpp/tests/test_geography.cpp +++ b/cpp/tests/test_geography.cpp @@ -142,4 +142,20 @@ TEST_F(TestGeography, rtreeinrange) locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); auto rtree = mio::geo::RTree(locations.begin(), locations.end()); EXPECT_EQ(rtree.inrange_indices(mio::geo::GeographicalLocation(50.933501, 6.875124), 150).size(), 2); -} \ No newline at end of file +} + +/** + * @brief Test the in-range query of an r-Tree with multiple radii + */ +TEST_F(TestGeography, rtreeinrange_multiple_radii) +{ + std::vector locations; + locations.push_back(mio::geo::GeographicalLocation(50.783, 6.083)); + locations.push_back(mio::geo::GeographicalLocation(52.083, 7.017)); + locations.push_back(mio::geo::GeographicalLocation(53.667, 10.233)); + auto rtree = mio::geo::RTree(locations.begin(), locations.end()); + auto result = rtree.inrange_indices_query(mio::geo::GeographicalLocation(51.492599, 7.451810), {130, 310, 80}); + EXPECT_EQ(result[0].size(), 2); + EXPECT_EQ(result[1].size(), 3); + EXPECT_EQ(result[2].size(), 1); +} From ab531a54cfa0434b52249985d390b6159adf4681 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:31:36 +0200 Subject: [PATCH 051/169] CHG: log exchanges of specified classes on the edges --- cpp/examples/asymmetric_graph.cpp | 22 +++-- .../metapopulation_mobility_asymmetric.h | 95 +++++++++++++------ 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index f282ca096e..cb8dfccf77 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -63,23 +63,29 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.5}}}); model.parameters.get>() = adoption_rates; - auto model2 = model; - auto rng = mio::RandomNumberGenerator(); + auto model2 = model; + model2.populations[{home, InfectionState::I}] = 9000; + model2.populations[{home, InfectionState::S}] = 800; + + auto rng = mio::RandomNumberGenerator(); mio::Graph>, mio::MobilityEdgeDirected> graph; graph.add_node(0, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), - mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model, t0); + mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); graph.add_node(1, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); - size_t num_nodes = 200000; + size_t num_nodes = 2000; for (size_t i = 2; i < num_nodes; i++) { auto local_model = model; graph.add_node(i, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), local_model, t0); } + std::vector> interesting_indices; + interesting_indices.push_back({model.populations.get_flat_index({home, InfectionState::I})}); + auto param = mio::MobilityParametersTimed(2.0, 10, 1); - graph.add_edge(0, 1); + graph.add_edge(0, 1, interesting_indices); auto distribution = mio::DiscreteDistributionInPlace(); @@ -88,7 +94,7 @@ int main(int /*argc*/, char** /*argv*/) for (size_t i = 0; i < 3 * num_nodes; ++i) { auto to = distribution(rng, {uniform_vector}); auto from = distribution(rng, {uniform_vector}); - graph.add_edge(from, to); + graph.add_edge(from, to, interesting_indices); } mio::log_info("Graph generated"); @@ -131,5 +137,9 @@ int main(int /*argc*/, char** /*argv*/) // std::cout << "Second Table" << std::endl; // sim.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R"}); + auto& edge_1_0 = sim.get_graph().edges()[1]; + auto& results = edge_1_0.property.get_mobility_results(); + results.print_table({"Commuter Sick", "Commuter Total"}); + return 0; } diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 7b75c2b96f..7e7d734499 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -23,13 +23,15 @@ #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/random_number_generator.h" #include "memilio/geography/locations.h" - #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/graph.h" #include "memilio/mobility/metapopulation_mobility_instant.h" +#include #include #include +#include +#include namespace mio { @@ -98,17 +100,37 @@ class MobilityEdgeDirected * create edge with timed movement parameters. * @param params mobility rate for each group and compartment */ - // MobilityEdgeDirected(const MobilityParametersTimed& params) - // : m_parameters(params) - // { - // } - // auto next_event_time() const - // { - // return m_parameters.next_event_time(); - // } + MobilityEdgeDirected(size_t size) + : m_mobility_results(size) + { + } + + MobilityEdgeDirected(size_t size, const std::vector>& saved_compartment_indices) + : m_mobility_results(size) + , m_saved_compartment_indices(saved_compartment_indices) + { + } + + MobilityEdgeDirected(const std::vector>& saved_compartment_indices) + : m_mobility_results(saved_compartment_indices.size() + 1) + , m_saved_compartment_indices(saved_compartment_indices) + { + } - MobilityEdgeDirected() = default; + /** + * @brief Get the count of exchanges in selected compartments, along with the total number of exchanges. + * + * @return A reference to the TimeSeries object representing the mobility results. + */ + TimeSeries& get_mobility_results() + { + return m_mobility_results; + } + const TimeSeries& get_mobility_results() const + { + return m_mobility_results; + } /** * compute mobility from node_from to node_to for a given event @@ -117,48 +139,63 @@ class MobilityEdgeDirected * @param node_to node that people changed to */ template - void apply_mobility(double num_moving, LocationNode& node_from, LocationNode& node_to); + void apply_mobility(double& t, double& num_moving, LocationNode& node_from, LocationNode& node_to); - // private: +private: // MobilityParametersTimed m_parameters; + TimeSeries m_mobility_results; + std::vector> m_saved_compartment_indices; + + void add_mobility_result_time_point(const double t, std::vector& travellers) + { + const size_t save_indices_size = this->m_saved_compartment_indices.size(); + if (save_indices_size > 0) { + + Eigen::VectorXd condensed_values = Eigen::VectorXd::Zero(save_indices_size + 1); + + // sum up the values of m_saved_compartment_indices for each group (e.g. Age groups) + std::transform(this->m_saved_compartment_indices.begin(), this->m_saved_compartment_indices.end(), + condensed_values.data(), [&travellers](const auto& indices) { + return std::accumulate(indices.begin(), indices.end(), 0.0, + [&travellers](double sum, auto i) { + return sum + travellers[i]; + }); + }); + + // the last value is the sum of commuters + condensed_values[save_indices_size] = std::accumulate(travellers.begin(), travellers.end(), 0); + + // Move the condensed values to the m_mobility_results time series + m_mobility_results.add_time_point(t, std::move(condensed_values)); + } + } }; template -void MobilityEdgeDirected::apply_mobility(double num_moving, LocationNode& node_from, LocationNode& node_to) +void MobilityEdgeDirected::apply_mobility(double& t, double& num_moving, LocationNode& node_from, + LocationNode& node_to) { // auto next_event = m_parameters.process_next_event(); // auto num_moving = next_event.number; // auto num_available = boost::numeric::ublas::sum(node_from.get_result().get_last_value()); auto rng = mio::RandomNumberGenerator(); auto distribution = DiscreteDistributionInPlace(); - + std::vector travellers(node_from.get_result().get_last_value().size(), 0); for (int i = 0; i < num_moving; ++i) { auto group = distribution(rng, {node_from.get_result().get_last_value()}); node_from.get_result().get_last_value()[group] -= 1; + travellers[group] += 1; node_to.get_result().get_last_value()[group] += 1; } + add_mobility_result_time_point(t, travellers); } template void apply_timed_mobility(double t, double num_moving, MobilityEdgeDirected& edge, LocationNode& node_from, LocationNode& node_to) { - // if (edge.next_event_time() >= t + dt) { - // return; - // } - mio::unused(t); - edge.apply_mobility(num_moving, node_from, node_to); + edge.apply_mobility(t, num_moving, node_from, node_to); } -// /**get_last_value -// * edge functor for mobility-based simulation. -// * @see MobilityEdgeDirected::apply_mobility -// */ -// template -// void apply_mobility(StochasticEdge& mobilityEdge, size_t event, SimulationNode& node_from, -// SimulationNode& node_to) -// { -// mobilityEdge.apply_mobility(event, node_from, node_to); -// } /** * create a mobility-based simulation. From a871ba6aeb270d31e119840e813dad4f766709f0 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:16:38 +0200 Subject: [PATCH 052/169] first working example --- cpp/examples/abm_minimal.cpp | 117 ++++++++++++++++++- cpp/memilio/compartments/parameter_studies.h | 4 +- cpp/models/abm/model.h | 2 + cpp/models/abm/simulation.h | 11 +- 4 files changed, 124 insertions(+), 10 deletions(-) diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index a78e0dddf7..d46618f5b8 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -17,12 +17,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "abm/simulation.h" +#include "abm/common_abm_loggers.h" +#include + +// shim class to make the abm simulation look like an ode simulation +class ResultSim : public mio::abm::Simulation +{ +public: + using Model = mio::abm::Model; + + ResultSim(const Model& m, double t, double) + : mio::abm::Simulation(mio::abm::TimePoint{mio::abm::days(t).seconds()}, Model(m)) + { + history.log(*this); // set initial results + } + + void advance(double tmax) + { + mio::abm::Simulation::advance(mio::abm::TimePoint{mio::abm::days(tmax).seconds()}, history); + } + + const mio::TimeSeries& get_result() const + { + return get<0>(history.get_log()); + } + + mio::History history{ + Eigen::Index(mio::abm::InfectionState::Count)}; +}; + +// graph ode specific function, overload with noop to avoid compiler errors +template +void calculate_mobility_returns(Eigen::Ref::Vector>, const ResultSim&, + Eigen::Ref::Vector>, double, double) +{ +} +#include "memilio/mobility/metapopulation_mobility_instant.h" + +namespace mio +{ + +// overload for ResultSim without mobility +auto make_mobility_sim(double t0, double dt, Graph, MobilityEdge>&& graph) +{ + using GraphT = GraphSimulation, MobilityEdge>, double, double, + void (*)(double, double, mio::MobilityEdge&, mio::SimulationNode&, + mio::SimulationNode&), + void (*)(double, double, mio::SimulationNode&)>; + return GraphT(t0, dt, std::move(graph), + static_cast&)>(&advance_model), + [](double, double, MobilityEdge&, SimulationNode&, SimulationNode&) {}); +} + +} // namespace mio + #include "abm/household.h" #include "abm/lockdown_rules.h" #include "abm/model.h" -#include "abm/common_abm_loggers.h" +#include "abm/time.h" +#include "memilio/compartments/parameter_studies.h" +#include "memilio/data/analyze_result.h" +#include "memilio/io/result_io.h" #include "memilio/utils/abstract_parameter_distribution.h" +#include "memilio/utils/time_series.h" +#include #include int main() @@ -154,14 +214,65 @@ int main() // Set start and end time for the simulation. auto t0 = mio::abm::TimePoint(0); auto tmax = t0 + mio::abm::days(10); - auto sim = mio::abm::Simulation(t0, std::move(model)); + // auto sim = mio::abm::Simulation(t0, std::move(model)); + const size_t num_runs = 10; // Create a history object to store the time series of the infection states. mio::History historyTimeSeries{ Eigen::Index(mio::abm::InfectionState::Count)}; + mio::ParameterStudy study(model, t0.days(), tmax.days(), num_runs); + + auto ensemble = study.run( + [](auto&& graph) { + // TODO: some actual sampling so that the percentiles show anything + return graph; + }, + [&](auto results_graph, auto&& /* run_idx */) { + auto interpolated_result = mio::interpolate_simulation_result(results_graph); + + auto params = std::vector{}; + params.reserve(results_graph.nodes().size()); + std::transform(results_graph.nodes().begin(), results_graph.nodes().end(), std::back_inserter(params), + [](auto&& node) { + return node.property.get_simulation().get_model(); + }); + return std::make_pair(std::move(interpolated_result), std::move(params)); + }); + if (ensemble.size() > 0) { + auto ensemble_results = std::vector>>{}; + ensemble_results.reserve(ensemble.size()); + auto ensemble_params = std::vector>{}; + ensemble_params.reserve(ensemble.size()); + for (auto&& run : ensemble) { + ensemble_results.emplace_back(std::move(run.first)); + ensemble_params.emplace_back(std::move(run.second)); + } + + auto ensemble_results_p05 = ensemble_percentile(ensemble_results, 0.05); + auto ensemble_results_p25 = ensemble_percentile(ensemble_results, 0.25); + auto ensemble_results_p50 = ensemble_percentile(ensemble_results, 0.50); + auto ensemble_results_p75 = ensemble_percentile(ensemble_results, 0.75); + auto ensemble_results_p95 = ensemble_percentile(ensemble_results, 0.95); + + const boost::filesystem::path save_dir = "/home/schm_r6/Code/memilio/memilio/results/"; + + if (!save_dir.empty()) { + + mio::unused(save_result(ensemble_results_p05, {0}, num_age_groups, + (save_dir / ("Results_" + std::string("p05") + ".h5")).string())); + mio::unused(save_result(ensemble_results_p25, {0}, num_age_groups, + (save_dir / ("Results_" + std::string("p25") + ".h5")).string())); + mio::unused(save_result(ensemble_results_p50, {0}, num_age_groups, + (save_dir / ("Results_" + std::string("p50") + ".h5")).string())); + mio::unused(save_result(ensemble_results_p75, {0}, num_age_groups, + (save_dir / ("Results_" + std::string("p75") + ".h5")).string())); + mio::unused(save_result(ensemble_results_p95, {0}, num_age_groups, + (save_dir / ("Results_" + std::string("p95") + ".h5")).string())); + } + } // Run the simulation until tmax with the history object. - sim.advance(tmax, historyTimeSeries); + // sim.advance(tmax, historyTimeSeries); // The results are written into the file "abm_minimal.txt" as a table with 9 columns. // The first column is Time. The other columns correspond to the number of people with a certain infection state at this Time: diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index 62cfe564a9..c4ae6d0132 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -43,7 +43,7 @@ namespace mio * Can simulate mobility graphs with one simulation in each node or single simulations. * @tparam S type of simulation that runs in one node of the graph. */ -template +template > class ParameterStudy { public: @@ -60,7 +60,7 @@ class ParameterStudy * The Graph type that stores simulations and their results of each run. * This is the output of ParameterStudies for each run. */ - using SimulationGraph = mio::Graph, mio::MobilityEdge>; + using SimulationGraph = mio::Graph, EdgeT>; /** * create study for graph of compartment models. diff --git a/cpp/models/abm/model.h b/cpp/models/abm/model.h index 0c6b320e25..acbda1fa3d 100644 --- a/cpp/models/abm/model.h +++ b/cpp/models/abm/model.h @@ -20,6 +20,7 @@ #ifndef MIO_ABM_MODEL_H #define MIO_ABM_MODEL_H +#include "abm/infection_state.h" #include "abm/model_functions.h" #include "abm/location_type.h" #include "abm/mobility_data.h" @@ -61,6 +62,7 @@ class Model using MobilityRuleType = LocationType (*)(PersonalRandomNumberGenerator&, const Person&, TimePoint, TimeSpan, const Parameters&); + using Compartments = mio::abm::InfectionState; /** * @brief Create a Model. * @param[in] num_agegroups The number of AgeGroup%s in the simulated Model. Must be less than MAX_NUM_AGE_GROUPS. diff --git a/cpp/models/abm/simulation.h b/cpp/models/abm/simulation.h index 2f973cf092..2778e7cea8 100644 --- a/cpp/models/abm/simulation.h +++ b/cpp/models/abm/simulation.h @@ -35,14 +35,15 @@ namespace abm template class Simulation { - public: + using Model = M; + /** * @brief Create a simulation. * @param[in] t0 The starting time of the Simulation. * @param[in] model The Model to simulate. */ - Simulation(TimePoint t0, M&& model) + Simulation(TimePoint t0, Model&& model) : m_model(std::move(model)) , m_t(t0) , m_dt(hours(1)) @@ -87,11 +88,11 @@ class Simulation /** * @brief Get the Model that this Simulation evolves. */ - M& get_model() + Model& get_model() { return m_model; } - const M& get_model() const + const Model& get_model() const { return m_model; } @@ -105,7 +106,7 @@ class Simulation m_t += m_dt; } - M m_model; ///< The Model to simulate. + Model m_model; ///< The Model to simulate. TimePoint m_t; ///< The current TimePoint of the Simulation. TimeSpan m_dt; ///< The length of the time steps. }; From d78bd7c364ac2a0c202ee1c7026877faae97da22 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:54:46 +0200 Subject: [PATCH 053/169] CHG: functional base model with template parameter list (not used yet) --- cpp/examples/CMakeLists.txt | 6 ++-- cpp/memilio/epidemiology/adoption_rate.h | 8 ++--- cpp/models/smm/model.h | 30 +++++++++---------- cpp/models/smm/parameters.h | 15 +++++----- cpp/models/smm/simulation.h | 37 ++++++++++-------------- 5 files changed, 41 insertions(+), 55 deletions(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 980164d324..d97c595ad7 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -148,9 +148,9 @@ add_executable(dabm_example d_abm.cpp) target_link_libraries(dabm_example PRIVATE memilio d_abm) target_compile_options(dabm_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) -add_executable(smm_example smm.cpp) -target_link_libraries(smm_example PRIVATE memilio smm) -target_compile_options(smm_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +# add_executable(smm_example smm.cpp) +# target_link_libraries(smm_example PRIVATE memilio smm) +# target_compile_options(smm_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) add_executable(temporal_hybrid_example temporal_hybrid_dabm_osecir.cpp) target_link_libraries(temporal_hybrid_example PRIVATE memilio hybrid ode_secir d_abm) diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index 43fc44aedf..b05ac2b0a8 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -32,10 +32,9 @@ namespace mio * The population having "status" is multiplied with "factor." * @tparam Status An infection state enum. */ -template +template struct Influence { Status status; - AgeGroup age_group; ScalarType factor; }; @@ -46,14 +45,13 @@ struct Influence { * the sum over all "influences", which scale their "status" with the respective "factor". * @tparam Status An infection state enum. */ -template +template struct AdoptionRate { Status from; // i Status to; // j - AgeGroup age_group; mio::regions::Region region; // k ScalarType factor; // gammahat_{ij}^k - std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) + std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) }; } // namespace mio diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 4c41732fc8..4699fb7e27 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -37,18 +37,18 @@ namespace smm * @tparam regions Number of regions. * @tparam Status An infection state enum. */ -template +template class Model : public mio::CompartmentalModel, - ParametersBase> + mio::Populations, + ParametersBase> { using Base = mio::CompartmentalModel, - ParametersBase>; + mio::Populations, + ParametersBase>; public: Model() - : Base(typename Base::Populations({static_cast(regions), Status::Count, AgeGroup::Count}), + : Base(typename Base::Populations({static_cast(regions), Status::Count}), typename Base::ParameterSet()) { } @@ -59,11 +59,10 @@ class Model : public mio::CompartmentalModel& rate, const Eigen::VectorXd& x) const + ScalarType evaluate(const AdoptionRate& rate, const Eigen::VectorXd& x) const { - const auto& pop = this->populations; - const auto source = - pop.get_flat_index({rate.region, rate.from, rate.age_group}); // Why is here rate.from used? KV + const auto& pop = this->populations; + const auto source = pop.get_flat_index({rate.region, rate.from}); // Why is here rate.from used? KV // determine order and calculate rate if (rate.influences.size() == 0) { // first order adoption return rate.factor * x[source]; @@ -71,16 +70,13 @@ class Model : public mio::CompartmentalModel(Status::Count); ++s) { - for (size_t age = 0; age < static_cast(AgeGroup::Count); ++age) { - N += x[pop.get_flat_index({rate.region, Status(s), AgeGroup(age)})]; - } + N += x[pop.get_flat_index({rate.region, Status(s)})]; } // accumulate influences ScalarType influences = 0.0; for (size_t i = 0; i < rate.influences.size(); i++) { influences += - rate.influences[i].factor * - x[pop.get_flat_index({rate.region, rate.influences[i].status, rate.influences[i].age_group})]; + rate.influences[i].factor * x[pop.get_flat_index({rate.region, rate.influences[i].status})]; } return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; } @@ -92,9 +88,9 @@ class Model : public mio::CompartmentalModel& rate, const Eigen::VectorXd& x) const + ScalarType evaluate(const TransitionRate& rate, const Eigen::VectorXd& x) const { - const auto source = this->populations.get_flat_index({rate.from, rate.status, rate.age_group}); + const auto source = this->populations.get_flat_index({rate.from, rate.status}); return rate.factor * x[source]; } diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index cc6e734851..8a251f63af 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -35,9 +35,9 @@ namespace smm * @brief A vector of AdoptionRate%s, see mio::AdoptionRate * @tparam Status An infection state enum. */ -template +template struct AdoptionRates { - using Type = std::vector>; + using Type = std::vector>; const static std::string name() { return "AdoptionRates"; @@ -48,25 +48,24 @@ struct AdoptionRates { * @brief Struct defining a possible regional transition in a Model based on Poisson Processes. * @tparam Status An infection state enum. */ -template +template struct TransitionRate { Status status; // i - AgeGroup age_group; mio::regions::Region from; // k mio::regions::Region to; // l ScalarType factor; // lambda_i^{kl} }; -template +template struct TransitionRates { - using Type = std::vector>; + using Type = std::vector>; const static std::string name() { return "TransitionRates"; } }; -template -using ParametersBase = mio::ParameterSet, TransitionRates>; +template +using ParametersBase = mio::ParameterSet, TransitionRates>; } // namespace smm diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 4cf13abff5..01afaa1689 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -37,12 +37,11 @@ namespace smm * @tparam regions The number of regions. * @tparam Status An infection state enum. */ -template +template class Simulation { public: -public: - using Model = smm::Model; + using Model = smm::Model; /** * @brief Set up the simulation for a Stochastic Metapopulation Model. @@ -100,24 +99,18 @@ class Simulation if (next_event < adoption_rates().size()) { // perform adoption event const auto& rate = adoption_rates()[next_event]; - m_result - .get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from, rate.age_group})] -= - 1; - m_model->populations[{rate.region, rate.from, rate.age_group}] -= 1; - m_result - .get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to, rate.age_group})] += 1; - m_model->populations[{rate.region, rate.to, rate.age_group}] += 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from})] -= 1; + m_model->populations[{rate.region, rate.from}] -= 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to})] += 1; + m_model->populations[{rate.region, rate.to}] += 1; } else { // perform transition event const auto& rate = transition_rates()[next_event - adoption_rates().size()]; - m_result - .get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status, rate.age_group})] -= - 1; - m_model->populations[{rate.from, rate.status, rate.age_group}] -= 1; - m_result - .get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status, rate.age_group})] += 1; - m_model->populations[{rate.to, rate.status, rate.age_group}] += 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status})] -= 1; + m_model->populations[{rate.from, rate.status}] -= 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status})] += 1; + m_model->populations[{rate.to, rate.status}] += 1; } // update internal times for (size_t i = 0; i < m_internal_time.size(); i++) { @@ -167,17 +160,17 @@ class Simulation /** * @brief Returns the model's transition rates. */ - inline constexpr const typename smm::TransitionRates::Type& transition_rates() + inline constexpr const typename smm::TransitionRates::Type& transition_rates() { - return m_model->parameters.template get>(); + return m_model->parameters.template get>(); } /** * @brief Returns the model's adoption rates. */ - inline constexpr const typename smm::AdoptionRates::Type& adoption_rates() + inline constexpr const typename smm::AdoptionRates::Type& adoption_rates() { - return m_model->parameters.template get>(); + return m_model->parameters.template get>(); } /** @@ -221,7 +214,7 @@ class Simulation m_current_rates; ///< Current values of both types of rates i.e. adoption and transition rates. }; -} //namespace smm +} // namespace smm } // namespace mio #endif From f2babeee69b842cdcfffc7584144adfd0a7a3215 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:58:24 +0200 Subject: [PATCH 054/169] CHG: Another compiling status that is not leading anywhere --- cpp/examples/CMakeLists.txt | 6 +- cpp/examples/smm.cpp | 72 +++++----- cpp/memilio/epidemiology/adoption_rate.h | 2 + cpp/models/smm/model.h | 100 +++++++++---- cpp/models/smm/parameters.h | 4 +- cpp/models/smm/simulation.h | 170 ++++++++++++++++------- 6 files changed, 238 insertions(+), 116 deletions(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index d97c595ad7..980164d324 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -148,9 +148,9 @@ add_executable(dabm_example d_abm.cpp) target_link_libraries(dabm_example PRIVATE memilio d_abm) target_compile_options(dabm_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) -# add_executable(smm_example smm.cpp) -# target_link_libraries(smm_example PRIVATE memilio smm) -# target_compile_options(smm_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +add_executable(smm_example smm.cpp) +target_link_libraries(smm_example PRIVATE memilio smm) +target_compile_options(smm_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) add_executable(temporal_hybrid_example temporal_hybrid_dabm_osecir.cpp) target_link_libraries(temporal_hybrid_example PRIVATE memilio hybrid ode_secir d_abm) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index 5ee794e6d5..ca43ebda00 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -37,53 +37,49 @@ enum class InfectionState }; -enum class AgeGroup -{ - All, - Count -}; - int main() { //Example how to run the stochastic metapopulation models with four regions - const size_t num_regions = 4; - using Model = mio::smm::Model; + const size_t num_regions = 4; + const size_t num_age_groups = 1; + using Model = mio::smm::Model; double numE = 12, numC = 4, numI = 12, numR = 0, numD = 0; Model model; //Population are distributed uniformly to the four regions for (size_t r = 0; r < num_regions; ++r) { - model.populations[{mio::regions::Region(r), InfectionState::S, AgeGroup::All}] = + model.populations[{mio::regions::Region(r), InfectionState::S, mio::AgeGroup(1)}] = (1000 - numE - numC - numI - numR - numD) / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::E, AgeGroup::All}] = numE / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::C, AgeGroup::All}] = numC / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::I, AgeGroup::All}] = numI / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::R, AgeGroup::All}] = numR / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::D, AgeGroup::All}] = numD / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::E, mio::AgeGroup(1)}] = numE / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::C, mio::AgeGroup(1)}] = numC / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::I, mio::AgeGroup(1)}] = numI / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::R, mio::AgeGroup(1)}] = numR / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::D, mio::AgeGroup(1)}] = numD / num_regions; } //Set infection state adoption and spatial transition rates - std::vector> adoption_rates; - std::vector> transition_rates; + std::vector> adoption_rates; + std::vector> transition_rates; for (size_t r = 0; r < num_regions; ++r) { - adoption_rates.push_back({InfectionState::S, - InfectionState::E, - AgeGroup::All, - mio::regions::Region(r), - 0.1, - {{InfectionState::C, AgeGroup::All, 1}, {InfectionState::I, AgeGroup::All, 0.5}}}); adoption_rates.push_back( - {InfectionState::E, InfectionState::C, AgeGroup::All, mio::regions::Region(r), 1.0 / 5., {}}); + {InfectionState::S, + InfectionState::E, + mio::regions::Region(r), + 0.1, + {{InfectionState::C, 1, mio::AgeGroup(1)}, {InfectionState::I, 0.5, mio::AgeGroup(1)}}, + mio::AgeGroup(1)}); + adoption_rates.push_back( + {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, mio::AgeGroup(1)}); adoption_rates.push_back( - {InfectionState::C, InfectionState::R, AgeGroup::All, mio::regions::Region(r), 0.2 / 3., {}}); + {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, mio::AgeGroup(1)}); adoption_rates.push_back( - {InfectionState::C, InfectionState::I, AgeGroup::All, mio::regions::Region(r), 0.8 / 3., {}}); + {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, mio::AgeGroup(1)}); adoption_rates.push_back( - {InfectionState::I, InfectionState::R, AgeGroup::All, mio::regions::Region(r), 0.99 / 5., {}}); + {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, mio::AgeGroup(1)}); adoption_rates.push_back( - {InfectionState::I, InfectionState::D, AgeGroup::All, mio::regions::Region(r), 0.01 / 5., {}}); + {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, mio::AgeGroup(1)}); } //Agents in infection state D do not transition @@ -91,16 +87,26 @@ int main() for (size_t i = 0; i < num_regions; ++i) { for (size_t j = 0; j < num_regions; ++j) if (i != j) { - transition_rates.push_back( - {InfectionState(s), AgeGroup::All, mio::regions::Region(i), mio::regions::Region(j), 0.01}); - transition_rates.push_back( - {InfectionState(s), AgeGroup::All, mio::regions::Region(j), mio::regions::Region(i), 0.01}); + transition_rates.push_back({ + InfectionState(s), + mio::regions::Region(i), + mio::regions::Region(j), + 0.01, + mio::AgeGroup(1), + }); + transition_rates.push_back({ + InfectionState(s), + mio::regions::Region(j), + mio::regions::Region(i), + 0.01, + mio::AgeGroup(1), + }); } } } - model.parameters.get>() = adoption_rates; - model.parameters.get>() = transition_rates; + model.parameters.get>() = adoption_rates; + model.parameters.get>() = transition_rates; double dt = 0.1; double tmax = 30.; diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index b05ac2b0a8..56f1bb78f1 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -36,6 +36,7 @@ template struct Influence { Status status; ScalarType factor; + std::tuple group_indices{}; }; /** @@ -52,6 +53,7 @@ struct AdoptionRate { mio::regions::Region region; // k ScalarType factor; // gammahat_{ij}^k std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) + std::tuple group_indices{}; }; } // namespace mio diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 4699fb7e27..ccd9d7ff38 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * -* Authors: René Schmieding, Julia Bicker +* Authors: René Schmieding, Julia Bicker, Kilian Volmer * * Contact: Martin J. Kuehn * @@ -22,6 +22,7 @@ #define MIO_SMM_MODEL_H #include "memilio/config.h" +#include "memilio/epidemiology/age_group.h" #include "smm/parameters.h" #include "memilio/compartments/compartmental_model.h" #include "memilio/epidemiology/populations.h" @@ -32,23 +33,29 @@ namespace mio namespace smm { +template +using age_group = T; + /** * @brief Stochastic Metapopulation Model. * @tparam regions Number of regions. * @tparam Status An infection state enum. */ -template -class Model : public mio::CompartmentalModel, - ParametersBase> +template +class Model : public mio::CompartmentalModel< + ScalarType, Status, + mio::Populations...>, + ParametersBase...>> { - using Base = mio::CompartmentalModel, - ParametersBase>; + using Base = mio::CompartmentalModel< + ScalarType, Status, + mio::Populations...>, + ParametersBase...>>; public: Model() - : Base(typename Base::Populations({static_cast(regions), Status::Count}), + : Base(typename Base::Populations( + {static_cast(regions), Status::Count, static_cast(Groups)...}), typename Base::ParameterSet()) { } @@ -59,26 +66,55 @@ class Model : public mio::CompartmentalModel& rate, const Eigen::VectorXd& x) const + ScalarType evaluate(const AdoptionRate...>& rate, + const Eigen::VectorXd& x) const { - const auto& pop = this->populations; - const auto source = pop.get_flat_index({rate.region, rate.from}); // Why is here rate.from used? KV - // determine order and calculate rate - if (rate.influences.size() == 0) { // first order adoption - return rate.factor * x[source]; + if constexpr (sizeof...(Groups) == 0) { + const auto& pop = this->populations; + const auto source = pop.get_flat_index({rate.region, rate.from}); // Why is here rate.from used? KV + // determine order and calculate rate + if (rate.influences.size() == 0) { // first order adoption + return rate.factor * x[source]; + } + else { // second order adoption + ScalarType N = 0; + for (size_t s = 0; s < static_cast(Status::Count); ++s) { + N += x[pop.get_flat_index({rate.region, Status(s)})]; + } + // accumulate influences + ScalarType influences = 0.0; + for (size_t i = 0; i < rate.influences.size(); i++) { + influences += + rate.influences[i].factor * x[pop.get_flat_index({rate.region, rate.influences[i].status})]; + } + return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; + } } - else { // second order adoption - ScalarType N = 0; - for (size_t s = 0; s < static_cast(Status::Count); ++s) { - N += x[pop.get_flat_index({rate.region, Status(s)})]; + else { + const auto& pop = this->populations; + const auto source = pop.get_flat_index({rate.region, rate.from, + std::make_from_tuple>( + rate.group_indices)}); // Why is here rate.from used? KV + // determine order and calculate rate + if (rate.influences.size() == 0) { // first order adoption + return rate.factor * x[source]; } - // accumulate influences - ScalarType influences = 0.0; - for (size_t i = 0; i < rate.influences.size(); i++) { - influences += - rate.influences[i].factor * x[pop.get_flat_index({rate.region, rate.influences[i].status})]; + else { // second order adoption + ScalarType N = 0; + for (size_t s = 0; s < static_cast(Status::Count); ++s) { + N += x[pop.get_flat_index( + {rate.region, Status(s), std::make_from_tuple>(rate.group_indices)})]; + } + // accumulate influences + ScalarType influences = 0.0; + for (size_t i = 0; i < rate.influences.size(); i++) { + influences += + rate.influences[i].factor * + x[pop.get_flat_index({rate.region, rate.influences[i].status, + std::make_from_tuple>(rate.group_indices)})]; + } + return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; } - return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; } } @@ -88,10 +124,18 @@ class Model : public mio::CompartmentalModel& rate, const Eigen::VectorXd& x) const + ScalarType evaluate(const TransitionRate...>& rate, + const Eigen::VectorXd& x) const { - const auto source = this->populations.get_flat_index({rate.from, rate.status}); - return rate.factor * x[source]; + if constexpr (sizeof...(Groups) == 0) { + const auto source = this->populations.get_flat_index({rate.from, rate.status}); + return rate.factor * x[source]; + } + else { + const auto source = this->populations.get_flat_index( + {rate.from, rate.status, std::make_from_tuple>(rate.group_indices)}); + return rate.factor * x[source]; + } } /** diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index 8a251f63af..945cd08f12 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -25,6 +25,7 @@ #include "memilio/geography/regions.h" #include "memilio/utils/parameter_set.h" #include "memilio/epidemiology/adoption_rate.h" +#include namespace mio { @@ -54,6 +55,7 @@ struct TransitionRate { mio::regions::Region from; // k mio::regions::Region to; // l ScalarType factor; // lambda_i^{kl} + std::tuple group_indices{}; }; template struct TransitionRates { @@ -65,7 +67,7 @@ struct TransitionRates { }; template -using ParametersBase = mio::ParameterSet, TransitionRates>; +using ParametersBase = mio::ParameterSet, TransitionRates>; } // namespace smm diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 01afaa1689..7a7bd69590 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -22,9 +22,12 @@ #define MIO_SMM_SIMULATION_H #include "memilio/config.h" +#include "memilio/epidemiology/age_group.h" +#include "memilio/utils/logging.h" #include "smm/model.h" #include "smm/parameters.h" #include "memilio/compartments/simulation.h" +#include namespace mio { @@ -37,7 +40,7 @@ namespace smm * @tparam regions The number of regions. * @tparam Status An infection state enum. */ -template +template class Simulation { public: @@ -79,56 +82,118 @@ class Simulation */ Eigen::Ref advance(ScalarType tmax) { - update_current_rates_and_waiting_times(); - size_t next_event = determine_next_event(); // index of the next event - ScalarType current_time = m_result.get_last_time(); - // set in the past to add a new time point immediately - ScalarType last_result_time = current_time - m_dt; - // iterate over time - while (current_time + m_waiting_times[next_event] < tmax) { - // update time - current_time += m_waiting_times[next_event]; - // regularily save current state in m_results - if (current_time > last_result_time + m_dt) { - last_result_time = current_time; - m_result.add_time_point(current_time); - // copy from the previous last value - m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; + if constexpr (sizeof...(Groups) == 0) { + update_current_rates_and_waiting_times(); + size_t next_event = determine_next_event(); // index of the next event + ScalarType current_time = m_result.get_last_time(); + // set in the past to add a new time point immediately + ScalarType last_result_time = current_time - m_dt; + // iterate over time + while (current_time + m_waiting_times[next_event] < tmax) { + // update time + current_time += m_waiting_times[next_event]; + // regularily save current state in m_results + if (current_time > last_result_time + m_dt) { + last_result_time = current_time; + m_result.add_time_point(current_time); + // copy from the previous last value + m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; + } + // decide event type by index and perform it + if (next_event < adoption_rates().size()) { + // perform adoption event + const auto& rate = adoption_rates()[next_event]; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from})] -= 1; + m_model->populations[{rate.region, rate.from}] -= 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to})] += 1; + m_model->populations[{rate.region, rate.to}] += 1; + } + else { + // perform transition event + const auto& rate = transition_rates()[next_event - adoption_rates().size()]; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status})] -= 1; + m_model->populations[{rate.from, rate.status}] -= 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status})] += 1; + m_model->populations[{rate.to, rate.status}] += 1; + } + // update internal times + for (size_t i = 0; i < m_internal_time.size(); i++) { + m_internal_time[i] += m_current_rates[i] * m_waiting_times[next_event]; + } + // draw new "next event" time for the occured event + m_tp_next_event[next_event] += + mio::ExponentialDistribution::get_instance()(m_model->get_rng(), 1.0); + // precalculate next event + update_current_rates_and_waiting_times(); + next_event = determine_next_event(); } - // decide event type by index and perform it - if (next_event < adoption_rates().size()) { - // perform adoption event - const auto& rate = adoption_rates()[next_event]; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from})] -= 1; - m_model->populations[{rate.region, rate.from}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to})] += 1; - m_model->populations[{rate.region, rate.to}] += 1; + // copy last result, if no event occurs between last_result_time and tmax + if (last_result_time < tmax) { + m_result.add_time_point(tmax); + m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; } - else { - // perform transition event - const auto& rate = transition_rates()[next_event - adoption_rates().size()]; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status})] -= 1; - m_model->populations[{rate.from, rate.status}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status})] += 1; - m_model->populations[{rate.to, rate.status}] += 1; + return m_result.get_last_value(); + } + else { + update_current_rates_and_waiting_times(); + size_t next_event = determine_next_event(); // index of the next event + ScalarType current_time = m_result.get_last_time(); + // set in the past to add a new time point immediately + ScalarType last_result_time = current_time - m_dt; + // iterate over time + while (current_time + m_waiting_times[next_event] < tmax) { + // update time + current_time += m_waiting_times[next_event]; + // regularily save current state in m_results + if (current_time > last_result_time + m_dt) { + last_result_time = current_time; + m_result.add_time_point(current_time); + // copy from the previous last value + m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; + } + // decide event type by index and perform it + if (next_event < adoption_rates().size()) { + // perform adoption event + const auto& rate = adoption_rates()[next_event]; + m_result.get_last_value()[m_model->populations.get_flat_index( + {rate.region, rate.from, std::make_from_tuple(rate.group_indices)})] -= 1; + m_model->populations[{rate.region, rate.from, + std::make_from_tuple(rate.group_indices)}] -= 1; + m_result.get_last_value()[m_model->populations.get_flat_index( + {rate.region, rate.to, std::make_from_tuple(rate.group_indices)})] += 1; + m_model->populations[{rate.region, rate.to, + std::make_from_tuple(rate.group_indices)}] += 1; + } + else { + // perform transition event + const auto& rate = transition_rates()[next_event - adoption_rates().size()]; + m_result.get_last_value()[m_model->populations.get_flat_index( + {rate.from, rate.status, std::make_from_tuple(rate.group_indices)})] -= 1; + m_model->populations[{rate.from, rate.status, + std::make_from_tuple(rate.group_indices)}] -= 1; + m_result.get_last_value()[m_model->populations.get_flat_index( + {rate.to, rate.status, std::make_from_tuple(rate.group_indices)})] += 1; + m_model->populations[{rate.to, rate.status, + std::make_from_tuple(rate.group_indices)}] += 1; + } + // update internal times + for (size_t i = 0; i < m_internal_time.size(); i++) { + m_internal_time[i] += m_current_rates[i] * m_waiting_times[next_event]; + } + // draw new "next event" time for the occured event + m_tp_next_event[next_event] += + mio::ExponentialDistribution::get_instance()(m_model->get_rng(), 1.0); + // precalculate next event + update_current_rates_and_waiting_times(); + next_event = determine_next_event(); } - // update internal times - for (size_t i = 0; i < m_internal_time.size(); i++) { - m_internal_time[i] += m_current_rates[i] * m_waiting_times[next_event]; + // copy last result, if no event occurs between last_result_time and tmax + if (last_result_time < tmax) { + m_result.add_time_point(tmax); + m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; } - // draw new "next event" time for the occured event - m_tp_next_event[next_event] += - mio::ExponentialDistribution::get_instance()(m_model->get_rng(), 1.0); - // precalculate next event - update_current_rates_and_waiting_times(); - next_event = determine_next_event(); - } - // copy last result, if no event occurs between last_result_time and tmax - if (last_result_time < tmax) { - m_result.add_time_point(tmax); - m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; + return m_result.get_last_value(); } - return m_result.get_last_value(); } /** @@ -160,21 +225,24 @@ class Simulation /** * @brief Returns the model's transition rates. */ - inline constexpr const typename smm::TransitionRates::Type& transition_rates() + inline constexpr const typename smm::TransitionRates...>::Type& + transition_rates() { - return m_model->parameters.template get>(); + return m_model->parameters.template get...>>(); } /** * @brief Returns the model's adoption rates. */ - inline constexpr const typename smm::AdoptionRates::Type& adoption_rates() + inline constexpr const typename smm::AdoptionRates...>::Type& + adoption_rates() { - return m_model->parameters.template get>(); + return m_model->parameters.template get...>>(); } /** - * @brief Calculate current values for m_current_rates and m_waiting_times. + * @brief + * */ inline void update_current_rates_and_waiting_times() { From bf4b839195421dab3d290e06dbfc6ab9d76c2a21 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:42:24 +0200 Subject: [PATCH 055/169] CHG: Compiling example with multiple age groups --- cpp/examples/smm.cpp | 45 +++++++++++++++++++----------------- cpp/models/smm/model.h | 38 ++++++++++++++++++++---------- cpp/models/smm/simulation.h | 46 ++++++++++++++++++++++++------------- 3 files changed, 80 insertions(+), 49 deletions(-) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index ca43ebda00..6eca53be93 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -34,52 +34,55 @@ enum class InfectionState R, D, Count - }; +using Age = mio::AgeGroup; +using Species = mio::AgeGroup; + int main() { //Example how to run the stochastic metapopulation models with four regions const size_t num_regions = 4; const size_t num_age_groups = 1; - using Model = mio::smm::Model; + const size_t num_groups = 1; + using Model = mio::smm::Model; double numE = 12, numC = 4, numI = 12, numR = 0, numD = 0; Model model; //Population are distributed uniformly to the four regions for (size_t r = 0; r < num_regions; ++r) { - model.populations[{mio::regions::Region(r), InfectionState::S, mio::AgeGroup(1)}] = + model.populations[{mio::regions::Region(r), InfectionState::S, Age(1), Species(1)}] = (1000 - numE - numC - numI - numR - numD) / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::E, mio::AgeGroup(1)}] = numE / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::C, mio::AgeGroup(1)}] = numC / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::I, mio::AgeGroup(1)}] = numI / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::R, mio::AgeGroup(1)}] = numR / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::D, mio::AgeGroup(1)}] = numD / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::E, Age(1), Species(1)}] = numE / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::C, Age(1), Species(1)}] = numC / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::I, Age(1), Species(1)}] = numI / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::R, Age(1), Species(1)}] = numR / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::D, Age(1), Species(1)}] = numD / num_regions; } //Set infection state adoption and spatial transition rates - std::vector> adoption_rates; - std::vector> transition_rates; + std::vector> adoption_rates; + std::vector> transition_rates; for (size_t r = 0; r < num_regions; ++r) { adoption_rates.push_back( {InfectionState::S, InfectionState::E, mio::regions::Region(r), 0.1, - {{InfectionState::C, 1, mio::AgeGroup(1)}, {InfectionState::I, 0.5, mio::AgeGroup(1)}}, - mio::AgeGroup(1)}); + {{InfectionState::C, 1, {Age(1), Species(1)}}, {InfectionState::I, 0.5, {Age(1), Species(1)}}}, + {Age(1), Species(1)}}); adoption_rates.push_back( - {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, mio::AgeGroup(1)}); + {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, mio::AgeGroup(1)}); + {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, mio::AgeGroup(1)}); + {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, mio::AgeGroup(1)}); + {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, mio::AgeGroup(1)}); + {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(1), Species(1)}}); } //Agents in infection state D do not transition @@ -92,21 +95,21 @@ int main() mio::regions::Region(i), mio::regions::Region(j), 0.01, - mio::AgeGroup(1), + {Age(1), Species(1)}, }); transition_rates.push_back({ InfectionState(s), mio::regions::Region(j), mio::regions::Region(i), 0.01, - mio::AgeGroup(1), + {Age(1), Species(1)}, }); } } } - model.parameters.get>() = adoption_rates; - model.parameters.get>() = transition_rates; + model.parameters.get>() = adoption_rates; + model.parameters.get>() = transition_rates; double dt = 0.1; double tmax = 30.; diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index ccd9d7ff38..1966d76b28 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -51,6 +51,7 @@ class Model : public mio::CompartmentalModel< ScalarType, Status, mio::Populations...>, ParametersBase...>>; + using Index = mio::Index...>; public: Model() @@ -91,10 +92,13 @@ class Model : public mio::CompartmentalModel< } } else { - const auto& pop = this->populations; - const auto source = pop.get_flat_index({rate.region, rate.from, - std::make_from_tuple>( - rate.group_indices)}); // Why is here rate.from used? KV + const auto& pop = this->populations; + const auto index_from = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.from, std::forward(args)...}; + }, + rate.group_indices); + const auto source = pop.get_flat_index(index_from); // Why is here rate.from used? KV // determine order and calculate rate if (rate.influences.size() == 0) { // first order adoption return rate.factor * x[source]; @@ -102,16 +106,22 @@ class Model : public mio::CompartmentalModel< else { // second order adoption ScalarType N = 0; for (size_t s = 0; s < static_cast(Status::Count); ++s) { - N += x[pop.get_flat_index( - {rate.region, Status(s), std::make_from_tuple>(rate.group_indices)})]; + const auto index = std::apply( + [&](auto&&... args) { + return Index{rate.region, Status(s), std::forward(args)...}; + }, + rate.group_indices); + N += x[pop.get_flat_index(index)]; } // accumulate influences ScalarType influences = 0.0; for (size_t i = 0; i < rate.influences.size(); i++) { - influences += - rate.influences[i].factor * - x[pop.get_flat_index({rate.region, rate.influences[i].status, - std::make_from_tuple>(rate.group_indices)})]; + const auto index = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.influences[i].status, std::forward(args)...}; + }, + rate.group_indices); + influences += rate.influences[i].factor * x[pop.get_flat_index(index)]; } return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; } @@ -132,8 +142,12 @@ class Model : public mio::CompartmentalModel< return rate.factor * x[source]; } else { - const auto source = this->populations.get_flat_index( - {rate.from, rate.status, std::make_from_tuple>(rate.group_indices)}); + auto index = std::apply( + [&](auto&&... args) { + return Index{rate.from, rate.status, std::forward(args)...}; + }, + rate.group_indices); + const auto source = this->populations.get_flat_index(index); return rate.factor * x[source]; } } diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 7a7bd69590..177e83218b 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -23,6 +23,7 @@ #include "memilio/config.h" #include "memilio/epidemiology/age_group.h" +#include "memilio/utils/index.h" #include "memilio/utils/logging.h" #include "smm/model.h" #include "smm/parameters.h" @@ -45,6 +46,7 @@ class Simulation { public: using Model = smm::Model; + using Index = mio::Index...>; /** * @brief Set up the simulation for a Stochastic Metapopulation Model. @@ -155,26 +157,38 @@ class Simulation if (next_event < adoption_rates().size()) { // perform adoption event const auto& rate = adoption_rates()[next_event]; - m_result.get_last_value()[m_model->populations.get_flat_index( - {rate.region, rate.from, std::make_from_tuple(rate.group_indices)})] -= 1; - m_model->populations[{rate.region, rate.from, - std::make_from_tuple(rate.group_indices)}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index( - {rate.region, rate.to, std::make_from_tuple(rate.group_indices)})] += 1; - m_model->populations[{rate.region, rate.to, - std::make_from_tuple(rate.group_indices)}] += 1; + auto index_from = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.from, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; + m_model->populations[index_from] -= 1; + auto index_to = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.to, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; + m_model->populations[index_to] += 1; } else { // perform transition event const auto& rate = transition_rates()[next_event - adoption_rates().size()]; - m_result.get_last_value()[m_model->populations.get_flat_index( - {rate.from, rate.status, std::make_from_tuple(rate.group_indices)})] -= 1; - m_model->populations[{rate.from, rate.status, - std::make_from_tuple(rate.group_indices)}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index( - {rate.to, rate.status, std::make_from_tuple(rate.group_indices)})] += 1; - m_model->populations[{rate.to, rate.status, - std::make_from_tuple(rate.group_indices)}] += 1; + auto index_from = std::apply( + [&](auto&&... args) { + return Index{rate.from, rate.status, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; + m_model->populations[index_from] -= 1; + auto index_to = std::apply( + [&](auto&&... args) { + return Index{rate.to, rate.status, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; + m_model->populations[index_to] += 1; } // update internal times for (size_t i = 0; i < m_internal_time.size(); i++) { From cdba05668a28cb026e44833c5e133db57902ac75 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:49:13 +0200 Subject: [PATCH 056/169] Compiling solution without code duplicates --- cpp/models/smm/model.h | 101 +++++++------------- cpp/models/smm/simulation.h | 185 +++++++++++++----------------------- 2 files changed, 102 insertions(+), 184 deletions(-) diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 1966d76b28..637b2f2aba 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -70,61 +70,38 @@ class Model : public mio::CompartmentalModel< ScalarType evaluate(const AdoptionRate...>& rate, const Eigen::VectorXd& x) const { - if constexpr (sizeof...(Groups) == 0) { - const auto& pop = this->populations; - const auto source = pop.get_flat_index({rate.region, rate.from}); // Why is here rate.from used? KV - // determine order and calculate rate - if (rate.influences.size() == 0) { // first order adoption - return rate.factor * x[source]; - } - else { // second order adoption - ScalarType N = 0; - for (size_t s = 0; s < static_cast(Status::Count); ++s) { - N += x[pop.get_flat_index({rate.region, Status(s)})]; - } - // accumulate influences - ScalarType influences = 0.0; - for (size_t i = 0; i < rate.influences.size(); i++) { - influences += - rate.influences[i].factor * x[pop.get_flat_index({rate.region, rate.influences[i].status})]; - } - return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; - } + const auto& pop = this->populations; + const auto index_from = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.from, std::forward(args)...}; + }, + rate.group_indices); + const auto source = pop.get_flat_index(index_from); // Why is here rate.from used? KV + // determine order and calculate rate + if (rate.influences.size() == 0) { // first order adoption + return rate.factor * x[source]; } - else { - const auto& pop = this->populations; - const auto index_from = std::apply( - [&](auto&&... args) { - return Index{rate.region, rate.from, std::forward(args)...}; - }, - rate.group_indices); - const auto source = pop.get_flat_index(index_from); // Why is here rate.from used? KV - // determine order and calculate rate - if (rate.influences.size() == 0) { // first order adoption - return rate.factor * x[source]; + else { // second order adoption + ScalarType N = 0; + for (size_t s = 0; s < static_cast(Status::Count); ++s) { + const auto index = std::apply( + [&](auto&&... args) { + return Index{rate.region, Status(s), std::forward(args)...}; + }, + rate.group_indices); + N += x[pop.get_flat_index(index)]; } - else { // second order adoption - ScalarType N = 0; - for (size_t s = 0; s < static_cast(Status::Count); ++s) { - const auto index = std::apply( - [&](auto&&... args) { - return Index{rate.region, Status(s), std::forward(args)...}; - }, - rate.group_indices); - N += x[pop.get_flat_index(index)]; - } - // accumulate influences - ScalarType influences = 0.0; - for (size_t i = 0; i < rate.influences.size(); i++) { - const auto index = std::apply( - [&](auto&&... args) { - return Index{rate.region, rate.influences[i].status, std::forward(args)...}; - }, - rate.group_indices); - influences += rate.influences[i].factor * x[pop.get_flat_index(index)]; - } - return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; + // accumulate influences + ScalarType influences = 0.0; + for (size_t i = 0; i < rate.influences.size(); i++) { + const auto index = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.influences[i].status, std::forward(args)...}; + }, + rate.group_indices); + influences += rate.influences[i].factor * x[pop.get_flat_index(index)]; } + return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; } } @@ -137,19 +114,13 @@ class Model : public mio::CompartmentalModel< ScalarType evaluate(const TransitionRate...>& rate, const Eigen::VectorXd& x) const { - if constexpr (sizeof...(Groups) == 0) { - const auto source = this->populations.get_flat_index({rate.from, rate.status}); - return rate.factor * x[source]; - } - else { - auto index = std::apply( - [&](auto&&... args) { - return Index{rate.from, rate.status, std::forward(args)...}; - }, - rate.group_indices); - const auto source = this->populations.get_flat_index(index); - return rate.factor * x[source]; - } + auto index = std::apply( + [&](auto&&... args) { + return Index{rate.from, rate.status, std::forward(args)...}; + }, + rate.group_indices); + const auto source = this->populations.get_flat_index(index); + return rate.factor * x[source]; } /** diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 177e83218b..140afe070b 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -84,130 +84,77 @@ class Simulation */ Eigen::Ref advance(ScalarType tmax) { - if constexpr (sizeof...(Groups) == 0) { - update_current_rates_and_waiting_times(); - size_t next_event = determine_next_event(); // index of the next event - ScalarType current_time = m_result.get_last_time(); - // set in the past to add a new time point immediately - ScalarType last_result_time = current_time - m_dt; - // iterate over time - while (current_time + m_waiting_times[next_event] < tmax) { - // update time - current_time += m_waiting_times[next_event]; - // regularily save current state in m_results - if (current_time > last_result_time + m_dt) { - last_result_time = current_time; - m_result.add_time_point(current_time); - // copy from the previous last value - m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; - } - // decide event type by index and perform it - if (next_event < adoption_rates().size()) { - // perform adoption event - const auto& rate = adoption_rates()[next_event]; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from})] -= 1; - m_model->populations[{rate.region, rate.from}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to})] += 1; - m_model->populations[{rate.region, rate.to}] += 1; - } - else { - // perform transition event - const auto& rate = transition_rates()[next_event - adoption_rates().size()]; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status})] -= 1; - m_model->populations[{rate.from, rate.status}] -= 1; - m_result.get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status})] += 1; - m_model->populations[{rate.to, rate.status}] += 1; - } - // update internal times - for (size_t i = 0; i < m_internal_time.size(); i++) { - m_internal_time[i] += m_current_rates[i] * m_waiting_times[next_event]; - } - // draw new "next event" time for the occured event - m_tp_next_event[next_event] += - mio::ExponentialDistribution::get_instance()(m_model->get_rng(), 1.0); - // precalculate next event - update_current_rates_and_waiting_times(); - next_event = determine_next_event(); - } - // copy last result, if no event occurs between last_result_time and tmax - if (last_result_time < tmax) { - m_result.add_time_point(tmax); + update_current_rates_and_waiting_times(); + size_t next_event = determine_next_event(); // index of the next event + ScalarType current_time = m_result.get_last_time(); + // set in the past to add a new time point immediately + ScalarType last_result_time = current_time - m_dt; + // iterate over time + while (current_time + m_waiting_times[next_event] < tmax) { + // update time + current_time += m_waiting_times[next_event]; + // regularily save current state in m_results + if (current_time > last_result_time + m_dt) { + last_result_time = current_time; + m_result.add_time_point(current_time); + // copy from the previous last value m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; } - return m_result.get_last_value(); - } - else { - update_current_rates_and_waiting_times(); - size_t next_event = determine_next_event(); // index of the next event - ScalarType current_time = m_result.get_last_time(); - // set in the past to add a new time point immediately - ScalarType last_result_time = current_time - m_dt; - // iterate over time - while (current_time + m_waiting_times[next_event] < tmax) { - // update time - current_time += m_waiting_times[next_event]; - // regularily save current state in m_results - if (current_time > last_result_time + m_dt) { - last_result_time = current_time; - m_result.add_time_point(current_time); - // copy from the previous last value - m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; - } - // decide event type by index and perform it - if (next_event < adoption_rates().size()) { - // perform adoption event - const auto& rate = adoption_rates()[next_event]; - auto index_from = std::apply( - [&](auto&&... args) { - return Index{rate.region, rate.from, std::forward(args)...}; - }, - rate.group_indices); - m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; - m_model->populations[index_from] -= 1; - auto index_to = std::apply( - [&](auto&&... args) { - return Index{rate.region, rate.to, std::forward(args)...}; - }, - rate.group_indices); - m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; - m_model->populations[index_to] += 1; - } - else { - // perform transition event - const auto& rate = transition_rates()[next_event - adoption_rates().size()]; - auto index_from = std::apply( - [&](auto&&... args) { - return Index{rate.from, rate.status, std::forward(args)...}; - }, - rate.group_indices); - m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; - m_model->populations[index_from] -= 1; - auto index_to = std::apply( - [&](auto&&... args) { - return Index{rate.to, rate.status, std::forward(args)...}; - }, - rate.group_indices); - m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; - m_model->populations[index_to] += 1; - } - // update internal times - for (size_t i = 0; i < m_internal_time.size(); i++) { - m_internal_time[i] += m_current_rates[i] * m_waiting_times[next_event]; - } - // draw new "next event" time for the occured event - m_tp_next_event[next_event] += - mio::ExponentialDistribution::get_instance()(m_model->get_rng(), 1.0); - // precalculate next event - update_current_rates_and_waiting_times(); - next_event = determine_next_event(); + // decide event type by index and perform it + if (next_event < adoption_rates().size()) { + // perform adoption event + const auto& rate = adoption_rates()[next_event]; + auto index_from = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.from, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; + m_model->populations[index_from] -= 1; + auto index_to = std::apply( + [&](auto&&... args) { + return Index{rate.region, rate.to, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; + m_model->populations[index_to] += 1; } - // copy last result, if no event occurs between last_result_time and tmax - if (last_result_time < tmax) { - m_result.add_time_point(tmax); - m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; + else { + // perform transition event + const auto& rate = transition_rates()[next_event - adoption_rates().size()]; + auto index_from = std::apply( + [&](auto&&... args) { + return Index{rate.from, rate.status, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; + m_model->populations[index_from] -= 1; + auto index_to = std::apply( + [&](auto&&... args) { + return Index{rate.to, rate.status, std::forward(args)...}; + }, + rate.group_indices); + m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; + m_model->populations[index_to] += 1; + } + // update internal times + for (size_t i = 0; i < m_internal_time.size(); i++) { + m_internal_time[i] += m_current_rates[i] * m_waiting_times[next_event]; } - return m_result.get_last_value(); + // draw new "next event" time for the occured event + m_tp_next_event[next_event] += + mio::ExponentialDistribution::get_instance()(m_model->get_rng(), 1.0); + // precalculate next event + update_current_rates_and_waiting_times(); + next_event = determine_next_event(); + } + // copy last result, if no event occurs between last_result_time and tmax + if (last_result_time < tmax) { + m_result.add_time_point(tmax); + m_result.get_last_value() = m_result[m_result.get_num_time_points() - 2]; } + return m_result.get_last_value(); + // } } /** From c0cb41b6be31e5fd5b51a1c56ed9a0a6b6e70f07 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 12 Sep 2025 20:09:54 +0200 Subject: [PATCH 057/169] CHG: semifunctional state --- cpp/examples/smm.cpp | 50 ++++++++++++------------ cpp/memilio/epidemiology/adoption_rate.h | 47 ++++++++++++++++++++++ cpp/models/smm/model.h | 2 +- cpp/models/smm/parameters.h | 3 +- cpp/models/smm/simulation.h | 4 +- 5 files changed, 76 insertions(+), 30 deletions(-) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index 6eca53be93..77d72d7f7c 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -66,23 +66,23 @@ int main() std::vector> adoption_rates; std::vector> transition_rates; for (size_t r = 0; r < num_regions; ++r) { + adoption_rates.push_back({InfectionState::S, + InfectionState::E, + mio::regions::Region(r), + 0.1, + {{InfectionState::C, 1, mio::regions::Region(1), {Age(1), Species(1)}}, + {InfectionState::I, 0.5, mio::regions::Region(1), {Age(1), Species(1)}}}, + {Age(1), Species(1)}}); adoption_rates.push_back( - {InfectionState::S, - InfectionState::E, - mio::regions::Region(r), - 0.1, - {{InfectionState::C, 1, {Age(1), Species(1)}}, {InfectionState::I, 0.5, {Age(1), Species(1)}}}, - {Age(1), Species(1)}}); + {{InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(1), Species(1)}}}); adoption_rates.push_back( - {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(1), Species(1)}}); + {{InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(1), Species(1)}}}); adoption_rates.push_back( - {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(1), Species(1)}}); + {{InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(1), Species(1)}}}); adoption_rates.push_back( - {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(1), Species(1)}}); + {{InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(1), Species(1)}}}); adoption_rates.push_back( - {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(1), Species(1)}}); - adoption_rates.push_back( - {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(1), Species(1)}}); + {{InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(1), Species(1)}}}); } //Agents in infection state D do not transition @@ -90,20 +90,18 @@ int main() for (size_t i = 0; i < num_regions; ++i) { for (size_t j = 0; j < num_regions; ++j) if (i != j) { - transition_rates.push_back({ - InfectionState(s), - mio::regions::Region(i), - mio::regions::Region(j), - 0.01, - {Age(1), Species(1)}, - }); - transition_rates.push_back({ - InfectionState(s), - mio::regions::Region(j), - mio::regions::Region(i), - 0.01, - {Age(1), Species(1)}, - }); + transition_rates.push_back({InfectionState(s), + mio::regions::Region(i), + mio::regions::Region(j), + 0.01, + {Age(1), Species(1)}, + {Age(1), Species(1)}}); + transition_rates.push_back({InfectionState(s), + mio::regions::Region(j), + mio::regions::Region(i), + 0.01, + {Age(1), Species(1)}, + {Age(1), Species(1)}}); } } } diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index 56f1bb78f1..dfe659e922 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -31,11 +31,13 @@ namespace mio * @brief Struct defining an influence for a second-order adoption. * The population having "status" is multiplied with "factor." * @tparam Status An infection state enum. + * @tparam Groups Additional grouping indices. */ template struct Influence { Status status; ScalarType factor; + mio::regions::Region region; std::tuple group_indices{}; }; @@ -45,6 +47,7 @@ struct Influence { * In the d_abm and smm simulations, "from" is implicitly an influence, scaled by "factor". This is multiplied by * the sum over all "influences", which scale their "status" with the respective "factor". * @tparam Status An infection state enum. + * @tparam Groups Additional grouping indices. */ template struct AdoptionRate { @@ -54,6 +57,50 @@ struct AdoptionRate { ScalarType factor; // gammahat_{ij}^k std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) std::tuple group_indices{}; + + template + AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + ScalarType initializer_factor, std::vector> second_order_influences, + T... Ts) + : from(initializer_from) + , to(initializer_to) + , region(initializer_region) + , factor(initializer_factor) + , group_indices{} + { + for (const auto& [status, scalar] : second_order_influences) { + influences.emplace_back(Influence{status, scalar, initializer_region}); + } + mio::unused(Ts...); + } + template + AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + ScalarType initializer_factor, + std::vector>> + second_order_influences, + T... Ts) + : from(initializer_from) + , to(initializer_to) + , region(initializer_region) + , factor(initializer_factor) + , group_indices{} + { + for (const auto& [status, scalar, influence_region, groups] : second_order_influences) { + influences.emplace_back(Influence{status, scalar, influence_region, groups}); + } + mio::unused(Ts...); + } + AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + ScalarType initializer_factor, std::tuple<> second_order_influences) + : from(initializer_from) + , to(initializer_to) + , region(initializer_region) + , factor(initializer_factor) + , influences{} + , group_indices{} + { + mio::unused(second_order_influences); + } }; } // namespace mio diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 637b2f2aba..f2e1a8d904 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -118,7 +118,7 @@ class Model : public mio::CompartmentalModel< [&](auto&&... args) { return Index{rate.from, rate.status, std::forward(args)...}; }, - rate.group_indices); + rate.group_indices_from); const auto source = this->populations.get_flat_index(index); return rate.factor * x[source]; } diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index 945cd08f12..71c1c90b94 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -55,7 +55,8 @@ struct TransitionRate { mio::regions::Region from; // k mio::regions::Region to; // l ScalarType factor; // lambda_i^{kl} - std::tuple group_indices{}; + std::tuple group_indices_from{}; + std::tuple group_indices_to{}; }; template struct TransitionRates { diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 140afe070b..924036736d 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -126,14 +126,14 @@ class Simulation [&](auto&&... args) { return Index{rate.from, rate.status, std::forward(args)...}; }, - rate.group_indices); + rate.group_indices_from); m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; m_model->populations[index_from] -= 1; auto index_to = std::apply( [&](auto&&... args) { return Index{rate.to, rate.status, std::forward(args)...}; }, - rate.group_indices); + rate.group_indices_to); m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; m_model->populations[index_to] += 1; } From ee5143b4963199c4e0a2faebe0db8a1779a1dff8 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:35:22 +0200 Subject: [PATCH 058/169] CHG: Change numbers in rates --- cpp/examples/smm.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index 77d72d7f7c..ad331bee19 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -74,15 +74,15 @@ int main() {InfectionState::I, 0.5, mio::regions::Region(1), {Age(1), Species(1)}}}, {Age(1), Species(1)}}); adoption_rates.push_back( - {{InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(1), Species(1)}}}); + {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {{InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(1), Species(1)}}}); + {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {{InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(1), Species(1)}}}); + {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {{InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(1), Species(1)}}}); + {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(1), Species(1)}}); adoption_rates.push_back( - {{InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(1), Species(1)}}}); + {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(1), Species(1)}}); } //Agents in infection state D do not transition From 4823827079d993d9b3ec2c80f8a4d3b4d674100c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:35:40 +0200 Subject: [PATCH 059/169] CHG: Move Influence into AdoptionRate --- cpp/memilio/epidemiology/adoption_rate.h | 159 +++++++++++++++-------- 1 file changed, 106 insertions(+), 53 deletions(-) diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index dfe659e922..4cf13f3970 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -31,15 +31,17 @@ namespace mio * @brief Struct defining an influence for a second-order adoption. * The population having "status" is multiplied with "factor." * @tparam Status An infection state enum. - * @tparam Groups Additional grouping indices. - */ -template -struct Influence { - Status status; - ScalarType factor; - mio::regions::Region region; - std::tuple group_indices{}; -}; +// * @tparam Groups Additional grouping indices. +// */ +// template +// struct Influence { +// Status status; +// ScalarType factor; +// mio::regions::Region region{0}; +// std::tuple group_indices{}; +// }; +//try out to define Influence struct in AdoptionRate struct +// otherwise Influence struct in Adoption Rate class /** * @brief Struct defining a possible status adoption in a Model based on Poisson Processes. @@ -51,56 +53,107 @@ struct Influence { */ template struct AdoptionRate { + struct Influence { + Status status; + ScalarType factor; + mio::regions::Region region = region; + std::tuple group_indices{}; + }; + Status from; // i Status to; // j mio::regions::Region region; // k ScalarType factor; // gammahat_{ij}^k - std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) + std::vector influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) std::tuple group_indices{}; - template - AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - ScalarType initializer_factor, std::vector> second_order_influences, - T... Ts) - : from(initializer_from) - , to(initializer_to) - , region(initializer_region) - , factor(initializer_factor) - , group_indices{} - { - for (const auto& [status, scalar] : second_order_influences) { - influences.emplace_back(Influence{status, scalar, initializer_region}); - } - mio::unused(Ts...); - } - template - AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - ScalarType initializer_factor, - std::vector>> - second_order_influences, - T... Ts) - : from(initializer_from) - , to(initializer_to) - , region(initializer_region) - , factor(initializer_factor) - , group_indices{} - { - for (const auto& [status, scalar, influence_region, groups] : second_order_influences) { - influences.emplace_back(Influence{status, scalar, influence_region, groups}); - } - mio::unused(Ts...); - } - AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - ScalarType initializer_factor, std::tuple<> second_order_influences) - : from(initializer_from) - , to(initializer_to) - , region(initializer_region) - , factor(initializer_factor) - , influences{} - , group_indices{} - { - mio::unused(second_order_influences); - } + // template + // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + // ScalarType initializer_factor, std::vector> second_order_influences, + // T... Ts) + // : from(initializer_from) + // , to(initializer_to) + // , region(initializer_region) + // , factor(initializer_factor) + // , group_indices{} + // { + // for (const auto& [status, scalar] : second_order_influences) { + // influences.emplace_back(Influence{status, scalar, initializer_region}); + // } + // mio::unused(Ts...); + // } + // template + // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + // ScalarType initializer_factor, + // std::vector>> + // second_order_influences, + // T... Ts) + // : from(initializer_from) + // , to(initializer_to) + // , region(initializer_region) + // , factor(initializer_factor) + // , group_indices{} + // { + // for (const auto& [status, scalar, influence_region, groups] : second_order_influences) { + // influences.emplace_back(Influence{status, scalar, influence_region, groups}); + // } + // mio::unused(Ts...); + // } + // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + // ScalarType initializer_factor, std::tuple<> second_order_influences) + // : from(initializer_from) + // , to(initializer_to) + // , region(initializer_region) + // , factor(initializer_factor) + // , influences{} + // , group_indices{} + // { + // mio::unused(second_order_influences); + // } + // template + // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + // ScalarType initializer_factor, std::vector> second_order_influences, + // std::tuple initializer_groups, T... Ts) + // : from(initializer_from) + // , to(initializer_to) + // , region(initializer_region) + // , factor(initializer_factor) + // , group_indices{initializer_groups} + // { + // for (const auto& [status, scalar] : second_order_influences) { + // influences.emplace_back(Influence{status, scalar, initializer_region}); + // } + // mio::unused(Ts...); + // } + // template + // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + // ScalarType initializer_factor, + // std::vector>> + // second_order_influences, + // std::tuple initializer_groups, T... Ts) + // : from(initializer_from) + // , to(initializer_to) + // , region(initializer_region) + // , factor(initializer_factor) + // , group_indices{initializer_groups} + // { + // for (const auto& [status, scalar, influence_region, groups] : second_order_influences) { + // influences.emplace_back(Influence{status, scalar, influence_region, groups}); + // } + // mio::unused(Ts...); + // } + // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, + // ScalarType initializer_factor, std::tuple<> second_order_influences, + // std::tuple initializer_groups) + // : from(initializer_from) + // , to(initializer_to) + // , region(initializer_region) + // , factor(initializer_factor) + // , influences{} + // , group_indices{initializer_groups} + // { + // mio::unused(second_order_influences); + // } }; } // namespace mio From 893214413513219c59a974f81baf8d82bc148e58 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:18:47 +0200 Subject: [PATCH 060/169] CHG: Update influences --- cpp/examples/smm.cpp | 36 +++---- cpp/memilio/epidemiology/adoption_rate.h | 119 +++-------------------- cpp/models/smm/model.h | 5 +- 3 files changed, 34 insertions(+), 126 deletions(-) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index ad331bee19..cb9c0f62fc 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -53,13 +53,13 @@ int main() Model model; //Population are distributed uniformly to the four regions for (size_t r = 0; r < num_regions; ++r) { - model.populations[{mio::regions::Region(r), InfectionState::S, Age(1), Species(1)}] = + model.populations[{mio::regions::Region(r), InfectionState::S, Age(0), Species(0)}] = (1000 - numE - numC - numI - numR - numD) / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::E, Age(1), Species(1)}] = numE / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::C, Age(1), Species(1)}] = numC / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::I, Age(1), Species(1)}] = numI / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::R, Age(1), Species(1)}] = numR / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::D, Age(1), Species(1)}] = numD / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::E, Age(0), Species(0)}] = numE / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::C, Age(0), Species(0)}] = numC / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::I, Age(0), Species(0)}] = numI / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::R, Age(0), Species(0)}] = numR / num_regions; + model.populations[{mio::regions::Region(r), InfectionState::D, Age(0), Species(0)}] = numD / num_regions; } //Set infection state adoption and spatial transition rates @@ -70,19 +70,19 @@ int main() InfectionState::E, mio::regions::Region(r), 0.1, - {{InfectionState::C, 1, mio::regions::Region(1), {Age(1), Species(1)}}, - {InfectionState::I, 0.5, mio::regions::Region(1), {Age(1), Species(1)}}}, - {Age(1), Species(1)}}); + {{InfectionState::C, 1, mio::regions::Region(3), {Age(0), Species(0)}}, + {InfectionState::I, 0.5, mio::regions::Region(1), {Age(0), Species(0)}}}, + {Age(0), Species(0)}}); adoption_rates.push_back( - {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(1), Species(1)}}); + {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(0), Species(0)}}); adoption_rates.push_back( - {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(1), Species(1)}}); + {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(0), Species(0)}}); adoption_rates.push_back( - {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(1), Species(1)}}); + {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(0), Species(0)}}); adoption_rates.push_back( - {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(1), Species(1)}}); + {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(0), Species(0)}}); adoption_rates.push_back( - {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(1), Species(1)}}); + {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(0), Species(0)}}); } //Agents in infection state D do not transition @@ -94,14 +94,14 @@ int main() mio::regions::Region(i), mio::regions::Region(j), 0.01, - {Age(1), Species(1)}, - {Age(1), Species(1)}}); + {Age(0), Species(0)}, + {Age(0), Species(0)}}); transition_rates.push_back({InfectionState(s), mio::regions::Region(j), mio::regions::Region(i), 0.01, - {Age(1), Species(1)}, - {Age(1), Species(1)}}); + {Age(0), Species(0)}, + {Age(0), Species(0)}}); } } } diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index 4cf13f3970..2917d841d3 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -23,26 +23,12 @@ #include "memilio/utils/index.h" #include "memilio/config.h" #include "memilio/geography/regions.h" - +#include +#include +#include namespace mio { -/** - * @brief Struct defining an influence for a second-order adoption. - * The population having "status" is multiplied with "factor." - * @tparam Status An infection state enum. -// * @tparam Groups Additional grouping indices. -// */ -// template -// struct Influence { -// Status status; -// ScalarType factor; -// mio::regions::Region region{0}; -// std::tuple group_indices{}; -// }; -//try out to define Influence struct in AdoptionRate struct -// otherwise Influence struct in Adoption Rate class - /** * @brief Struct defining a possible status adoption in a Model based on Poisson Processes. * The AdoptionRate is considered to be of second-order if there are any "influences". @@ -53,10 +39,19 @@ namespace mio */ template struct AdoptionRate { + + /** + * @brief Struct defining an influence for a second-order adoption. + * The population having "status" is multiplied with "factor." + * @tparam status An infection state enum. + * @tparam factor Scaling factor for the influence. + * @tparam + * @tparam Groups Additional grouping indices. + */ struct Influence { Status status; ScalarType factor; - mio::regions::Region region = region; + std::optional region = std::nullopt; std::tuple group_indices{}; }; @@ -66,94 +61,6 @@ struct AdoptionRate { ScalarType factor; // gammahat_{ij}^k std::vector influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) std::tuple group_indices{}; - - // template - // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - // ScalarType initializer_factor, std::vector> second_order_influences, - // T... Ts) - // : from(initializer_from) - // , to(initializer_to) - // , region(initializer_region) - // , factor(initializer_factor) - // , group_indices{} - // { - // for (const auto& [status, scalar] : second_order_influences) { - // influences.emplace_back(Influence{status, scalar, initializer_region}); - // } - // mio::unused(Ts...); - // } - // template - // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - // ScalarType initializer_factor, - // std::vector>> - // second_order_influences, - // T... Ts) - // : from(initializer_from) - // , to(initializer_to) - // , region(initializer_region) - // , factor(initializer_factor) - // , group_indices{} - // { - // for (const auto& [status, scalar, influence_region, groups] : second_order_influences) { - // influences.emplace_back(Influence{status, scalar, influence_region, groups}); - // } - // mio::unused(Ts...); - // } - // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - // ScalarType initializer_factor, std::tuple<> second_order_influences) - // : from(initializer_from) - // , to(initializer_to) - // , region(initializer_region) - // , factor(initializer_factor) - // , influences{} - // , group_indices{} - // { - // mio::unused(second_order_influences); - // } - // template - // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - // ScalarType initializer_factor, std::vector> second_order_influences, - // std::tuple initializer_groups, T... Ts) - // : from(initializer_from) - // , to(initializer_to) - // , region(initializer_region) - // , factor(initializer_factor) - // , group_indices{initializer_groups} - // { - // for (const auto& [status, scalar] : second_order_influences) { - // influences.emplace_back(Influence{status, scalar, initializer_region}); - // } - // mio::unused(Ts...); - // } - // template - // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - // ScalarType initializer_factor, - // std::vector>> - // second_order_influences, - // std::tuple initializer_groups, T... Ts) - // : from(initializer_from) - // , to(initializer_to) - // , region(initializer_region) - // , factor(initializer_factor) - // , group_indices{initializer_groups} - // { - // for (const auto& [status, scalar, influence_region, groups] : second_order_influences) { - // influences.emplace_back(Influence{status, scalar, influence_region, groups}); - // } - // mio::unused(Ts...); - // } - // AdoptionRate(Status initializer_from, Status initializer_to, mio::regions::Region initializer_region, - // ScalarType initializer_factor, std::tuple<> second_order_influences, - // std::tuple initializer_groups) - // : from(initializer_from) - // , to(initializer_to) - // , region(initializer_region) - // , factor(initializer_factor) - // , influences{} - // , group_indices{initializer_groups} - // { - // mio::unused(second_order_influences); - // } }; } // namespace mio diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index f2e1a8d904..878066552c 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -96,9 +96,10 @@ class Model : public mio::CompartmentalModel< for (size_t i = 0; i < rate.influences.size(); i++) { const auto index = std::apply( [&](auto&&... args) { - return Index{rate.region, rate.influences[i].status, std::forward(args)...}; + return Index{rate.influences[i].region.value_or(rate.region), rate.influences[i].status, + std::forward(args)...}; }, - rate.group_indices); + rate.influences[i].group_indices); influences += rate.influences[i].factor * x[pop.get_flat_index(index)]; } return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; From 098e1ff182c500d2e7fc6afb505b114cadd9e86c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:42:47 +0200 Subject: [PATCH 061/169] CHG: Change location of N, modify tests --- cpp/models/smm/model.h | 26 +++++----- cpp/tests/test_smm_model.cpp | 93 +++++++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 878066552c..1c3e3f8123 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -82,27 +82,31 @@ class Model : public mio::CompartmentalModel< return rate.factor * x[source]; } else { // second order adoption - ScalarType N = 0; - for (size_t s = 0; s < static_cast(Status::Count); ++s) { - const auto index = std::apply( - [&](auto&&... args) { - return Index{rate.region, Status(s), std::forward(args)...}; - }, - rate.group_indices); - N += x[pop.get_flat_index(index)]; - } // accumulate influences ScalarType influences = 0.0; for (size_t i = 0; i < rate.influences.size(); i++) { + ScalarType N = 0; // Welches N brauchen wir hier?? + + for (size_t s = 0; s < static_cast(Status::Count); ++s) { + const auto index = std::apply( + [&](auto&&... args) { + return Index{rate.influences[i].region.value_or(rate.region), Status(s), + std::forward(args)...}; + }, + rate.influences[i].group_indices); + N += x[pop.get_flat_index(index)]; + } const auto index = std::apply( [&](auto&&... args) { return Index{rate.influences[i].region.value_or(rate.region), rate.influences[i].status, std::forward(args)...}; }, rate.influences[i].group_indices); - influences += rate.influences[i].factor * x[pop.get_flat_index(index)]; + if (N > 0) { + influences += rate.influences[i].factor * x[pop.get_flat_index(index)] / N; + } } - return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; + return rate.factor * x[source] * influences; } } diff --git a/cpp/tests/test_smm_model.cpp b/cpp/tests/test_smm_model.cpp index 39fe57451e..85a4a4b85c 100644 --- a/cpp/tests/test_smm_model.cpp +++ b/cpp/tests/test_smm_model.cpp @@ -23,7 +23,9 @@ #include #include #include +#include "memilio/epidemiology/age_group.h" #include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/logging.h" #include "smm/model.h" #include "smm/parameters.h" #include "smm/simulation.h" @@ -45,6 +47,14 @@ enum class InfectionState }; +enum class Infections +{ + S, + I, + R, + Count +}; + TEST(TestSMM, evaluateAdoptionRate) { //Test whether the adoption rates are evaluated correctly. @@ -80,31 +90,80 @@ TEST(TestSMM, evaluateAdoptionRate) EXPECT_EQ(model.evaluate(adoption_rates[1], model.populations.get_compartments()), 2.); } +TEST(TestSMM, evaluateinterregionalAdoptionRate) +{ + using Model = mio::smm::Model<2, Infections, 2>; + using Age = mio::AgeGroup; + + std::vector> adoption_rates; + //Second-order adoption + adoption_rates.push_back({Infections::S, + Infections::I, + mio::regions::Region(0), + 0.1, + {{Infections::I, 0.1, mio::regions::Region(0), {Age(1)}}, + {Infections::I, 0.2, mio::regions::Region(1), {Age(1)}}}, + Age(0)}); + //First-order adoption + adoption_rates.push_back({Infections::I, Infections::R, mio::regions::Region(0), 0.2, {}, {Age(1)}}); + Model model; + + model.populations[{mio::regions::Region(0), Infections::S, Age(0)}] = 50; + model.populations[{mio::regions::Region(0), Infections::I, Age(0)}] = 10; + model.populations[{mio::regions::Region(0), Infections::R, Age(0)}] = 0; + + model.populations[{mio::regions::Region(0), Infections::S, Age(1)}] = 100; + model.populations[{mio::regions::Region(0), Infections::I, Age(1)}] = 20; + model.populations[{mio::regions::Region(0), Infections::R, Age(1)}] = 0; + + model.populations[{mio::regions::Region(1), Infections::S, Age(0)}] = 40; + model.populations[{mio::regions::Region(1), Infections::I, Age(0)}] = 80; + model.populations[{mio::regions::Region(1), Infections::R, Age(0)}] = 0; + + model.populations[{mio::regions::Region(1), Infections::S, Age(1)}] = 80; + model.populations[{mio::regions::Region(1), Infections::I, Age(1)}] = 16; + model.populations[{mio::regions::Region(1), Infections::R, Age(1)}] = 0; + + EXPECT_FLOAT_EQ(model.evaluate(adoption_rates[0], model.populations.get_compartments()), + 0.1 * 50. * (0.1 * 20. * 1. / 120. + 0.2 * 16 * 1. / 96.)); + EXPECT_FLOAT_EQ(model.evaluate(adoption_rates[1], model.populations.get_compartments()), 4.); +} + TEST(TestSMM, evaluateTransitionRate) { //Same test as 'evaluateAdoptionRate' only for spatial transition rates. //Transition rates are given by: rate.factor * N(rate.status, rate.from) - using Model = mio::smm::Model<2, InfectionState>; + using Model = mio::smm::Model<2, InfectionState, 2>; Model model; //Initialize model populations - model.populations[{mio::regions::Region(0), InfectionState::S}] = 50; - model.populations[{mio::regions::Region(0), InfectionState::E}] = 10; - model.populations[{mio::regions::Region(0), InfectionState::C}] = 5; - model.populations[{mio::regions::Region(0), InfectionState::I}] = 0; - model.populations[{mio::regions::Region(0), InfectionState::R}] = 0; - model.populations[{mio::regions::Region(0), InfectionState::D}] = 0; - - model.populations[{mio::regions::Region(1), InfectionState::S}] = 55; - model.populations[{mio::regions::Region(1), InfectionState::E}] = 10; - model.populations[{mio::regions::Region(1), InfectionState::C}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::I}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::R}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::D}] = 0; + model.populations[{mio::regions::Region(0), InfectionState::S, mio::AgeGroup(0)}] = 50; + model.populations[{mio::regions::Region(0), InfectionState::E, mio::AgeGroup(0)}] = 10; + model.populations[{mio::regions::Region(0), InfectionState::C, mio::AgeGroup(0)}] = 5; + model.populations[{mio::regions::Region(0), InfectionState::I, mio::AgeGroup(0)}] = 0; + model.populations[{mio::regions::Region(0), InfectionState::R, mio::AgeGroup(0)}] = 0; + model.populations[{mio::regions::Region(0), InfectionState::D, mio::AgeGroup(0)}] = 0; + + model.populations[{mio::regions::Region(1), InfectionState::S, mio::AgeGroup(0)}] = 55; + model.populations[{mio::regions::Region(1), InfectionState::E, mio::AgeGroup(0)}] = 10; + model.populations[{mio::regions::Region(1), InfectionState::C, mio::AgeGroup(0)}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::I, mio::AgeGroup(0)}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::R, mio::AgeGroup(0)}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::D, mio::AgeGroup(0)}] = 0; //Set transition rates - std::vector> transition_rates; - transition_rates.push_back({InfectionState::S, mio::regions::Region(0), mio::regions::Region(1), 0.01}); - transition_rates.push_back({InfectionState::E, mio::regions::Region(1), mio::regions::Region(0), 0.1}); + std::vector> transition_rates; + transition_rates.push_back({InfectionState::S, + mio::regions::Region(0), + mio::regions::Region(1), + 0.01, + {mio::AgeGroup(0)}, + {mio::AgeGroup(0)}}); + transition_rates.push_back({InfectionState::E, + mio::regions::Region(1), + mio::regions::Region(0), + 0.1, + {mio::AgeGroup(0)}, + {mio::AgeGroup(0)}}); EXPECT_EQ(model.evaluate(transition_rates[0], model.populations.get_compartments()), 0.5); EXPECT_EQ(model.evaluate(transition_rates[1], model.populations.get_compartments()), 1.); From dcda987cdb2d5728ad65675893d1cf0110af4389 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:48:42 +0200 Subject: [PATCH 062/169] [skip ci] CHG: Update Header --- cpp/examples/smm.cpp | 2 +- cpp/memilio/epidemiology/adoption_rate.h | 3 ++- cpp/tests/test_smm_model.cpp | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index cb9c0f62fc..cf457e537e 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) * -* Authors: Julia Bicker, René Schmieding +* Authors: Julia Bicker, René Schmieding, Kilian Volmer * * Contact: Martin J. Kuehn * diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index 2917d841d3..86addd7544 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2025 MEmilio * -* Authors: René Schmieding, Julia Bicker +* Authors: René Schmieding, Julia Bicker, Kilian Volmer * * Contact: Martin J. Kuehn * @@ -26,6 +26,7 @@ #include #include #include + namespace mio { diff --git a/cpp/tests/test_smm_model.cpp b/cpp/tests/test_smm_model.cpp index 85a4a4b85c..555d660cc0 100644 --- a/cpp/tests/test_smm_model.cpp +++ b/cpp/tests/test_smm_model.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) * -* Authors: Julia Bicker +* Authors: Julia Bicker, Kilian Volmer * * Contact: Martin J. Kuehn * From c58b74ae408e89e7d501b4d90bb11fc4163537b6 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:20:06 +0200 Subject: [PATCH 063/169] [skip ci] Update copyright information --- cpp/examples/d_abm.cpp | 2 +- cpp/examples/graph_abm.cpp | 2 +- cpp/examples/smm.cpp | 2 +- cpp/models/d_abm/model.cpp | 2 +- cpp/models/d_abm/model.h | 2 +- cpp/models/d_abm/parameters.h | 2 +- cpp/models/d_abm/quad_well.h | 2 +- cpp/models/d_abm/simulation.cpp | 2 +- cpp/models/d_abm/simulation.h | 2 +- cpp/models/d_abm/single_well.h | 2 +- cpp/models/graph_abm/graph_abm_mobility.cpp | 2 +- cpp/models/graph_abm/graph_abm_mobility.h | 2 +- cpp/models/graph_abm/graph_abmodel.h | 2 +- cpp/models/hybrid/temporal_hybrid_model.cpp | 2 +- cpp/models/hybrid/temporal_hybrid_model.h | 2 +- cpp/models/smm/model.cpp | 2 +- cpp/models/smm/model.h | 2 +- cpp/models/smm/parameters.h | 2 +- cpp/models/smm/simulation.h | 2 +- cpp/tests/test_d_abm_model.cpp | 2 +- cpp/tests/test_smm_model.cpp | 2 +- pycode/memilio-epidata/memilio/epidata/getJHData.py | 2 +- .../memilio/generation/template/template_cpp.txt | 2 +- .../memilio/generation/template/template_py.txt | 2 +- .../memilio/generation_test/test_data/test_oseir.cpp.txt | 2 +- .../memilio/generation_test/test_data/test_oseir.py.txt | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cpp/examples/d_abm.cpp b/cpp/examples/d_abm.cpp index 09ffa04d5c..4f91f65de0 100644 --- a/cpp/examples/d_abm.cpp +++ b/cpp/examples/d_abm.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/examples/graph_abm.cpp b/cpp/examples/graph_abm.cpp index 3e7443fbc9..dedc28b60e 100644 --- a/cpp/examples/graph_abm.cpp +++ b/cpp/examples/graph_abm.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 MEmilio +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index 9eae9129a4..cf13a0c7be 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker, René Schmieding, Kilian Volmer * diff --git a/cpp/models/d_abm/model.cpp b/cpp/models/d_abm/model.cpp index f52bd1c298..528fa1a4f4 100644 --- a/cpp/models/d_abm/model.cpp +++ b/cpp/models/d_abm/model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding * diff --git a/cpp/models/d_abm/model.h b/cpp/models/d_abm/model.h index b0983c5496..5b858a4efa 100644 --- a/cpp/models/d_abm/model.h +++ b/cpp/models/d_abm/model.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/parameters.h b/cpp/models/d_abm/parameters.h index bd02befd9b..5b16eb433f 100644 --- a/cpp/models/d_abm/parameters.h +++ b/cpp/models/d_abm/parameters.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/quad_well.h b/cpp/models/d_abm/quad_well.h index 305617304e..ef1d0e021e 100644 --- a/cpp/models/d_abm/quad_well.h +++ b/cpp/models/d_abm/quad_well.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/simulation.cpp b/cpp/models/d_abm/simulation.cpp index 3b538490b8..3915b84300 100644 --- a/cpp/models/d_abm/simulation.cpp +++ b/cpp/models/d_abm/simulation.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 MEmilio +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/simulation.h b/cpp/models/d_abm/simulation.h index 54ccad8b65..e20f40f595 100644 --- a/cpp/models/d_abm/simulation.h +++ b/cpp/models/d_abm/simulation.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/single_well.h b/cpp/models/d_abm/single_well.h index 62ef7671c6..7bd8cb82e1 100644 --- a/cpp/models/d_abm/single_well.h +++ b/cpp/models/d_abm/single_well.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/models/graph_abm/graph_abm_mobility.cpp b/cpp/models/graph_abm/graph_abm_mobility.cpp index af3fae0c89..c8482f2e48 100644 --- a/cpp/models/graph_abm/graph_abm_mobility.cpp +++ b/cpp/models/graph_abm/graph_abm_mobility.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 MEmilio +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/models/graph_abm/graph_abm_mobility.h b/cpp/models/graph_abm/graph_abm_mobility.h index 2d7a749edf..1af88d311a 100644 --- a/cpp/models/graph_abm/graph_abm_mobility.h +++ b/cpp/models/graph_abm/graph_abm_mobility.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 MEmilio +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/models/graph_abm/graph_abmodel.h b/cpp/models/graph_abm/graph_abmodel.h index 0be1a7f222..040cd8c2da 100644 --- a/cpp/models/graph_abm/graph_abmodel.h +++ b/cpp/models/graph_abm/graph_abmodel.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 MEmilio +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/models/hybrid/temporal_hybrid_model.cpp b/cpp/models/hybrid/temporal_hybrid_model.cpp index f356bd2321..337b8da3de 100644 --- a/cpp/models/hybrid/temporal_hybrid_model.cpp +++ b/cpp/models/hybrid/temporal_hybrid_model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/models/hybrid/temporal_hybrid_model.h b/cpp/models/hybrid/temporal_hybrid_model.h index 25ea35d8d0..3ba096c115 100644 --- a/cpp/models/hybrid/temporal_hybrid_model.h +++ b/cpp/models/hybrid/temporal_hybrid_model.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/models/smm/model.cpp b/cpp/models/smm/model.cpp index af0d58eb73..d613003ac2 100644 --- a/cpp/models/smm/model.cpp +++ b/cpp/models/smm/model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding * diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 7d6583bd08..b5942bc251 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker, Kilian Volmer * diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index c7759d8953..fe27c35526 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 8364236f28..8fb1423571 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/tests/test_d_abm_model.cpp b/cpp/tests/test_d_abm_model.cpp index c31b4aa5e4..79397d8e65 100644 --- a/cpp/tests/test_d_abm_model.cpp +++ b/cpp/tests/test_d_abm_model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/tests/test_smm_model.cpp b/cpp/tests/test_smm_model.cpp index 3d8e07f2db..2351e33ef6 100644 --- a/cpp/tests/test_smm_model.cpp +++ b/cpp/tests/test_smm_model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) +* Copyright (C) 2020-2025 MEmilio * * Authors: Julia Bicker, Kilian Volmer * diff --git a/pycode/memilio-epidata/memilio/epidata/getJHData.py b/pycode/memilio-epidata/memilio/epidata/getJHData.py index 19ec2b67f9..dd0a3bb873 100644 --- a/pycode/memilio-epidata/memilio/epidata/getJHData.py +++ b/pycode/memilio-epidata/memilio/epidata/getJHData.py @@ -1,5 +1,5 @@ ############################################################################# -# Copyright (C) 2020-2024 MEmilio +# Copyright (C) 2020-2025 MEmilio # # Authors: Kathrin Rack # diff --git a/pycode/memilio-generation/memilio/generation/template/template_cpp.txt b/pycode/memilio-generation/memilio/generation/template/template_cpp.txt index 28d9f80042..e33668d1ee 100644 --- a/pycode/memilio-generation/memilio/generation/template/template_cpp.txt +++ b/pycode/memilio-generation/memilio/generation/template/template_cpp.txt @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 MEmilio +* Copyright (C) 2020-2025 MEmilio * * Authors: Martin Siggel, Daniel Abele, Martin J. Kuehn, Jan Kleinert * diff --git a/pycode/memilio-generation/memilio/generation/template/template_py.txt b/pycode/memilio-generation/memilio/generation/template/template_py.txt index 2ae2f78e5a..d1cc9710c7 100644 --- a/pycode/memilio-generation/memilio/generation/template/template_py.txt +++ b/pycode/memilio-generation/memilio/generation/template/template_py.txt @@ -1,5 +1,5 @@ ############################################################################# -# Copyright (C) 2020-2024 MEmilio +# Copyright (C) 2020-2025 MEmilio # # Authors: Daniel Abele # diff --git a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt index b419020ba3..edacafb623 100644 --- a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt +++ b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2024 MEmilio +* Copyright (C) 2020-2025 MEmilio * * Authors: Martin Siggel, Daniel Abele, Martin J. Kuehn, Jan Kleinert * diff --git a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt index 3f289a1251..1fd1861e46 100644 --- a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt +++ b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt @@ -1,5 +1,5 @@ ############################################################################# -# Copyright (C) 2020-2024 MEmilio +# Copyright (C) 2020-2025 MEmilio # # Authors: Daniel Abele # From 17cd0b9caf021035f34211ad5ccc5166f975ebaa Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:24:28 +0200 Subject: [PATCH 064/169] CHG: REplace FLOAT test by double test --- cpp/tests/test_smm_model.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/tests/test_smm_model.cpp b/cpp/tests/test_smm_model.cpp index 2351e33ef6..9e7be226f4 100644 --- a/cpp/tests/test_smm_model.cpp +++ b/cpp/tests/test_smm_model.cpp @@ -124,9 +124,9 @@ TEST(TestSMM, evaluateinterregionalAdoptionRate) model.populations[{mio::regions::Region(1), Infections::I, Age(1)}] = 16; model.populations[{mio::regions::Region(1), Infections::R, Age(1)}] = 0; - EXPECT_FLOAT_EQ(model.evaluate(adoption_rates[0], model.populations.get_compartments()), - 0.1 * 50. * (0.1 * 20. * 1. / 120. + 0.2 * 16 * 1. / 96.)); - EXPECT_FLOAT_EQ(model.evaluate(adoption_rates[1], model.populations.get_compartments()), 4.); + EXPECT_DOUBLE_EQ(model.evaluate(adoption_rates[0], model.populations.get_compartments()), + 0.1 * 50. * (0.1 * 20. * 1. / 120. + 0.2 * 16 * 1. / 96.)); + EXPECT_DOUBLE_EQ(model.evaluate(adoption_rates[1], model.populations.get_compartments()), 4.); } TEST(TestSMM, evaluateTransitionRate) From 78fb40794e96fd287c3d5c598d3aa765a1b4edc0 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:26:57 +0200 Subject: [PATCH 065/169] FIX: Add FPs --- cpp/examples/asymmetric_graph.cpp | 25 ++++--- .../metapopulation_mobility_asymmetric.h | 69 ++++++++++--------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index cb8dfccf77..35231d3746 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -17,6 +17,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "memilio/config.h" #include "memilio/geography/locations.h" #include "memilio/geography/tree.h" #include "memilio/mobility/graph_simulation.h" @@ -47,7 +48,7 @@ int main(int /*argc*/, char** /*argv*/) //total compartment sizes double num_total = 10000, num_exp = 200, num_ins = 50, num_rec = 0; - using Model = mio::smm::Model<1, InfectionState>; + using Model = mio::smm::Model; Model model; auto home = mio::regions::Region(0); @@ -57,11 +58,11 @@ int main(int /*argc*/, char** /*argv*/) model.populations[{home, InfectionState::R}] = num_rec; model.populations[{home, InfectionState::S}] = num_total - num_exp - num_ins - num_rec; - std::vector> adoption_rates; + std::vector> adoption_rates; adoption_rates.push_back({InfectionState::E, InfectionState::I, home, 0.2, {}}); adoption_rates.push_back({InfectionState::I, InfectionState::R, home, 0.333, {}}); adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.5}}}); - model.parameters.get>() = adoption_rates; + model.parameters.get>() = adoption_rates; auto model2 = model; model2.populations[{home, InfectionState::I}] = 9000; @@ -69,16 +70,18 @@ int main(int /*argc*/, char** /*argv*/) auto rng = mio::RandomNumberGenerator(); - mio::Graph>, mio::MobilityEdgeDirected> graph; - graph.add_node(0, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), - mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); - graph.add_node(1, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), - mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); + mio::Graph>, + mio::MobilityEdgeDirected> + graph; + graph.add_node(0, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), + mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); + graph.add_node(1, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), + mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); size_t num_nodes = 2000; for (size_t i = 2; i < num_nodes; i++) { auto local_model = model; - graph.add_node(i, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), - mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), local_model, t0); + graph.add_node(i, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), + mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), local_model, t0); } std::vector> interesting_indices; @@ -89,7 +92,7 @@ int main(int /*argc*/, char** /*argv*/) auto distribution = mio::DiscreteDistributionInPlace(); - std::vector uniform_vector(num_nodes, 1.0); + std::vector uniform_vector(num_nodes, 1.0); mio::log_info("Nodes generated"); for (size_t i = 0; i < 3 * num_nodes; ++i) { auto to = distribution(rng, {uniform_vector}); diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 7e7d734499..21154e7d74 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -36,14 +36,14 @@ namespace mio { -template -class LocationNode : public SimulationNode +template +class LocationNode : public SimulationNode { - using Base = SimulationNode; + using Base = SimulationNode; public: template ::value, void>> - LocationNode(double latitude, double longitude, Args&&... args) + LocationNode(FP latitude, FP longitude, Args&&... args) : Base(std::forward(args)...) , m_location(latitude, longitude) , regional_neighbor_indices{} @@ -55,17 +55,17 @@ class LocationNode : public SimulationNode return m_location; } - void set_location(double latitude, double longitude) + void set_location(FP latitude, FP longitude) { m_location = mio::geo::GeographicalLocation(latitude, longitude); } - double get_longitude() const + FP get_longitude() const { return m_location.get_longitude(); } - double get_latitude() const + FP get_latitude() const { return m_location.get_latitude(); } @@ -84,8 +84,8 @@ class LocationNode : public SimulationNode * node functor for mobility-based simulation. * @see SimulationNode::advance */ -template -void advance_model(double t, double dt, LocationNode& node) +template +void advance_model(FP t, FP dt, LocationNode& node) { node.advance(t, dt); } @@ -93,6 +93,7 @@ void advance_model(double t, double dt, LocationNode& node) /** * represents the mobility between two nodes. */ +template class MobilityEdgeDirected { public: @@ -139,14 +140,15 @@ class MobilityEdgeDirected * @param node_to node that people changed to */ template - void apply_mobility(double& t, double& num_moving, LocationNode& node_from, LocationNode& node_to); + void apply_mobility(const FP t, const FP num_moving, LocationNode& node_from, + LocationNode& node_to); private: // MobilityParametersTimed m_parameters; - TimeSeries m_mobility_results; + TimeSeries m_mobility_results; std::vector> m_saved_compartment_indices; - void add_mobility_result_time_point(const double t, std::vector& travellers) + void add_mobility_result_time_point(const FP t, std::vector& travellers) { const size_t save_indices_size = this->m_saved_compartment_indices.size(); if (save_indices_size > 0) { @@ -157,13 +159,13 @@ class MobilityEdgeDirected std::transform(this->m_saved_compartment_indices.begin(), this->m_saved_compartment_indices.end(), condensed_values.data(), [&travellers](const auto& indices) { return std::accumulate(indices.begin(), indices.end(), 0.0, - [&travellers](double sum, auto i) { + [&travellers](FP sum, auto i) { return sum + travellers[i]; }); }); // the last value is the sum of commuters - condensed_values[save_indices_size] = std::accumulate(travellers.begin(), travellers.end(), 0); + condensed_values[save_indices_size] = std::accumulate(travellers.begin(), travellers.end(), 0.0); // Move the condensed values to the m_mobility_results time series m_mobility_results.add_time_point(t, std::move(condensed_values)); @@ -171,9 +173,10 @@ class MobilityEdgeDirected } }; +template template -void MobilityEdgeDirected::apply_mobility(double& t, double& num_moving, LocationNode& node_from, - LocationNode& node_to) +void MobilityEdgeDirected::apply_mobility(const FP t, const FP num_moving, LocationNode& node_from, + LocationNode& node_to) { // auto next_event = m_parameters.process_next_event(); // auto num_moving = next_event.number; @@ -190,9 +193,9 @@ void MobilityEdgeDirected::apply_mobility(double& t, double& num_moving, Locatio add_mobility_result_time_point(t, travellers); } -template -void apply_timed_mobility(double t, double num_moving, MobilityEdgeDirected& edge, LocationNode& node_from, - LocationNode& node_to) +template +void apply_timed_mobility(const FP t, const FP num_moving, MobilityEdgeDirected& edge, + LocationNode& node_from, LocationNode& node_to) { edge.apply_mobility(t, num_moving, node_from, node_to); } @@ -208,25 +211,25 @@ void apply_timed_mobility(double t, double num_moving, MobilityEdgeDirected& edg * @{ */ template -AsymmetricGraphSimulation, MobilityEdgeDirected>> -make_mobility_sim(FP t0, FP dt, const Graph, MobilityEdgeDirected>& graph) +AsymmetricGraphSimulation, MobilityEdgeDirected>> +make_mobility_sim(FP t0, FP dt, const Graph, MobilityEdgeDirected>& graph) { - using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, - void (*)(FP, FP, mio::MobilityEdgeDirected&, mio::LocationNode&, - mio::LocationNode&), - void (*)(FP, FP, mio::LocationNode&)>; - return GraphSim(t0, dt, graph, &advance_model, &apply_timed_mobility); + using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, + void (*)(FP, FP, mio::MobilityEdgeDirected&, + mio::LocationNode&, mio::LocationNode&), + void (*)(FP, FP, mio::LocationNode&)>; + return GraphSim(t0, dt, graph, &advance_model, &apply_timed_mobility); } template -AsymmetricGraphSimulation, MobilityEdgeDirected>> -make_mobility_sim(FP t0, FP dt, Graph, MobilityEdgeDirected>&& graph) +AsymmetricGraphSimulation, MobilityEdgeDirected>> +make_mobility_sim(FP t0, FP dt, Graph, MobilityEdgeDirected>&& graph) { - using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, - void (*)(FP, FP, mio::MobilityEdgeDirected&, mio::LocationNode&, - mio::LocationNode&), - void (*)(FP, FP, mio::LocationNode&)>; - return GraphSim(t0, dt, std::move(graph), &advance_model, &apply_timed_mobility); + using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, + void (*)(FP, FP, mio::MobilityEdgeDirected&, + mio::LocationNode&, mio::LocationNode&), + void (*)(FP, FP, mio::LocationNode&)>; + return GraphSim(t0, dt, std::move(graph), &advance_model, &apply_timed_mobility); } /** @} */ From f37fbe1ae453a31db174709da3edb12c78916f7e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:49:39 +0200 Subject: [PATCH 066/169] Revert "[skip ci] Update copyright information" This reverts commit c58b74ae408e89e7d501b4d90bb11fc4163537b6. --- cpp/examples/d_abm.cpp | 2 +- cpp/examples/graph_abm.cpp | 2 +- cpp/examples/smm.cpp | 2 +- cpp/models/d_abm/model.cpp | 2 +- cpp/models/d_abm/model.h | 2 +- cpp/models/d_abm/parameters.h | 2 +- cpp/models/d_abm/quad_well.h | 2 +- cpp/models/d_abm/simulation.cpp | 2 +- cpp/models/d_abm/simulation.h | 2 +- cpp/models/d_abm/single_well.h | 2 +- cpp/models/graph_abm/graph_abm_mobility.cpp | 2 +- cpp/models/graph_abm/graph_abm_mobility.h | 2 +- cpp/models/graph_abm/graph_abmodel.h | 2 +- cpp/models/hybrid/temporal_hybrid_model.cpp | 2 +- cpp/models/hybrid/temporal_hybrid_model.h | 2 +- cpp/models/smm/model.cpp | 2 +- cpp/models/smm/model.h | 2 +- cpp/models/smm/parameters.h | 2 +- cpp/models/smm/simulation.h | 2 +- cpp/tests/test_d_abm_model.cpp | 2 +- cpp/tests/test_smm_model.cpp | 2 +- pycode/memilio-epidata/memilio/epidata/getJHData.py | 2 +- .../memilio/generation/template/template_cpp.txt | 2 +- .../memilio/generation/template/template_py.txt | 2 +- .../memilio/generation_test/test_data/test_oseir.cpp.txt | 2 +- .../memilio/generation_test/test_data/test_oseir.py.txt | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cpp/examples/d_abm.cpp b/cpp/examples/d_abm.cpp index 4f91f65de0..09ffa04d5c 100644 --- a/cpp/examples/d_abm.cpp +++ b/cpp/examples/d_abm.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/examples/graph_abm.cpp b/cpp/examples/graph_abm.cpp index dedc28b60e..3e7443fbc9 100644 --- a/cpp/examples/graph_abm.cpp +++ b/cpp/examples/graph_abm.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index cf13a0c7be..9eae9129a4 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) * * Authors: Julia Bicker, René Schmieding, Kilian Volmer * diff --git a/cpp/models/d_abm/model.cpp b/cpp/models/d_abm/model.cpp index 528fa1a4f4..f52bd1c298 100644 --- a/cpp/models/d_abm/model.cpp +++ b/cpp/models/d_abm/model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding * diff --git a/cpp/models/d_abm/model.h b/cpp/models/d_abm/model.h index 5b858a4efa..b0983c5496 100644 --- a/cpp/models/d_abm/model.h +++ b/cpp/models/d_abm/model.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/parameters.h b/cpp/models/d_abm/parameters.h index 5b16eb433f..bd02befd9b 100644 --- a/cpp/models/d_abm/parameters.h +++ b/cpp/models/d_abm/parameters.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/quad_well.h b/cpp/models/d_abm/quad_well.h index ef1d0e021e..305617304e 100644 --- a/cpp/models/d_abm/quad_well.h +++ b/cpp/models/d_abm/quad_well.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/simulation.cpp b/cpp/models/d_abm/simulation.cpp index 3915b84300..3b538490b8 100644 --- a/cpp/models/d_abm/simulation.cpp +++ b/cpp/models/d_abm/simulation.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 MEmilio * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/simulation.h b/cpp/models/d_abm/simulation.h index e20f40f595..54ccad8b65 100644 --- a/cpp/models/d_abm/simulation.h +++ b/cpp/models/d_abm/simulation.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/d_abm/single_well.h b/cpp/models/d_abm/single_well.h index 7bd8cb82e1..62ef7671c6 100644 --- a/cpp/models/d_abm/single_well.h +++ b/cpp/models/d_abm/single_well.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/models/graph_abm/graph_abm_mobility.cpp b/cpp/models/graph_abm/graph_abm_mobility.cpp index c8482f2e48..af3fae0c89 100644 --- a/cpp/models/graph_abm/graph_abm_mobility.cpp +++ b/cpp/models/graph_abm/graph_abm_mobility.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/models/graph_abm/graph_abm_mobility.h b/cpp/models/graph_abm/graph_abm_mobility.h index 1af88d311a..2d7a749edf 100644 --- a/cpp/models/graph_abm/graph_abm_mobility.h +++ b/cpp/models/graph_abm/graph_abm_mobility.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/models/graph_abm/graph_abmodel.h b/cpp/models/graph_abm/graph_abmodel.h index 040cd8c2da..0be1a7f222 100644 --- a/cpp/models/graph_abm/graph_abmodel.h +++ b/cpp/models/graph_abm/graph_abmodel.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 MEmilio * * Authors: Julia Bicker * diff --git a/cpp/models/hybrid/temporal_hybrid_model.cpp b/cpp/models/hybrid/temporal_hybrid_model.cpp index 337b8da3de..f356bd2321 100644 --- a/cpp/models/hybrid/temporal_hybrid_model.cpp +++ b/cpp/models/hybrid/temporal_hybrid_model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/models/hybrid/temporal_hybrid_model.h b/cpp/models/hybrid/temporal_hybrid_model.h index 3ba096c115..25ea35d8d0 100644 --- a/cpp/models/hybrid/temporal_hybrid_model.h +++ b/cpp/models/hybrid/temporal_hybrid_model.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: Julia Bicker, René Schmieding * diff --git a/cpp/models/smm/model.cpp b/cpp/models/smm/model.cpp index d613003ac2..af0d58eb73 100644 --- a/cpp/models/smm/model.cpp +++ b/cpp/models/smm/model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding * diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index b5942bc251..7d6583bd08 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding, Julia Bicker, Kilian Volmer * diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index fe27c35526..c7759d8953 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 8fb1423571..8364236f28 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2025 German Aerospace Center (DLR-SC) * * Authors: René Schmieding, Julia Bicker * diff --git a/cpp/tests/test_d_abm_model.cpp b/cpp/tests/test_d_abm_model.cpp index 79397d8e65..c31b4aa5e4 100644 --- a/cpp/tests/test_d_abm_model.cpp +++ b/cpp/tests/test_d_abm_model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) * * Authors: Julia Bicker * diff --git a/cpp/tests/test_smm_model.cpp b/cpp/tests/test_smm_model.cpp index 9e7be226f4..6e8901e37e 100644 --- a/cpp/tests/test_smm_model.cpp +++ b/cpp/tests/test_smm_model.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 German Aerospace Center (DLR-SC) * * Authors: Julia Bicker, Kilian Volmer * diff --git a/pycode/memilio-epidata/memilio/epidata/getJHData.py b/pycode/memilio-epidata/memilio/epidata/getJHData.py index dd0a3bb873..19ec2b67f9 100644 --- a/pycode/memilio-epidata/memilio/epidata/getJHData.py +++ b/pycode/memilio-epidata/memilio/epidata/getJHData.py @@ -1,5 +1,5 @@ ############################################################################# -# Copyright (C) 2020-2025 MEmilio +# Copyright (C) 2020-2024 MEmilio # # Authors: Kathrin Rack # diff --git a/pycode/memilio-generation/memilio/generation/template/template_cpp.txt b/pycode/memilio-generation/memilio/generation/template/template_cpp.txt index e33668d1ee..28d9f80042 100644 --- a/pycode/memilio-generation/memilio/generation/template/template_cpp.txt +++ b/pycode/memilio-generation/memilio/generation/template/template_cpp.txt @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 MEmilio * * Authors: Martin Siggel, Daniel Abele, Martin J. Kuehn, Jan Kleinert * diff --git a/pycode/memilio-generation/memilio/generation/template/template_py.txt b/pycode/memilio-generation/memilio/generation/template/template_py.txt index d1cc9710c7..2ae2f78e5a 100644 --- a/pycode/memilio-generation/memilio/generation/template/template_py.txt +++ b/pycode/memilio-generation/memilio/generation/template/template_py.txt @@ -1,5 +1,5 @@ ############################################################################# -# Copyright (C) 2020-2025 MEmilio +# Copyright (C) 2020-2024 MEmilio # # Authors: Daniel Abele # diff --git a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt index edacafb623..b419020ba3 100644 --- a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt +++ b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.cpp.txt @@ -1,5 +1,5 @@ /* -* Copyright (C) 2020-2025 MEmilio +* Copyright (C) 2020-2024 MEmilio * * Authors: Martin Siggel, Daniel Abele, Martin J. Kuehn, Jan Kleinert * diff --git a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt index 1fd1861e46..3f289a1251 100644 --- a/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt +++ b/pycode/memilio-generation/memilio/generation_test/test_data/test_oseir.py.txt @@ -1,5 +1,5 @@ ############################################################################# -# Copyright (C) 2020-2025 MEmilio +# Copyright (C) 2020-2024 MEmilio # # Authors: Daniel Abele # From 7f69da2b238f6c77619c31ad0854324eb3b9182e Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:19:19 +0200 Subject: [PATCH 067/169] [wip] parameter studies v2 --- cpp/examples/abm_minimal.cpp | 117 ++++----- cpp/memilio/compartments/parameter_studies.h | 246 +++++++++++++++++++ 2 files changed, 297 insertions(+), 66 deletions(-) diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index 63c8b7776b..72ce3871ee 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -20,6 +20,19 @@ #include "abm/simulation.h" #include "abm/common_abm_loggers.h" #include +#include "abm/household.h" +#include "abm/lockdown_rules.h" +#include "abm/model.h" +#include "abm/time.h" +#include "memilio/compartments/parameter_studies.h" +#include "memilio/data/analyze_result.h" +#include "memilio/io/result_io.h" +#include "memilio/utils/abstract_parameter_distribution.h" +#include "memilio/utils/miompi.h" +#include "memilio/utils/time_series.h" + +#include +#include // shim class to make the abm simulation look like an ode simulation class ResultSim : public mio::abm::Simulation @@ -33,9 +46,15 @@ class ResultSim : public mio::abm::Simulation history.log(*this); // set initial results } - void advance(double tmax) + ResultSim(const Model& m, mio::abm::TimePoint t) + : mio::abm::Simulation(t, Model(m)) + { + history.log(*this); // set initial results + } + + void advance(mio::abm::TimePoint tmax) { - mio::abm::Simulation::advance(mio::abm::TimePoint{mio::abm::days(tmax).seconds()}, history); + mio::abm::Simulation::advance(tmax, history); } const mio::TimeSeries& get_result() const @@ -47,46 +66,9 @@ class ResultSim : public mio::abm::Simulation Eigen::Index(mio::abm::InfectionState::Count)}; }; -// graph ode specific function, overload with noop to avoid compiler errors -template -void calculate_mobility_returns(Eigen::Ref::Vector>, const ResultSim&, - Eigen::Ref::Vector>, double, double) -{ -} -#include "memilio/mobility/metapopulation_mobility_instant.h" - -namespace mio -{ - -// overload for ResultSim without mobility -auto make_mobility_sim(double t0, double dt, Graph, MobilityEdge>&& graph) -{ - using GraphT = GraphSimulation, MobilityEdge>, double, double, - void (*)(double, double, mio::MobilityEdge&, mio::SimulationNode&, - mio::SimulationNode&), - void (*)(double, double, mio::SimulationNode&)>; - return GraphT(t0, dt, std::move(graph), - static_cast&)>(&advance_model), - [](double, double, MobilityEdge&, SimulationNode&, SimulationNode&) {}); -} - -} // namespace mio - -#include "abm/household.h" -#include "abm/lockdown_rules.h" -#include "abm/model.h" -#include "abm/time.h" -#include "memilio/compartments/parameter_studies.h" -#include "memilio/data/analyze_result.h" -#include "memilio/io/result_io.h" -#include "memilio/utils/abstract_parameter_distribution.h" -#include "memilio/utils/time_series.h" - -#include -#include - int main() { + mio::mpi::init(); // This is a minimal example with children and adults < 60 year old. // We divided them into 4 different age groups, which are defined as follows: mio::set_log_level(mio::LogLevel::warn); @@ -95,7 +77,6 @@ int main() const auto age_group_5_to_14 = mio::AgeGroup(1); const auto age_group_15_to_34 = mio::AgeGroup(2); const auto age_group_35_to_59 = mio::AgeGroup(3); - // Create the model with 4 age groups. auto model = mio::abm::Model(num_age_groups); // Set same infection parameter for all age groups. For example, the incubation period is log normally distributed with parameters 4 and 1. @@ -111,7 +92,7 @@ int main() model.parameters.check_constraints(); // There are 10 households for each household group. - int n_households = 10; + int n_households = 100; // For more than 1 family households we need families. These are parents and children and randoms (which are distributed like the data we have for these households). auto child = mio::abm::HouseholdMember(num_age_groups); // A child is 50/50% 0-4 or 5-14. @@ -213,40 +194,42 @@ int main() // Set start and end time for the simulation. auto t0 = mio::abm::TimePoint(0); - auto tmax = t0 + mio::abm::days(10); + auto tmax = t0 + mio::abm::days(100); // auto sim = mio::abm::Simulation(t0, std::move(model)); - const size_t num_runs = 10; + const size_t num_runs = 100; // Create a history object to store the time series of the infection states. mio::History historyTimeSeries{ Eigen::Index(mio::abm::InfectionState::Count)}; - mio::ParameterStudy study(model, t0.days(), tmax.days(), num_runs); + mio::ParameterStudy2 study( + model, t0, tmax, mio::abm::TimeSpan(0), num_runs); auto ensemble = study.run( - [](auto&& graph) { - // TODO: some actual sampling so that the percentiles show anything - return graph; + [](auto&& model_, auto t0_, auto) { + // TODO: change the parameters around a bit? + auto sim = ResultSim(model_, t0_); + sim.get_model().get_rng().generate_seeds(); + return sim; }, - [&](auto results_graph, auto&& /* run_idx */) { - auto interpolated_result = mio::interpolate_simulation_result(results_graph); + [](auto&& sim, auto&& /* run_idx */) { + auto interpolated_result = mio::interpolate_simulation_result(sim.get_result()); auto params = std::vector{}; - params.reserve(results_graph.nodes().size()); - std::transform(results_graph.nodes().begin(), results_graph.nodes().end(), std::back_inserter(params), - [](auto&& node) { - return node.property.get_simulation().get_model(); - }); - return std::make_pair(std::move(interpolated_result), std::move(params)); + return std::make_pair(std::move(interpolated_result), sim.get_model()); }); + if (ensemble.size() > 0) { auto ensemble_results = std::vector>>{}; ensemble_results.reserve(ensemble.size()); auto ensemble_params = std::vector>{}; ensemble_params.reserve(ensemble.size()); + int j = 0; + std::cout << "## " << ensemble[0].first.get_value(0).transpose() << "\n"; for (auto&& run : ensemble) { - ensemble_results.emplace_back(std::move(run.first)); - ensemble_params.emplace_back(std::move(run.second)); + std::cout << j++ << " " << run.first.get_last_value().transpose() << "\n"; + ensemble_results.emplace_back(std::vector{std::move(run.first)}); + ensemble_params.emplace_back(std::vector{std::move(run.second)}); } auto ensemble_results_p05 = ensemble_percentile(ensemble_results, 0.05); @@ -274,14 +257,16 @@ int main() // Run the simulation until tmax with the history object. // sim.advance(tmax, historyTimeSeries); - // The results are written into the file "abm_minimal.txt" as a table with 9 columns. - // The first column is Time. The other columns correspond to the number of people with a certain infection state at this Time: - // Time = Time in days, S = Susceptible, E = Exposed, I_NS = InfectedNoSymptoms, I_Sy = InfectedSymptoms, I_Sev = InfectedSevere, - // I_Crit = InfectedCritical, R = Recovered, D = Dead - std::ofstream outfile("abm_minimal.txt"); - std::get<0>(historyTimeSeries.get_log()) - .print_table(outfile, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); - std::cout << "Results written to abm_minimal.txt" << std::endl; + // // The results are written into the file "abm_minimal.txt" as a table with 9 columns. + // // The first column is Time. The other columns correspond to the number of people with a certain infection state at this Time: + // // Time = Time in days, S = Susceptible, E = Exposed, I_NS = InfectedNoSymptoms, I_Sy = InfectedSymptoms, I_Sev = InfectedSevere, + // // I_Crit = InfectedCritical, R = Recovered, D = Dead + // std::ofstream outfile("abm_minimal.txt"); + // std::get<0>(historyTimeSeries.get_log()) + // .print_table(outfile, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); + // std::cout << "Results written to abm_minimal.txt" << std::endl; + + mio::mpi::finalize(); return 0; } diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index 98819b03d3..68dc1865fd 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -23,17 +23,22 @@ #include "memilio/io/binary_serializer.h" #include "memilio/mobility/graph_simulation.h" #include "memilio/utils/logging.h" +#include "memilio/utils/metaprogramming.h" #include "memilio/utils/miompi.h" #include "memilio/utils/random_number_generator.h" #include "memilio/utils/time_series.h" #include "memilio/mobility/metapopulation_mobility_instant.h" #include "memilio/compartments/simulation.h" +#include #include +#include #include #include #include #include +#include +#include namespace mio { @@ -383,6 +388,247 @@ class ParameterStudy RandomNumberGenerator m_rng; }; +/** + * Class that performs multiple simulation runs with randomly sampled parameters. + * Can simulate mobility graphs with one simulation in each node or single simulations. + * @tparam S type of simulation that runs in one node of the graph. + */ +template +class ParameterStudy2 +{ +public: + using Simulation = SimulationType; + using Parameters = ParameterType; + using Time = TimeType; + using Step = StepType; + + // TODO: replacement for "set_params_distributions_normal". Maybe a special ctor for UncertainParameterSet? + + /** + * create study for graph of compartment models. + * @param graph graph of parameters + * @param t0 start time of simulations + * @param tmax end time of simulations + * @param graph_sim_dt time step of graph simulation + * @param num_runs number of runs + */ + ParameterStudy2(const ParameterType& global_parameters, Time t0, Time tmax, Step dt, size_t num_runs) + : m_parameters(global_parameters) + , m_num_runs(num_runs) + , m_t0{t0} + , m_tmax{tmax} + , m_dt(dt) + { + } + + // /** + // * @brief Create study for single compartment model. + // * @param model compartment model with initial values + // * @param t0 start time of simulations + // * @param tmax end time of simulations + // * @param num_runs number of runs in ensemble run + // */ + // template >> + // ParameterStudy2(typename Simulation::Model const& model, Time t0, Time tmax, Step dt, size_t num_runs) + // : ParameterStudy2(model, t0, tmax, dt, num_runs) + // { + // // TODO how is this supposed to work wrt. model? is this just a special case where ParameterType=Model? + // } + + /** + * @brief + * @param sample_simulation A function that accepts ParameterType and returns an instance of SimulationType. + * @param process_simulation_result A function that accepts S + */ + template + std::vector> + run(CreateSimulationFunction&& create_simulation, ProcessSimulationResultFunction&& process_simulation_result) + { + static_assert(std::is_invocable_r_v, + "Incorrect Type for create_simulation."); + static_assert(std::is_invocable_v, + "Incorrect Type for process_simulation_result."); + int num_procs, rank; +#ifdef MEMILIO_ENABLE_MPI + MPI_Comm_size(mpi::get_world(), &num_procs); + MPI_Comm_rank(mpi::get_world(), &rank); +#else + num_procs = 1; + rank = 0; +#endif + + //The ParameterDistributions used for sampling parameters use thread_local_rng() + //So we set our own RNG to be used. + //Assume that sampling uses the thread_local_rng() and isn't multithreaded + m_rng.synchronize(); + thread_local_rng() = m_rng; + + auto run_distribution = distribute_runs(m_num_runs, num_procs); + auto start_run_idx = + std::accumulate(run_distribution.begin(), run_distribution.begin() + size_t(rank), size_t(0)); + auto end_run_idx = start_run_idx + run_distribution[size_t(rank)]; + + std::vector> ensemble_result; + ensemble_result.reserve(m_num_runs); + + for (size_t run_idx = start_run_idx; run_idx < end_run_idx; run_idx++) { + log(LogLevel::info, "ParameterStudies: run {}", run_idx); + + //prepare rng for this run by setting the counter to the right offset + //Add the old counter so that this call of run() produces different results + //from the previous call + auto run_rng_counter = m_rng.get_counter() + rng_totalsequence_counter( + static_cast(run_idx), Counter(0)); + thread_local_rng().set_counter(run_rng_counter); + + //sample + auto sim = create_simulation(m_parameters, m_t0, m_dt); + log(LogLevel::info, "ParameterStudies: Generated {} random numbers.", + (thread_local_rng().get_counter() - run_rng_counter).get()); + + //perform run + sim.advance(m_tmax); + + //handle result and store + ensemble_result.emplace_back(process_simulation_result(std::move(sim), run_idx)); + } + + //Set the counter of our RNG so that future calls of run() produce different parameters. + m_rng.set_counter(m_rng.get_counter() + rng_totalsequence_counter(m_num_runs, Counter(0))); + +#ifdef MEMILIO_ENABLE_MPI + //gather results + if (rank == 0) { + for (int src_rank = 1; src_rank < num_procs; ++src_rank) { + int bytes_size; + MPI_Recv(&bytes_size, 1, MPI_INT, src_rank, 0, mpi::get_world(), MPI_STATUS_IGNORE); + ByteStream bytes(bytes_size); + MPI_Recv(bytes.data(), bytes.data_size(), MPI_BYTE, src_rank, 0, mpi::get_world(), MPI_STATUS_IGNORE); + + auto src_ensemble_results = deserialize_binary(bytes, Tag{}); + if (!src_ensemble_results) { + log_error("Error receiving ensemble results from rank {}.", src_rank); + } + std::copy(src_ensemble_results.value().begin(), src_ensemble_results.value().end(), + std::back_inserter(ensemble_result)); + } + } + else { + auto bytes = serialize_binary(ensemble_result); + auto bytes_size = int(bytes.data_size()); + MPI_Send(&bytes_size, 1, MPI_INT, 0, 0, mpi::get_world()); + MPI_Send(bytes.data(), bytes.data_size(), MPI_BYTE, 0, 0, mpi::get_world()); + ensemble_result.clear(); //only return root process + } +#endif + + return ensemble_result; + } + + /** + * @brief Carry out all simulations in the parameter study. + * Convenience function for a few number of runs, but can use more memory because it stores all runs until the end. + * Unlike the other overload, this function is not MPI-parallel. + * @return vector of SimulationGraph for each run. + */ + template + std::vector run(CreateSimulationFunction&& create_simulation) + { + return run(std::forward(create_simulation), + [](Simulation&& sim, size_t) -> Simulation&& { + return sim; + }); + } + + /** + * @brief returns the number of Monte Carlo runs + */ + size_t get_num_runs() const + { + return m_num_runs; + } + + /** + * @brief returns end point in simulation + */ + Time get_tmax() const + { + return m_tmax; + } + + /** + * @brief returns start point in simulation + */ + Time get_t0() const + { + return m_t0; + } + /** + * Get the input graph that the parameter study is run for. + * Use for graph simulations, use get_model for single node simulations. + * @{ + */ + const Parameters& get_parameters() const + { + return m_parameters; + } + /** @} */ + + RandomNumberGenerator& get_rng() + { + return m_rng; + } + +private: + std::vector distribute_runs(size_t num_runs, int num_procs) + { + assert(num_procs > 0); + //evenly distribute runs + //lower processes do one more run if runs are not evenly distributable + auto num_runs_local = num_runs / num_procs; //integer division! + auto remainder = num_runs % num_procs; + + std::vector run_distribution(num_procs); + std::fill(run_distribution.begin(), run_distribution.begin() + remainder, num_runs_local + 1); + std::fill(run_distribution.begin() + remainder, run_distribution.end(), num_runs_local); + + return run_distribution; + } + +private: + // Stores Graph with the names and ranges of all parameters + ParameterType m_parameters; + + size_t m_num_runs; + + // Start time (should be the same for all simulations) + Time m_t0; + // End time (should be the same for all simulations) + Time m_tmax; + // adaptive time step of the integrator (will be corrected if too large/small) + Step m_dt; + // + RandomNumberGenerator m_rng; +}; + +// //sample parameters and create simulation +// template +// GraphSimulation create_sampled_simulation(auto&& params_graph, +// SampleGraphFunction sample_graph) +// { +// SimulationGraph sim_graph; + +// auto sampled_graph = sample_graph(params_graph); +// for (auto&& node : sampled_graph.nodes()) { +// sim_graph.add_node(node.id, node.property, m_t0, m_dt_integration); +// } +// for (auto&& edge : sampled_graph.edges()) { +// sim_graph.add_edge(edge.start_node_idx, edge.end_node_idx, edge.property); +// } + +// return make_mobility_sim(m_t0, m_dt_graph_sim, std::move(sim_graph)); +// } + } // namespace mio #endif // MIO_COMPARTMENTS_PARAMETER_STUDIES_H From eaaa5c2e52863e76575444dff0de15c3c9a6edee Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:34:29 +0200 Subject: [PATCH 068/169] [wip] add run_idx to create_sim function --- cpp/examples/abm_minimal.cpp | 5 +++-- cpp/memilio/compartments/parameter_studies.h | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index 72ce3871ee..5c90624b8b 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -32,6 +32,7 @@ #include "memilio/utils/time_series.h" #include +#include #include // shim class to make the abm simulation look like an ode simulation @@ -206,10 +207,10 @@ int main() model, t0, tmax, mio::abm::TimeSpan(0), num_runs); auto ensemble = study.run( - [](auto&& model_, auto t0_, auto) { + [](auto&& model_, auto t0_, auto, size_t run_idx) { // TODO: change the parameters around a bit? auto sim = ResultSim(model_, t0_); - sim.get_model().get_rng().generate_seeds(); + sim.get_model().get_rng().set_counter(mio::Counter(run_idx)); return sim; }, [](auto&& sim, auto&& /* run_idx */) { diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index 68dc1865fd..fcbc2ea8db 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -444,7 +444,7 @@ class ParameterStudy2 std::vector> run(CreateSimulationFunction&& create_simulation, ProcessSimulationResultFunction&& process_simulation_result) { - static_assert(std::is_invocable_r_v, + static_assert(std::is_invocable_r_v, "Incorrect Type for create_simulation."); static_assert(std::is_invocable_v, "Incorrect Type for process_simulation_result."); @@ -482,7 +482,7 @@ class ParameterStudy2 thread_local_rng().set_counter(run_rng_counter); //sample - auto sim = create_simulation(m_parameters, m_t0, m_dt); + auto sim = create_simulation(m_parameters, m_t0, m_dt, run_idx); log(LogLevel::info, "ParameterStudies: Generated {} random numbers.", (thread_local_rng().get_counter() - run_rng_counter).get()); From 280afa5824257b630748799bfbb9eb48c0d89fff Mon Sep 17 00:00:00 2001 From: Sascha Korf <51127093+xsaschako@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:03:31 +0200 Subject: [PATCH 069/169] mpi wokrks --- cpp/examples/abm_minimal.cpp | 116 ++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index 5c90624b8b..df6392ec75 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -35,6 +35,8 @@ #include #include +constexpr size_t num_age_groups = 4; + // shim class to make the abm simulation look like an ode simulation class ResultSim : public mio::abm::Simulation { @@ -67,19 +69,23 @@ class ResultSim : public mio::abm::Simulation Eigen::Index(mio::abm::InfectionState::Count)}; }; -int main() +mio::abm::Model make_model(uint32_t run_idx) { - mio::mpi::init(); - // This is a minimal example with children and adults < 60 year old. - // We divided them into 4 different age groups, which are defined as follows: - mio::set_log_level(mio::LogLevel::warn); - size_t num_age_groups = 4; + const auto age_group_0_to_4 = mio::AgeGroup(0); const auto age_group_5_to_14 = mio::AgeGroup(1); const auto age_group_15_to_34 = mio::AgeGroup(2); const auto age_group_35_to_59 = mio::AgeGroup(3); // Create the model with 4 age groups. - auto model = mio::abm::Model(num_age_groups); + auto model = mio::abm::Model(num_age_groups); + std::initializer_list seeds = {14159265, 243141u + run_idx}; + auto rng = mio::RandomNumberGenerator(); + rng.seed(seeds); + auto run_rng_counter = + mio::rng_totalsequence_counter(static_cast(run_idx), mio::Counter(0)); + rng.set_counter(run_rng_counter); + model.get_rng() = rng; + // Set same infection parameter for all age groups. For example, the incubation period is log normally distributed with parameters 4 and 1. model.parameters.get() = mio::ParameterDistributionLogNormal(4., 1.); @@ -164,9 +170,9 @@ int main() for (auto& person : model.get_persons()) { mio::abm::InfectionState infection_state = mio::abm::InfectionState( mio::DiscreteDistribution::get_instance()(mio::thread_local_rng(), infection_distribution)); - auto rng = mio::abm::PersonalRandomNumberGenerator(person); + auto person_rng = mio::abm::PersonalRandomNumberGenerator(person); if (infection_state != mio::abm::InfectionState::Susceptible) { - person.add_new_infection(mio::abm::Infection(rng, mio::abm::VirusVariant::Wildtype, person.get_age(), + person.add_new_infection(mio::abm::Infection(person_rng, mio::abm::VirusVariant::Wildtype, person.get_age(), model.parameters, start_date, infection_state)); } } @@ -193,31 +199,45 @@ int main() auto t_lockdown = mio::abm::TimePoint(0) + mio::abm::days(10); mio::abm::close_social_events(t_lockdown, 0.9, model.parameters); + return model; +} + +int main() +{ + mio::mpi::init(); + // This is a minimal example with children and adults < 60 year old. + // We divided them into 4 different age groups, which are defined as follows: + + mio::set_log_level(mio::LogLevel::warn); + // Set start and end time for the simulation. auto t0 = mio::abm::TimePoint(0); - auto tmax = t0 + mio::abm::days(100); + auto tmax = t0 + mio::abm::days(5); // auto sim = mio::abm::Simulation(t0, std::move(model)); - const size_t num_runs = 100; + const size_t num_runs = 10; // Create a history object to store the time series of the infection states. mio::History historyTimeSeries{ Eigen::Index(mio::abm::InfectionState::Count)}; - mio::ParameterStudy2 study( - model, t0, tmax, mio::abm::TimeSpan(0), num_runs); + mio::ParameterStudy2 study( + 1, t0, tmax, mio::abm::TimeSpan(0), num_runs); auto ensemble = study.run( - [](auto&& model_, auto t0_, auto, size_t run_idx) { + [](auto&&, auto t0_, auto, size_t run_idx) { // TODO: change the parameters around a bit? - auto sim = ResultSim(model_, t0_); - sim.get_model().get_rng().set_counter(mio::Counter(run_idx)); + + auto sim = ResultSim(make_model(run_idx), t0_); return sim; }, - [](auto&& sim, auto&& /* run_idx */) { + [](auto&& sim, auto&& run_idx) { auto interpolated_result = mio::interpolate_simulation_result(sim.get_result()); - + std::string name_run = "abm_minimal_run_" + std::to_string(run_idx) + ".txt"; + std::ofstream outfile_run(name_run); + sim.get_result().print_table(outfile_run, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); + std::cout << "Results written to " << name_run << std::endl; auto params = std::vector{}; - return std::make_pair(std::move(interpolated_result), sim.get_model()); + return std::move(interpolated_result); }); if (ensemble.size() > 0) { @@ -225,35 +245,35 @@ int main() ensemble_results.reserve(ensemble.size()); auto ensemble_params = std::vector>{}; ensemble_params.reserve(ensemble.size()); - int j = 0; - std::cout << "## " << ensemble[0].first.get_value(0).transpose() << "\n"; - for (auto&& run : ensemble) { - std::cout << j++ << " " << run.first.get_last_value().transpose() << "\n"; - ensemble_results.emplace_back(std::vector{std::move(run.first)}); - ensemble_params.emplace_back(std::vector{std::move(run.second)}); - } - - auto ensemble_results_p05 = ensemble_percentile(ensemble_results, 0.05); - auto ensemble_results_p25 = ensemble_percentile(ensemble_results, 0.25); - auto ensemble_results_p50 = ensemble_percentile(ensemble_results, 0.50); - auto ensemble_results_p75 = ensemble_percentile(ensemble_results, 0.75); - auto ensemble_results_p95 = ensemble_percentile(ensemble_results, 0.95); - - const boost::filesystem::path save_dir = "/home/schm_r6/Code/memilio/memilio/results/"; - - if (!save_dir.empty()) { - - mio::unused(save_result(ensemble_results_p05, {0}, num_age_groups, - (save_dir / ("Results_" + std::string("p05") + ".h5")).string())); - mio::unused(save_result(ensemble_results_p25, {0}, num_age_groups, - (save_dir / ("Results_" + std::string("p25") + ".h5")).string())); - mio::unused(save_result(ensemble_results_p50, {0}, num_age_groups, - (save_dir / ("Results_" + std::string("p50") + ".h5")).string())); - mio::unused(save_result(ensemble_results_p75, {0}, num_age_groups, - (save_dir / ("Results_" + std::string("p75") + ".h5")).string())); - mio::unused(save_result(ensemble_results_p95, {0}, num_age_groups, - (save_dir / ("Results_" + std::string("p95") + ".h5")).string())); - } + // int j = 0; + // std::cout << "## " << ensemble[0].first.get_value(0).transpose() << "\n"; + // for (auto&& run : ensemble) { + // std::cout << j++ << " " << run.first.get_last_value().transpose() << "\n"; + // ensemble_results.emplace_back(std::vector{std::move(run.first)}); + // ensemble_params.emplace_back(std::vector{std::move(run.second)}); + // } + + // auto ensemble_results_p05 = ensemble_percentile(ensemble_results, 0.05); + // auto ensemble_results_p25 = ensemble_percentile(ensemble_results, 0.25); + // auto ensemble_results_p50 = ensemble_percentile(ensemble_results, 0.50); + // auto ensemble_results_p75 = ensemble_percentile(ensemble_results, 0.75); + // auto ensemble_results_p95 = ensemble_percentile(ensemble_results, 0.95); + + // const boost::filesystem::path save_dir = "/Users/saschakorf/Nosynch/Arbeit/memilio/cpp/examples/results"; + + // if (!save_dir.empty()) { + + // mio::unused(save_result(ensemble_results_p05, {0}, num_age_groups, + // (save_dir / ("Results_" + std::string("p05") + ".h5")).string())); + // mio::unused(save_result(ensemble_results_p25, {0}, num_age_groups, + // (save_dir / ("Results_" + std::string("p25") + ".h5")).string())); + // mio::unused(save_result(ensemble_results_p50, {0}, num_age_groups, + // (save_dir / ("Results_" + std::string("p50") + ".h5")).string())); + // mio::unused(save_result(ensemble_results_p75, {0}, num_age_groups, + // (save_dir / ("Results_" + std::string("p75") + ".h5")).string())); + // mio::unused(save_result(ensemble_results_p95, {0}, num_age_groups, + // (save_dir / ("Results_" + std::string("p95") + ".h5")).string())); + // } } // Run the simulation until tmax with the history object. // sim.advance(tmax, historyTimeSeries); From 59708a8fc268a6acf35c35095401d304388585a9 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:39:38 +0200 Subject: [PATCH 070/169] CHG: Add CSV import for nodes, edges and exchanges --- cpp/examples/asymmetric_graph.cpp | 61 +- .../metapopulation_mobility_asymmetric.h | 5 + cpp/thirdparty/csv.h | 1371 +++++++++++++++++ 3 files changed, 1403 insertions(+), 34 deletions(-) create mode 100644 cpp/thirdparty/csv.h diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 35231d3746..2c7292994c 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -23,11 +23,13 @@ #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" +#include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" #include "memilio/utils/parameter_distributions.h" #include "memilio/utils/random_number_generator.h" #include "smm/simulation.h" #include "smm/parameters.h" +#include "thirdparty/csv.h" #include enum class InfectionState @@ -50,7 +52,6 @@ int main(int /*argc*/, char** /*argv*/) using Model = mio::smm::Model; Model model; - auto home = mio::regions::Region(0); model.populations[{home, InfectionState::E}] = num_exp; @@ -64,39 +65,33 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.5}}}); model.parameters.get>() = adoption_rates; - auto model2 = model; - model2.populations[{home, InfectionState::I}] = 9000; - model2.populations[{home, InfectionState::S}] = 800; - - auto rng = mio::RandomNumberGenerator(); - mio::Graph>, mio::MobilityEdgeDirected> graph; - graph.add_node(0, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), - mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); - graph.add_node(1, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), - mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), model2, t0); - size_t num_nodes = 2000; - for (size_t i = 2; i < num_nodes; i++) { - auto local_model = model; - graph.add_node(i, mio::UniformDistribution::get_instance()(rng, 6.0, 15.0), - mio::UniformDistribution::get_instance()(rng, 48.0, 54.0), local_model, t0); + + io::CSVReader<4> farms("../../farms.csv"); + farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); + int farm_id, num_cows; + double latitude, longitude; + while (farms.read_row(farm_id, num_cows, latitude, longitude)) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph.add_node(farm_id, longitude, latitude, curr_model, t0); } + auto rng = mio::RandomNumberGenerator(); + std::vector> interesting_indices; interesting_indices.push_back({model.populations.get_flat_index({home, InfectionState::I})}); - auto param = mio::MobilityParametersTimed(2.0, 10, 1); - graph.add_edge(0, 1, interesting_indices); - - auto distribution = mio::DiscreteDistributionInPlace(); - - std::vector uniform_vector(num_nodes, 1.0); - mio::log_info("Nodes generated"); - for (size_t i = 0; i < 3 * num_nodes; ++i) { - auto to = distribution(rng, {uniform_vector}); - auto from = distribution(rng, {uniform_vector}); + io::CSVReader<2> edges("../../edges.csv"); + edges.read_header(io::ignore_extra_column, "from", "to"); + int from, to; + while (edges.read_row(from, to)) { graph.add_edge(from, to, interesting_indices); } @@ -118,14 +113,12 @@ int main(int /*argc*/, char** /*argv*/) auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - for (size_t i = 0; i < 3 * num_nodes; i++) { - sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); - } - for (size_t i = 0; i < 3 * num_nodes; i++) { - sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); - } - for (size_t i = 0; i < 3 * num_nodes; i++) { - sim.add_exchange(distribution(rng, {std::vector(100, 1)}) + 1, 10, i); + io::CSVReader<5> exchanges("../../trade.csv"); + exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); + + int date, num_animals, edge; + while (exchanges.read_row(date, num_animals, from, to, edge)) { + sim.add_exchange(date, num_animals, edge); } mio::log_info("Number of exchanges: {}", sim.get_parameters().size()); diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 21154e7d74..e1c7aa3705 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -75,6 +75,11 @@ class LocationNode : public SimulationNode regional_neighbor_indices = neighbors; } + auto get_regional_neighbors() const + { + return regional_neighbor_indices; + } + private: mio::geo::GeographicalLocation m_location; // location of the node std::vector> regional_neighbor_indices; diff --git a/cpp/thirdparty/csv.h b/cpp/thirdparty/csv.h new file mode 100644 index 0000000000..a4741f03ff --- /dev/null +++ b/cpp/thirdparty/csv.h @@ -0,0 +1,1371 @@ +// Copyright: (2012-2015) Ben Strasser +// License: BSD-3 +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#ifndef CSV_H +#define CSV_H + +#include +#include +#include +#include +#include +#include +#include +#ifndef CSV_IO_NO_THREAD +#include +#include +#include +#endif +#include +#include +#include +#include +#include + +namespace io +{ +//////////////////////////////////////////////////////////////////////////// +// LineReader // +//////////////////////////////////////////////////////////////////////////// + +namespace error +{ +struct base : std::exception { + virtual void format_error_message() const = 0; + + const char* what() const noexcept override + { + format_error_message(); + return error_message_buffer; + } + + mutable char error_message_buffer[2048]; +}; + +// this only affects the file name in the error message +const int max_file_name_length = 1024; + +struct with_file_name { + with_file_name() + { + std::memset(file_name, 0, sizeof(file_name)); + } + + void set_file_name(const char* file_name) + { + if (file_name != nullptr) { + // This call to strncpy has parenthesis around it + // to silence the GCC -Wstringop-truncation warning + (strncpy(this->file_name, file_name, sizeof(this->file_name))); + this->file_name[sizeof(this->file_name) - 1] = '\0'; + } + else { + this->file_name[0] = '\0'; + } + } + + char file_name[max_file_name_length + 1]; +}; + +struct with_file_line { + with_file_line() + { + file_line = -1; + } + + void set_file_line(int file_line) + { + this->file_line = file_line; + } + + int file_line; +}; + +struct with_errno { + with_errno() + { + errno_value = 0; + } + + void set_errno(int errno_value) + { + this->errno_value = errno_value; + } + + int errno_value; +}; + +struct can_not_open_file : base, with_file_name, with_errno { + void format_error_message() const override + { + if (errno_value != 0) + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + "Can not open file \"%s\" because \"%s\".", file_name, std::strerror(errno_value)); + else + std::snprintf(error_message_buffer, sizeof(error_message_buffer), "Can not open file \"%s\".", file_name); + } +}; + +struct line_length_limit_exceeded : base, with_file_name, with_file_line { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + "Line number %d in file \"%s\" exceeds the maximum length of 2^24-1.", file_line, file_name); + } +}; +} // namespace error + +class ByteSourceBase +{ +public: + virtual int read(char* buffer, int size) = 0; + virtual ~ByteSourceBase() + { + } +}; + +namespace detail +{ + +class OwningStdIOByteSourceBase : public ByteSourceBase +{ +public: + explicit OwningStdIOByteSourceBase(FILE* file) + : file(file) + { + // Tell the std library that we want to do the buffering ourself. + std::setvbuf(file, 0, _IONBF, 0); + } + + int read(char* buffer, int size) + { + return std::fread(buffer, 1, size, file); + } + + ~OwningStdIOByteSourceBase() + { + std::fclose(file); + } + +private: + FILE* file; +}; + +class NonOwningIStreamByteSource : public ByteSourceBase +{ +public: + explicit NonOwningIStreamByteSource(std::istream& in) + : in(in) + { + } + + int read(char* buffer, int size) + { + in.read(buffer, size); + return in.gcount(); + } + + ~NonOwningIStreamByteSource() + { + } + +private: + std::istream& in; +}; + +class NonOwningStringByteSource : public ByteSourceBase +{ +public: + NonOwningStringByteSource(const char* str, long long size) + : str(str) + , remaining_byte_count(size) + { + } + + int read(char* buffer, int desired_byte_count) + { + int to_copy_byte_count = desired_byte_count; + if (remaining_byte_count < to_copy_byte_count) + to_copy_byte_count = remaining_byte_count; + std::memcpy(buffer, str, to_copy_byte_count); + remaining_byte_count -= to_copy_byte_count; + str += to_copy_byte_count; + return to_copy_byte_count; + } + + ~NonOwningStringByteSource() + { + } + +private: + const char* str; + long long remaining_byte_count; +}; + +#ifndef CSV_IO_NO_THREAD +class AsynchronousReader +{ +public: + void init(std::unique_ptr arg_byte_source) + { + std::unique_lock guard(lock); + byte_source = std::move(arg_byte_source); + desired_byte_count = -1; + termination_requested = false; + worker = std::thread([&] { + std::unique_lock guard(lock); + try { + for (;;) { + read_requested_condition.wait(guard, [&] { + return desired_byte_count != -1 || termination_requested; + }); + if (termination_requested) + return; + + read_byte_count = byte_source->read(buffer, desired_byte_count); + desired_byte_count = -1; + if (read_byte_count == 0) + break; + read_finished_condition.notify_one(); + } + } + catch (...) { + read_error = std::current_exception(); + } + read_finished_condition.notify_one(); + }); + } + + bool is_valid() const + { + return byte_source != nullptr; + } + + void start_read(char* arg_buffer, int arg_desired_byte_count) + { + std::unique_lock guard(lock); + buffer = arg_buffer; + desired_byte_count = arg_desired_byte_count; + read_byte_count = -1; + read_requested_condition.notify_one(); + } + + int finish_read() + { + std::unique_lock guard(lock); + read_finished_condition.wait(guard, [&] { + return read_byte_count != -1 || read_error; + }); + if (read_error) + std::rethrow_exception(read_error); + else + return read_byte_count; + } + + ~AsynchronousReader() + { + if (byte_source != nullptr) { + { + std::unique_lock guard(lock); + termination_requested = true; + } + read_requested_condition.notify_one(); + worker.join(); + } + } + +private: + std::unique_ptr byte_source; + + std::thread worker; + + bool termination_requested; + std::exception_ptr read_error; + char* buffer; + int desired_byte_count; + int read_byte_count; + + std::mutex lock; + std::condition_variable read_finished_condition; + std::condition_variable read_requested_condition; +}; +#endif + +class SynchronousReader +{ +public: + void init(std::unique_ptr arg_byte_source) + { + byte_source = std::move(arg_byte_source); + } + + bool is_valid() const + { + return byte_source != nullptr; + } + + void start_read(char* arg_buffer, int arg_desired_byte_count) + { + buffer = arg_buffer; + desired_byte_count = arg_desired_byte_count; + } + + int finish_read() + { + return byte_source->read(buffer, desired_byte_count); + } + +private: + std::unique_ptr byte_source; + char* buffer; + int desired_byte_count; +}; +} // namespace detail + +class LineReader +{ +private: + static const int block_len = 1 << 20; + std::unique_ptr buffer; // must be constructed before (and thus + // destructed after) the reader! +#ifdef CSV_IO_NO_THREAD + detail::SynchronousReader reader; +#else + detail::AsynchronousReader reader; +#endif + int data_begin; + int data_end; + + char file_name[error::max_file_name_length + 1]; + unsigned file_line; + + static std::unique_ptr open_file(const char* file_name) + { + // We open the file in binary mode as it makes no difference under *nix + // and under Windows we handle \r\n newlines ourself. + FILE* file = std::fopen(file_name, "rb"); + if (file == 0) { + int x = errno; // store errno as soon as possible, doing it after + // constructor call can fail. + error::can_not_open_file err; + err.set_errno(x); + err.set_file_name(file_name); + throw err; + } + return std::unique_ptr(new detail::OwningStdIOByteSourceBase(file)); + } + + void init(std::unique_ptr byte_source) + { + file_line = 0; + + buffer = std::unique_ptr(new char[3 * block_len]); + data_begin = 0; + data_end = byte_source->read(buffer.get(), 2 * block_len); + + // Ignore UTF-8 BOM + if (data_end >= 3 && buffer[0] == '\xEF' && buffer[1] == '\xBB' && buffer[2] == '\xBF') + data_begin = 3; + + if (data_end == 2 * block_len) { + reader.init(std::move(byte_source)); + reader.start_read(buffer.get() + 2 * block_len, block_len); + } + } + +public: + LineReader() = delete; + LineReader(const LineReader&) = delete; + LineReader& operator=(const LineReader&) = delete; + + explicit LineReader(const char* file_name) + { + set_file_name(file_name); + init(open_file(file_name)); + } + + explicit LineReader(const std::string& file_name) + { + set_file_name(file_name.c_str()); + init(open_file(file_name.c_str())); + } + + LineReader(const char* file_name, std::unique_ptr byte_source) + { + set_file_name(file_name); + init(std::move(byte_source)); + } + + LineReader(const std::string& file_name, std::unique_ptr byte_source) + { + set_file_name(file_name.c_str()); + init(std::move(byte_source)); + } + + LineReader(const char* file_name, const char* data_begin, const char* data_end) + { + set_file_name(file_name); + init(std::unique_ptr(new detail::NonOwningStringByteSource(data_begin, data_end - data_begin))); + } + + LineReader(const std::string& file_name, const char* data_begin, const char* data_end) + { + set_file_name(file_name.c_str()); + init(std::unique_ptr(new detail::NonOwningStringByteSource(data_begin, data_end - data_begin))); + } + + LineReader(const char* file_name, FILE* file) + { + set_file_name(file_name); + init(std::unique_ptr(new detail::OwningStdIOByteSourceBase(file))); + } + + LineReader(const std::string& file_name, FILE* file) + { + set_file_name(file_name.c_str()); + init(std::unique_ptr(new detail::OwningStdIOByteSourceBase(file))); + } + + LineReader(const char* file_name, std::istream& in) + { + set_file_name(file_name); + init(std::unique_ptr(new detail::NonOwningIStreamByteSource(in))); + } + + LineReader(const std::string& file_name, std::istream& in) + { + set_file_name(file_name.c_str()); + init(std::unique_ptr(new detail::NonOwningIStreamByteSource(in))); + } + + void set_file_name(const std::string& file_name) + { + set_file_name(file_name.c_str()); + } + + void set_file_name(const char* file_name) + { + if (file_name != nullptr) { + strncpy(this->file_name, file_name, sizeof(this->file_name) - 1); + this->file_name[sizeof(this->file_name) - 1] = '\0'; + } + else { + this->file_name[0] = '\0'; + } + } + + const char* get_truncated_file_name() const + { + return file_name; + } + + void set_file_line(unsigned file_line) + { + this->file_line = file_line; + } + + unsigned get_file_line() const + { + return file_line; + } + + char* next_line() + { + if (data_begin == data_end) + return nullptr; + + ++file_line; + + assert(data_begin < data_end); + assert(data_end <= block_len * 2); + + if (data_begin >= block_len) { + std::memcpy(buffer.get(), buffer.get() + block_len, block_len); + data_begin -= block_len; + data_end -= block_len; + if (reader.is_valid()) { + data_end += reader.finish_read(); + std::memcpy(buffer.get() + block_len, buffer.get() + 2 * block_len, block_len); + reader.start_read(buffer.get() + 2 * block_len, block_len); + } + } + + int line_end = data_begin; + while (line_end != data_end && buffer[line_end] != '\n') { + ++line_end; + } + + if (line_end - data_begin + 1 > block_len) { + error::line_length_limit_exceeded err; + err.set_file_name(file_name); + err.set_file_line(file_line); + throw err; + } + + if (line_end != data_end && buffer[line_end] == '\n') { + buffer[line_end] = '\0'; + } + else { + // some files are missing the newline at the end of the + // last line + ++data_end; + buffer[line_end] = '\0'; + } + + // handle windows \r\n-line breaks + if (line_end != data_begin && buffer[line_end - 1] == '\r') + buffer[line_end - 1] = '\0'; + + char* ret = buffer.get() + data_begin; + data_begin = line_end + 1; + return ret; + } +}; + +//////////////////////////////////////////////////////////////////////////// +// CSV // +//////////////////////////////////////////////////////////////////////////// + +namespace error +{ +const int max_column_name_length = 63; +struct with_column_name { + with_column_name() + { + std::memset(column_name, 0, max_column_name_length + 1); + } + + void set_column_name(const char* column_name) + { + if (column_name != nullptr) { + std::strncpy(this->column_name, column_name, max_column_name_length); + this->column_name[max_column_name_length] = '\0'; + } + else { + this->column_name[0] = '\0'; + } + } + + char column_name[max_column_name_length + 1]; +}; + +const int max_column_content_length = 63; + +struct with_column_content { + with_column_content() + { + std::memset(column_content, 0, max_column_content_length + 1); + } + + void set_column_content(const char* column_content) + { + if (column_content != nullptr) { + std::strncpy(this->column_content, column_content, max_column_content_length); + this->column_content[max_column_content_length] = '\0'; + } + else { + this->column_content[0] = '\0'; + } + } + + char column_content[max_column_content_length + 1]; +}; + +struct extra_column_in_header : base, with_file_name, with_column_name { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(Extra column "%s" in header of file "%s".)", column_name, file_name); + } +}; + +struct missing_column_in_header : base, with_file_name, with_column_name { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(Missing column "%s" in header of file "%s".)", column_name, file_name); + } +}; + +struct duplicated_column_in_header : base, with_file_name, with_column_name { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(Duplicated column "%s" in header of file "%s".)", column_name, file_name); + } +}; + +struct header_missing : base, with_file_name { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), "Header missing in file \"%s\".", file_name); + } +}; + +struct too_few_columns : base, with_file_name, with_file_line { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), "Too few columns in line %d in file \"%s\".", + file_line, file_name); + } +}; + +struct too_many_columns : base, with_file_name, with_file_line { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), "Too many columns in line %d in file \"%s\".", + file_line, file_name); + } +}; + +struct escaped_string_not_closed : base, with_file_name, with_file_line { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + "Escaped string was not closed in line %d in file \"%s\".", file_line, file_name); + } +}; + +struct integer_must_be_positive : base, with_file_name, with_file_line, with_column_name, with_column_content { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(The integer "%s" must be positive or 0 in column "%s" in file "%s" in line "%d".)", + column_content, column_name, file_name, file_line); + } +}; + +struct no_digit : base, with_file_name, with_file_line, with_column_name, with_column_content { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(The integer "%s" contains an invalid digit in column "%s" in file "%s" in line "%d".)", + column_content, column_name, file_name, file_line); + } +}; + +struct integer_overflow : base, with_file_name, with_file_line, with_column_name, with_column_content { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(The integer "%s" overflows in column "%s" in file "%s" in line "%d".)", column_content, + column_name, file_name, file_line); + } +}; + +struct integer_underflow : base, with_file_name, with_file_line, with_column_name, with_column_content { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(The integer "%s" underflows in column "%s" in file "%s" in line "%d".)", column_content, + column_name, file_name, file_line); + } +}; + +struct invalid_single_character : base, with_file_name, with_file_line, with_column_name, with_column_content { + void format_error_message() const override + { + std::snprintf(error_message_buffer, sizeof(error_message_buffer), + R"(The content "%s" of column "%s" in file "%s" in line "%d" is not a single character.)", + column_content, column_name, file_name, file_line); + } +}; +} // namespace error + +using ignore_column = unsigned int; +static const ignore_column ignore_no_column = 0; +static const ignore_column ignore_extra_column = 1; +static const ignore_column ignore_missing_column = 2; + +template +struct trim_chars { +private: + constexpr static bool is_trim_char(char) + { + return false; + } + + template + constexpr static bool is_trim_char(char c, char trim_char, OtherTrimChars... other_trim_chars) + { + return c == trim_char || is_trim_char(c, other_trim_chars...); + } + +public: + static void trim(char*& str_begin, char*& str_end) + { + while (str_begin != str_end && is_trim_char(*str_begin, trim_char_list...)) + ++str_begin; + while (str_begin != str_end && is_trim_char(*(str_end - 1), trim_char_list...)) + --str_end; + *str_end = '\0'; + } +}; + +struct no_comment { + static bool is_comment(const char*) + { + return false; + } +}; + +template +struct single_line_comment { +private: + constexpr static bool is_comment_start_char(char) + { + return false; + } + + template + constexpr static bool is_comment_start_char(char c, char comment_start_char, + OtherCommentStartChars... other_comment_start_chars) + { + return c == comment_start_char || is_comment_start_char(c, other_comment_start_chars...); + } + +public: + static bool is_comment(const char* line) + { + return is_comment_start_char(*line, comment_start_char_list...); + } +}; + +struct empty_line_comment { + static bool is_comment(const char* line) + { + if (*line == '\0') + return true; + while (*line == ' ' || *line == '\t') { + ++line; + if (*line == 0) + return true; + } + return false; + } +}; + +template +struct single_and_empty_line_comment { + static bool is_comment(const char* line) + { + return single_line_comment::is_comment(line) || + empty_line_comment::is_comment(line); + } +}; + +template +struct no_quote_escape { + static const char* find_next_column_end(const char* col_begin) + { + while (*col_begin != sep && *col_begin != '\0') + ++col_begin; + return col_begin; + } + + static void unescape(char*&, char*&) + { + } +}; + +template +struct double_quote_escape { + static const char* find_next_column_end(const char* col_begin) + { + while (*col_begin != sep && *col_begin != '\0') + if (*col_begin != quote) + ++col_begin; + else { + do { + ++col_begin; + while (*col_begin != quote) { + if (*col_begin == '\0') + throw error::escaped_string_not_closed(); + ++col_begin; + } + ++col_begin; + } while (*col_begin == quote); + } + return col_begin; + } + + static void unescape(char*& col_begin, char*& col_end) + { + if (col_end - col_begin >= 2) { + if (*col_begin == quote && *(col_end - 1) == quote) { + ++col_begin; + --col_end; + char* out = col_begin; + for (char* in = col_begin; in != col_end; ++in) { + if (*in == quote && (in + 1) != col_end && *(in + 1) == quote) { + ++in; + } + *out = *in; + ++out; + } + col_end = out; + *col_end = '\0'; + } + } + } +}; + +struct throw_on_overflow { + template + static void on_overflow(T&) + { + throw error::integer_overflow(); + } + + template + static void on_underflow(T&) + { + throw error::integer_underflow(); + } +}; + +struct ignore_overflow { + template + static void on_overflow(T&) + { + } + + template + static void on_underflow(T&) + { + } +}; + +struct set_to_max_on_overflow { + template + static void on_overflow(T& x) + { + // using (std::numeric_limits::max) instead of + // std::numeric_limits::max to make code including windows.h with its max + // macro happy + x = (std::numeric_limits::max)(); + } + + template + static void on_underflow(T& x) + { + x = (std::numeric_limits::min)(); + } +}; + +namespace detail +{ +template +void chop_next_column(char*& line, char*& col_begin, char*& col_end) +{ + assert(line != nullptr); + + col_begin = line; + // the col_begin + (... - col_begin) removes the constness + col_end = col_begin + (quote_policy::find_next_column_end(col_begin) - col_begin); + + if (*col_end == '\0') { + line = nullptr; + } + else { + *col_end = '\0'; + line = col_end + 1; + } +} + +template +void parse_line(char* line, char** sorted_col, const std::vector& col_order) +{ + for (int i : col_order) { + if (line == nullptr) + throw ::io::error::too_few_columns(); + char *col_begin, *col_end; + chop_next_column(line, col_begin, col_end); + + if (i != -1) { + trim_policy::trim(col_begin, col_end); + quote_policy::unescape(col_begin, col_end); + + sorted_col[i] = col_begin; + } + } + if (line != nullptr) + throw ::io::error::too_many_columns(); +} + +template +void parse_header_line(char* line, std::vector& col_order, const std::string* col_name, + ignore_column ignore_policy) +{ + col_order.clear(); + + bool found[column_count]; + std::fill(found, found + column_count, false); + while (line) { + char *col_begin, *col_end; + chop_next_column(line, col_begin, col_end); + + trim_policy::trim(col_begin, col_end); + quote_policy::unescape(col_begin, col_end); + + for (unsigned i = 0; i < column_count; ++i) + if (col_begin == col_name[i]) { + if (found[i]) { + error::duplicated_column_in_header err; + err.set_column_name(col_begin); + throw err; + } + found[i] = true; + col_order.push_back(i); + col_begin = 0; + break; + } + if (col_begin) { + if (ignore_policy & ::io::ignore_extra_column) + col_order.push_back(-1); + else { + error::extra_column_in_header err; + err.set_column_name(col_begin); + throw err; + } + } + } + if (!(ignore_policy & ::io::ignore_missing_column)) { + for (unsigned i = 0; i < column_count; ++i) { + if (!found[i]) { + error::missing_column_in_header err; + err.set_column_name(col_name[i].c_str()); + throw err; + } + } + } +} + +template +void parse(char* col, char& x) +{ + if (!*col) + throw error::invalid_single_character(); + x = *col; + ++col; + if (*col) + throw error::invalid_single_character(); +} + +template +void parse(char* col, std::string& x) +{ + x = col; +} + +template +void parse(char* col, const char*& x) +{ + x = col; +} + +template +void parse(char* col, char*& x) +{ + x = col; +} + +template +void parse_unsigned_integer(const char* col, T& x) +{ + x = 0; + while (*col != '\0') { + if ('0' <= *col && *col <= '9') { + T y = *col - '0'; + if (x > ((std::numeric_limits::max)() - y) / 10) { + overflow_policy::on_overflow(x); + return; + } + x = 10 * x + y; + } + else + throw error::no_digit(); + ++col; + } +} + +template +void parse(char* col, unsigned char& x) +{ + parse_unsigned_integer(col, x); +} +template +void parse(char* col, unsigned short& x) +{ + parse_unsigned_integer(col, x); +} +template +void parse(char* col, unsigned int& x) +{ + parse_unsigned_integer(col, x); +} +template +void parse(char* col, unsigned long& x) +{ + parse_unsigned_integer(col, x); +} +template +void parse(char* col, unsigned long long& x) +{ + parse_unsigned_integer(col, x); +} + +template +void parse_signed_integer(const char* col, T& x) +{ + if (*col == '-') { + ++col; + + x = 0; + while (*col != '\0') { + if ('0' <= *col && *col <= '9') { + T y = *col - '0'; + if (x < ((std::numeric_limits::min)() + y) / 10) { + overflow_policy::on_underflow(x); + return; + } + x = 10 * x - y; + } + else + throw error::no_digit(); + ++col; + } + return; + } + else if (*col == '+') + ++col; + parse_unsigned_integer(col, x); +} + +template +void parse(char* col, signed char& x) +{ + parse_signed_integer(col, x); +} +template +void parse(char* col, signed short& x) +{ + parse_signed_integer(col, x); +} +template +void parse(char* col, signed int& x) +{ + parse_signed_integer(col, x); +} +template +void parse(char* col, signed long& x) +{ + parse_signed_integer(col, x); +} +template +void parse(char* col, signed long long& x) +{ + parse_signed_integer(col, x); +} + +template +void parse_float(const char* col, T& x) +{ + bool is_neg = false; + if (*col == '-') { + is_neg = true; + ++col; + } + else if (*col == '+') + ++col; + + x = 0; + while ('0' <= *col && *col <= '9') { + int y = *col - '0'; + x *= 10; + x += y; + ++col; + } + + if (*col == '.' || *col == ',') { + ++col; + T pos = 1; + while ('0' <= *col && *col <= '9') { + pos /= 10; + int y = *col - '0'; + ++col; + x += y * pos; + } + } + + if (*col == 'e' || *col == 'E') { + ++col; + int e; + + parse_signed_integer(col, e); + + if (e != 0) { + T base; + if (e < 0) { + base = T(0.1); + e = -e; + } + else { + base = T(10); + } + + while (e != 1) { + if ((e & 1) == 0) { + base = base * base; + e >>= 1; + } + else { + x *= base; + --e; + } + } + x *= base; + } + } + else { + if (*col != '\0') + throw error::no_digit(); + } + + if (is_neg) + x = -x; +} + +template +void parse(char* col, float& x) +{ + parse_float(col, x); +} +template +void parse(char* col, double& x) +{ + parse_float(col, x); +} +template +void parse(char* col, long double& x) +{ + parse_float(col, x); +} + +template +void parse(char* col, T& x) +{ + // Mute unused variable compiler warning + (void)col; + (void)x; + // GCC evaluates "false" when reading the template and + // "sizeof(T)!=sizeof(T)" only when instantiating it. This is why + // this strange construct is used. + static_assert(sizeof(T) != sizeof(T), "Can not parse this type. Only builtin integrals, floats, " + "char, char*, const char* and std::string are supported"); +} + +} // namespace detail + +template , class quote_policy = no_quote_escape<','>, + class overflow_policy = throw_on_overflow, class comment_policy = no_comment> +class CSVReader +{ +private: + LineReader in; + + char* row[column_count]; + std::string column_names[column_count]; + + std::vector col_order; + + template + void set_column_names(std::string s, ColNames... cols) + { + column_names[column_count - sizeof...(ColNames) - 1] = std::move(s); + set_column_names(std::forward(cols)...); + } + + void set_column_names() + { + } + +public: + CSVReader() = delete; + CSVReader(const CSVReader&) = delete; + CSVReader& operator=(const CSVReader&); + + template + explicit CSVReader(Args&&... args) + : in(std::forward(args)...) + { + std::fill(row, row + column_count, nullptr); + col_order.resize(column_count); + for (unsigned i = 0; i < column_count; ++i) + col_order[i] = i; + for (unsigned i = 1; i <= column_count; ++i) + column_names[i - 1] = "col" + std::to_string(i); + } + + char* next_line() + { + return in.next_line(); + } + + template + void read_header(ignore_column ignore_policy, ColNames... cols) + { + static_assert(sizeof...(ColNames) >= column_count, "not enough column names specified"); + static_assert(sizeof...(ColNames) <= column_count, "too many column names specified"); + try { + set_column_names(std::forward(cols)...); + + char* line; + do { + line = in.next_line(); + if (!line) + throw error::header_missing(); + } while (comment_policy::is_comment(line)); + + detail::parse_header_line(line, col_order, column_names, + ignore_policy); + } + catch (error::with_file_name& err) { + err.set_file_name(in.get_truncated_file_name()); + throw; + } + } + + template + void set_header(ColNames... cols) + { + static_assert(sizeof...(ColNames) >= column_count, "not enough column names specified"); + static_assert(sizeof...(ColNames) <= column_count, "too many column names specified"); + set_column_names(std::forward(cols)...); + std::fill(row, row + column_count, nullptr); + col_order.resize(column_count); + for (unsigned i = 0; i < column_count; ++i) + col_order[i] = i; + } + + bool has_column(const std::string& name) const + { + return col_order.end() != + std::find(col_order.begin(), col_order.end(), + std::find(std::begin(column_names), std::end(column_names), name) - std::begin(column_names)); + } + + void set_file_name(const std::string& file_name) + { + in.set_file_name(file_name); + } + + void set_file_name(const char* file_name) + { + in.set_file_name(file_name); + } + + const char* get_truncated_file_name() const + { + return in.get_truncated_file_name(); + } + + void set_file_line(unsigned file_line) + { + in.set_file_line(file_line); + } + + unsigned get_file_line() const + { + return in.get_file_line(); + } + +private: + void parse_helper(std::size_t) + { + } + + template + void parse_helper(std::size_t r, T& t, ColType&... cols) + { + if (row[r]) { + try { + try { + ::io::detail::parse(row[r], t); + } + catch (error::with_column_content& err) { + err.set_column_content(row[r]); + throw; + } + } + catch (error::with_column_name& err) { + err.set_column_name(column_names[r].c_str()); + throw; + } + } + parse_helper(r + 1, cols...); + } + +public: + template + bool read_row(ColType&... cols) + { + static_assert(sizeof...(ColType) >= column_count, "not enough columns specified"); + static_assert(sizeof...(ColType) <= column_count, "too many columns specified"); + try { + try { + + char* line; + do { + line = in.next_line(); + if (!line) + return false; + } while (comment_policy::is_comment(line)); + + detail::parse_line(line, row, col_order); + + parse_helper(0, cols...); + } + catch (error::with_file_name& err) { + err.set_file_name(in.get_truncated_file_name()); + throw; + } + } + catch (error::with_file_line& err) { + err.set_file_line(in.get_file_line()); + throw; + } + + return true; + } +}; +} // namespace io +#endif \ No newline at end of file From 88d78223ed3c7aef473eea72ec2b32173a3bc5b2 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:04:23 +0200 Subject: [PATCH 071/169] CHG: make smm simulation copy constructible --- cpp/models/smm/simulation.h | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 8fb1423571..7ce8006352 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -77,6 +77,30 @@ class Simulation } } + Simulation(const Simulation& other) + : m_dt(other.m_dt) + , m_model(std::make_unique(*other.m_model)) + , m_result(other.m_result) + , m_internal_time(other.m_internal_time) + , m_tp_next_event(other.m_tp_next_event) + , m_waiting_times(other.m_waiting_times) + , m_current_rates(other.m_current_rates) + { + } + + Simulation& operator=(const Simulation& other) + { + if (this != &other) { + m_model = std::make_unique(*other.m_model); + m_result = other.m_result; + m_internal_time = other.m_internal_time; + m_tp_next_event = other.m_tp_next_event; + m_waiting_times = other.m_waiting_times; + m_current_rates = other.m_current_rates; + } + return *this; + } + /** * @brief Advance simulation to tmax. * This function performs a Gillespie algorithm. From a9ad34c3aef1ef7fc6ae41b6408b9c5ba586c07d Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:10:33 +0200 Subject: [PATCH 072/169] CHG: use nodes instead of edges --- cpp/memilio/mobility/graph_simulation.h | 27 +++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 206292a261..051c68372d 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -22,6 +22,7 @@ #include "memilio/mobility/graph.h" #include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/logging.h" #include "memilio/utils/random_number_generator.h" #include #include "memilio/compartments/feedback_simulation.h" @@ -45,15 +46,15 @@ class MobilityParametersTimed { insert_input_data(input_data); }; - MobilityParametersTimed(double time, double number, int edge) + MobilityParametersTimed(double time, double number, size_t from, size_t to) : _exchanges{} { - _exchanges.push(ExchangeData(time, number, edge)); + _exchanges.push(ExchangeData(time, number, from, to)); }; - void add_exchange(double time, double number, int edge) + void add_exchange(double time, double number, size_t from, size_t to) { - _exchanges.push(ExchangeData{time, number, edge}); + _exchanges.push(ExchangeData{time, number, from, to}); } /** @@ -86,6 +87,14 @@ class MobilityParametersTimed { return _exchanges.top().edge_id; } + auto next_event_to_id() + { + return _exchanges.top().to; + } + auto next_event_from_id() + { + return _exchanges.top().from; + } /** * @brief Return a const reference to the next event * @@ -137,7 +146,9 @@ class MobilityParametersTimed struct ExchangeData { double time; double number; - int edge_id; + size_t from; + size_t to; + int edge_id{}; }; struct CompareExchangeData { @@ -402,7 +413,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase Date: Wed, 15 Oct 2025 21:10:53 +0200 Subject: [PATCH 073/169] CHG: add get_edge function --- cpp/memilio/mobility/graph.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index e710f83322..3a88e01719 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -20,6 +20,7 @@ #ifndef GRAPH_H #define GRAPH_H +#include #include #include "memilio/utils/stl_util.h" #include "memilio/epidemiology/age_group.h" @@ -28,6 +29,7 @@ #include "memilio/utils/parameter_distributions.h" #include "memilio/epidemiology/damping.h" #include "memilio/geography/regions.h" +#include "memilio/utils/logging.h" #include #include "boost/filesystem.hpp" @@ -211,7 +213,7 @@ class Graph } /** - * @brief Range of edges going pout from a specific node + * @brief Range of edges going out from a specific node * * @param node_idx ID of node */ @@ -220,6 +222,18 @@ class Graph return out_edges(begin(m_edges), end(m_edges), node_idx); } + auto get_edge(size_t node_a, size_t node_b) const + { + auto edges = out_edges(node_a); + for (auto edge = edges.begin(); edge != edges.end(); ++edge) { + if (edge->end_node_idx == node_b) { + return *edge; + } + } + mio::log_error("Edge from {} to {} not found.", node_a, node_b); + throw std::out_of_range("Edge not found"); + } + /** * @brief range of edges going out from a specific node * From c969d093fa02ece111b69b5eb1626adba721dc24 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:11:08 +0200 Subject: [PATCH 074/169] CHG: Filter for impossible exchanges --- .../metapopulation_mobility_asymmetric.h | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index e1c7aa3705..2615aef31d 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -189,11 +189,17 @@ void MobilityEdgeDirected::apply_mobility(const FP t, const FP num_moving, L auto rng = mio::RandomNumberGenerator(); auto distribution = DiscreteDistributionInPlace(); std::vector travellers(node_from.get_result().get_last_value().size(), 0); - for (int i = 0; i < num_moving; ++i) { - auto group = distribution(rng, {node_from.get_result().get_last_value()}); - node_from.get_result().get_last_value()[group] -= 1; - travellers[group] += 1; - node_to.get_result().get_last_value()[group] += 1; + if (num_moving > std::accumulate(node_from.get_result().get_last_value().begin(), + node_from.get_result().get_last_value().end(), 0.0)) { + mio::log_warning("Trying to move more individuals than available ({}) at time {}.", num_moving, t); + } + else { + for (int i = 0; i < num_moving; ++i) { + auto group = distribution(rng, {node_from.get_result().get_last_value()}); + node_from.get_result().get_last_value()[group] -= 1; + travellers[group] += 1; + node_to.get_result().get_last_value()[group] += 1; + } } add_mobility_result_time_point(t, travellers); } From 6e8cb019cb65948ab949fa3db851492c4c9bb76e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:23:00 +0200 Subject: [PATCH 075/169] CHG: add reference? --- cpp/memilio/mobility/graph.h | 2 +- cpp/memilio/mobility/graph_simulation.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 3a88e01719..41356cb7be 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -222,7 +222,7 @@ class Graph return out_edges(begin(m_edges), end(m_edges), node_idx); } - auto get_edge(size_t node_a, size_t node_b) const + auto& get_edge(size_t node_a, size_t node_b) { auto edges = out_edges(node_a); for (auto edge = edges.begin(); edge != edges.end(); ++edge) { diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 051c68372d..51b5e7bd34 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -413,7 +413,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase Date: Thu, 16 Oct 2025 16:19:41 +0200 Subject: [PATCH 076/169] use ParameterStudy2 in ode_secir examples --- cpp/examples/ode_secir_parameter_study.cpp | 49 ++++++--------- .../ode_secir_parameter_study_graph.cpp | 25 ++++++-- cpp/memilio/compartments/parameter_studies.h | 59 +++++++++++++------ cpp/memilio/mobility/graph_simulation.h | 4 +- .../metapopulation_mobility_instant.h | 2 + cpp/memilio/utils/custom_index_array.h | 13 ++++ cpp/memilio/utils/index.h | 46 +++++++++++++++ 7 files changed, 142 insertions(+), 56 deletions(-) diff --git a/cpp/examples/ode_secir_parameter_study.cpp b/cpp/examples/ode_secir_parameter_study.cpp index 8ad8c1cd2b..751b0b6745 100644 --- a/cpp/examples/ode_secir_parameter_study.cpp +++ b/cpp/examples/ode_secir_parameter_study.cpp @@ -17,6 +17,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "memilio/compartments/simulation.h" +#include "memilio/config.h" +#include "ode_secir/model.h" #include "ode_secir/parameters_io.h" #include "ode_secir/parameter_space.h" #include "memilio/compartments/parameter_studies.h" @@ -30,10 +33,7 @@ * @param t0 starting point of simulation * @param tmax end point of simulation */ -mio::IOResult -write_single_run_result(const size_t run, - const mio::Graph>, - mio::MobilityEdge>& graph) +mio::IOResult write_single_run_result(const size_t run, const mio::osecir::Simulation& sim) { std::string abs_path; BOOST_OUTCOME_TRY(auto&& created, mio::create_directory("results", abs_path)); @@ -46,32 +46,14 @@ write_single_run_result(const size_t run, } //write sampled parameters for this run - //omit edges to save space as they are not sampled - int inode = 0; - for (auto&& node : graph.nodes()) { - BOOST_OUTCOME_TRY(auto&& js_node_model, serialize_json(node.property.get_result(), mio::IOF_OmitDistributions)); - Json::Value js_node(Json::objectValue); - js_node["NodeId"] = node.id; - js_node["Model"] = js_node_model; - auto node_filename = mio::path_join(abs_path, "Parameters_run" + std::to_string(run) + "_node" + - std::to_string(inode++) + ".json"); - BOOST_OUTCOME_TRY(mio::write_json(node_filename, js_node)); - } + auto node_filename = mio::path_join(abs_path, "Parameters_run" + std::to_string(run) + ".json"); + BOOST_OUTCOME_TRY(mio::write_json(node_filename, sim.get_result())); //write results for this run std::vector> all_results; std::vector ids; - ids.reserve(graph.nodes().size()); - all_results.reserve(graph.nodes().size()); - std::transform(graph.nodes().begin(), graph.nodes().end(), std::back_inserter(all_results), [](auto& node) { - return node.property.get_result(); - }); - std::transform(graph.nodes().begin(), graph.nodes().end(), std::back_inserter(ids), [](auto& node) { - return node.id; - }); - auto num_groups = (int)(size_t)graph.nodes()[0].property.get_simulation().get_model().parameters.get_num_groups(); - BOOST_OUTCOME_TRY(mio::save_result(all_results, ids, num_groups, + BOOST_OUTCOME_TRY(mio::save_result({sim.get_result()}, {0}, sim.get_model().parameters.get_num_groups().get(), mio::path_join(abs_path, ("Results_run" + std::to_string(run) + ".h5")))); return mio::success(); @@ -83,6 +65,7 @@ int main() ScalarType t0 = 0; ScalarType tmax = 50; + ScalarType dt = 0.1; ScalarType cont_freq = 10; // see Polymod study @@ -141,14 +124,20 @@ int main() //create study auto num_runs = size_t(1); - mio::ParameterStudy> parameter_study(model, t0, tmax, num_runs); + // mio::ParameterStudy2, mio::osecir::Model, ScalarType> + // parameter_study(model, t0, tmax, dt, num_runs); + + auto parameter_study = + mio::make_parameter_study>(model, t0, tmax, dt, num_runs); //run study - auto sample_graph = [](auto&& graph) { - return mio::osecir::draw_sample(graph); + auto sample_graph = [](const auto& model_, ScalarType t0_, ScalarType dt_, size_t) { + mio::osecir::Model copy = model_; + mio::osecir::draw_sample(copy); + return mio::osecir::Simulation(std::move(copy), t0_, dt_); }; - auto handle_result = [](auto&& graph, auto&& run) { - auto write_result_status = write_single_run_result(run, graph); + auto handle_result = [](auto&& sim, auto&& run) { + auto write_result_status = write_single_run_result(run, sim); if (!write_result_status) { std::cout << "Error writing result: " << write_result_status.error().formatted_message(); } diff --git a/cpp/examples/ode_secir_parameter_study_graph.cpp b/cpp/examples/ode_secir_parameter_study_graph.cpp index b2e8260cc3..c3cb5b7564 100644 --- a/cpp/examples/ode_secir_parameter_study_graph.cpp +++ b/cpp/examples/ode_secir_parameter_study_graph.cpp @@ -24,11 +24,13 @@ #include "memilio/io/epi_data.h" #include "memilio/io/result_io.h" #include "memilio/io/mobility_io.h" +#include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_instant.h" #include "memilio/utils/logging.h" #include "memilio/utils/miompi.h" #include "memilio/utils/random_number_generator.h" #include "memilio/utils/time_series.h" +#include "ode_secir/model.h" #include "ode_secir/parameters_io.h" #include "ode_secir/parameter_space.h" #include "boost/filesystem.hpp" @@ -266,8 +268,16 @@ int main() indices_save_edges); //run parameter study - auto parameter_study = mio::ParameterStudy>{ - params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)}; + // auto parameter_study = mio::ParameterStudy2< + // mio::GraphSimulation>, + // mio::MobilityEdge>, + // ScalarType, ScalarType>, + // mio::Graph, mio::MobilityParameters>, ScalarType>{ + // params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)}; + + auto parameter_study = mio::make_parameter_study_graph_ode>( + params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)); if (mio::mpi::is_root()) { printf("Seeds: "); @@ -279,10 +289,13 @@ int main() auto save_single_run_result = mio::IOResult(mio::success()); auto ensemble = parameter_study.run( - [](auto&& graph) { - return draw_sample(graph); + [](auto&& graph, ScalarType t0, ScalarType dt, size_t) { + auto copy = graph; + return mio::make_sampled_graph_simulation( + mio::osecir::draw_sample(copy), t0, dt, dt); }, - [&](auto results_graph, auto&& run_id) { + [&](auto&& results_sim, auto&& run_id) { + auto results_graph = results_sim.get_graph(); auto interpolated_result = mio::interpolate_simulation_result(results_graph); auto params = std::vector>{}; @@ -319,7 +332,7 @@ int main() boost::filesystem::path results_dir("test_results"); bool created = boost::filesystem::create_directories(results_dir); if (created) { - mio::log_info("Directory '{:s}' was created.", results_dir.string()); + mio::log_info("Directory '{}' was created.", results_dir.string()); } auto county_ids = std::vector{1001, 1002, 1003}; diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index fcbc2ea8db..69ec36ec80 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -412,7 +413,7 @@ class ParameterStudy2 * @param graph_sim_dt time step of graph simulation * @param num_runs number of runs */ - ParameterStudy2(const ParameterType& global_parameters, Time t0, Time tmax, Step dt, size_t num_runs) + ParameterStudy2(const Parameters& global_parameters, Time t0, Time tmax, Step dt, size_t num_runs) : m_parameters(global_parameters) , m_num_runs(num_runs) , m_t0{t0} @@ -482,7 +483,8 @@ class ParameterStudy2 thread_local_rng().set_counter(run_rng_counter); //sample - auto sim = create_simulation(m_parameters, m_t0, m_dt, run_idx); + Simulation sim = + create_simulation(std::as_const(m_parameters), std::as_const(m_t0), std::as_const(m_dt), run_idx); log(LogLevel::info, "ParameterStudies: Generated {} random numbers.", (thread_local_rng().get_counter() - run_rng_counter).get()); @@ -611,23 +613,42 @@ class ParameterStudy2 RandomNumberGenerator m_rng; }; -// //sample parameters and create simulation -// template -// GraphSimulation create_sampled_simulation(auto&& params_graph, -// SampleGraphFunction sample_graph) -// { -// SimulationGraph sim_graph; - -// auto sampled_graph = sample_graph(params_graph); -// for (auto&& node : sampled_graph.nodes()) { -// sim_graph.add_node(node.id, node.property, m_t0, m_dt_integration); -// } -// for (auto&& edge : sampled_graph.edges()) { -// sim_graph.add_edge(edge.start_node_idx, edge.end_node_idx, edge.property); -// } - -// return make_mobility_sim(m_t0, m_dt_graph_sim, std::move(sim_graph)); -// } +template +ParameterStudy2 make_parameter_study(const Parameters& global_parameters, Time t0, + Time tmax, Step dt, size_t num_runs) +{ + return {global_parameters, t0, tmax, dt, num_runs}; +} + +template +auto make_parameter_study_graph_ode(const Graph>& global_parameters, FP t0, + FP tmax, FP dt, size_t num_runs) +{ + using SimGraph = Graph, mio::MobilityEdge>; + using SimGraphSim = mio::GraphSimulation; + using Params = Graph>; + + return ParameterStudy2{global_parameters, t0, tmax, dt, num_runs}; +} + +//sample parameters and create simulation +template +GraphSim make_sampled_graph_simulation( + const Graph>& sampled_graph, + FP t0, FP dt_node_sim, FP dt_graph_sim) +{ + typename GraphSim::Graph sim_graph; + + for (auto&& node : sampled_graph.nodes()) { + sim_graph.add_node(node.id, node.property, t0, dt_node_sim); + } + for (auto&& edge : sampled_graph.edges()) { + sim_graph.add_edge(edge.start_node_idx, edge.end_node_idx, edge.property); + } + + return make_mobility_sim(t0, dt_graph_sim, + std::move(sim_graph)); +} } // namespace mio diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 33770f8a47..0ceb33a821 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -31,10 +31,12 @@ namespace mio /** * @brief abstract simulation on a graph with alternating node and edge actions */ -template +template class GraphSimulationBase { public: + using Graph = GraphT; + using node_function = node_f; using edge_function = edge_f; diff --git a/cpp/memilio/mobility/metapopulation_mobility_instant.h b/cpp/memilio/mobility/metapopulation_mobility_instant.h index 4a01e1392e..f0684557eb 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_instant.h +++ b/cpp/memilio/mobility/metapopulation_mobility_instant.h @@ -46,6 +46,8 @@ template class SimulationNode { public: + using Simulation = Sim; + template ::value, void>> SimulationNode(Args&&... args) : m_simulation(std::forward(args)...) diff --git a/cpp/memilio/utils/custom_index_array.h b/cpp/memilio/utils/custom_index_array.h index d17d496275..e23991db03 100644 --- a/cpp/memilio/utils/custom_index_array.h +++ b/cpp/memilio/utils/custom_index_array.h @@ -23,6 +23,8 @@ #include "memilio/math/eigen_util.h" #include "memilio/utils/index.h" #include "memilio/utils/stl_util.h" +#include +#include namespace { @@ -90,6 +92,17 @@ std::enable_if_t<(I < (Index::size - 1)), std::pair> flatten_ind return {val + (size_t)mio::get(indices) * prod, prod * (size_t)mio::get(dimensions)}; } +template +void assign_from_vector(Index& index, const std::vector& values) +{ + assert(values.size() == sizeof...(Tags)); + + if constexpr (I < sizeof...(Tags)) { + get(index) = values[I]; + assign_from_vector(index, values); + } +} + template struct is_random_access_iterator : std::is_base_of::iterator_category, std::random_access_iterator_tag> { diff --git a/cpp/memilio/utils/index.h b/cpp/memilio/utils/index.h index 595db59f37..54e336c7c0 100644 --- a/cpp/memilio/utils/index.h +++ b/cpp/memilio/utils/index.h @@ -21,7 +21,11 @@ #define INDEX_H #include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/metaprogramming.h" #include "memilio/utils/type_safe.h" +#include +#include +#include namespace mio { @@ -29,6 +33,41 @@ namespace mio template class Index; +namespace details +{ + +template +std::tuple...> get_tuple(Index i) +{ + if constexpr (sizeof...(Tags) == 1) { + return std::tuple(i); + } + else { + return i.indices; + } +} + +template +std::tuple> get_tuple(Enum i) + requires std::is_enum::value +{ + return std::tuple(Index(i)); +} + +// template +// Index merge_index_from_tuple(std::tuple...>); + +template +auto merge_indices(IndexArgs... args) +{ + return std::tuple_cat(get_tuple(args)...); +} + +template +using merge_indices_expr = decltype(std::tuple(get_tuple(std::declval())...)); + +} // namespace details + /** * @brief An Index with a single template parameter is a typesafe wrapper for size_t * that is associated with a Tag. It is used to index into a CustomIndexArray @@ -139,6 +178,13 @@ class Index } public: + template ::value>> + Index(IndexArgs... _index_args) + : indices(details::merge_indices(_index_args...)) + { + } + // comparison operators bool operator==(Index const& other) const { From e1a5c8f70441a36a9a5e9095fb8acc8b57da4dc2 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:02:06 +0200 Subject: [PATCH 077/169] FIX: multiple fixes --- cpp/memilio/mobility/graph.h | 20 ++++++++++---------- cpp/memilio/mobility/graph_simulation.h | 3 +++ cpp/models/smm/simulation.h | 3 +++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 41356cb7be..0aa4cb56b7 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -222,6 +222,16 @@ class Graph return out_edges(begin(m_edges), end(m_edges), node_idx); } + /** + * @brief range of edges going out from a specific node + * + * @param node_idx ID of node + */ + auto out_edges(size_t node_idx) const + { + return out_edges(begin(m_edges), end(m_edges), node_idx); + } + auto& get_edge(size_t node_a, size_t node_b) { auto edges = out_edges(node_a); @@ -234,16 +244,6 @@ class Graph throw std::out_of_range("Edge not found"); } - /** - * @brief range of edges going out from a specific node - * - * @param node_idx ID of node - */ - auto out_edges(size_t node_idx) const - { - return out_edges(begin(m_edges), end(m_edges), node_idx); - } - private: template static auto out_edges(Iter b, Iter e, size_t idx) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 51b5e7bd34..ec3c827537 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -401,6 +401,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase t_max) { dt = t_max - Base::m_t; } @@ -414,10 +415,12 @@ class AsymmetricGraphSimulation : public GraphSimulationBase Date: Fri, 17 Oct 2025 17:12:26 +0200 Subject: [PATCH 078/169] CHG: clean up --- cpp/models/smm/simulation.h | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index a63c8cd83e..295b037f32 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -180,7 +180,6 @@ class Simulation } } return m_result.get_last_value(); - // } } /** From 20dae7ad575fa12f170e6acb6675b7119aee3eda Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:12:40 +0200 Subject: [PATCH 079/169] CHG: Add some missing TimeSeries features --- cpp/memilio/utils/time_series.h | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cpp/memilio/utils/time_series.h b/cpp/memilio/utils/time_series.h index e43c3318b2..730515a01a 100644 --- a/cpp/memilio/utils/time_series.h +++ b/cpp/memilio/utils/time_series.h @@ -158,6 +158,26 @@ class TimeSeries return value_matrix; } + /** + * @brief constructs TimeSeries instance and initializes it with zeros + * @param num_time_points number of time steps + * @param num_elements number of compartiments * number of groups + * @param timepoints vector of time points + * @return + */ + static TimeSeries zero(Eigen::Index num_time_points, Eigen::Index num_elements, std::vector timepoints) + { + assert(num_time_points == static_cast(timepoints.size())); + TimeSeries value_matrix(num_elements); + value_matrix.m_data = Matrix::Zero(num_elements + 1, num_time_points); + value_matrix.m_num_time_points = num_time_points; + for (Eigen::Index i = 0; i < num_time_points; ++i) { + value_matrix.m_data(0, i) = timepoints[i]; + } + + return value_matrix; + } + /** copy assignment */ TimeSeries& operator=(const TimeSeries& other) { @@ -280,6 +300,21 @@ class TimeSeries return m_data(0, i); } + auto get_time_points() + { + return m_data(0, Eigen::all); + } + + auto get_index_of_time(FP time) + { + for (Eigen::Index i = 0; i < m_num_time_points; ++i) { + if (m_data(0, i) == time) { + return i; + } + } + return Eigen::Index(-1); + } + /** * time of time point at index num_time_points - 1 */ From 4b21c0ae5ff0de06d205b81450478699bf6fbfe7 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:13:00 +0200 Subject: [PATCH 080/169] CHG: Add quarantine option to LocaitonNode --- .../mobility/metapopulation_mobility_asymmetric.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 2615aef31d..75aa62147f 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -80,9 +80,20 @@ class LocationNode : public SimulationNode return regional_neighbor_indices; } + auto is_quarantined() const + { + return m_is_quarantined; + } + + void set_quarantined(bool quarantine) + { + m_is_quarantined = quarantine; + } + private: mio::geo::GeographicalLocation m_location; // location of the node std::vector> regional_neighbor_indices; + bool m_is_quarantined{false}; }; /** From 670c2f6b6320de05e1bda58d1c03aa8f0a624b33 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:13:58 +0200 Subject: [PATCH 081/169] CHG: Add data collection and quarantines --- cpp/memilio/mobility/graph_simulation.h | 163 +++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index ec3c827537..5c465a9801 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -20,6 +20,8 @@ #ifndef MIO_MOBILITY_GRAPH_SIMULATION_H #define MIO_MOBILITY_GRAPH_SIMULATION_H +#include "memilio/config.h" +#include "memilio/epidemiology/adoption_rate.h" #include "memilio/mobility/graph.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" @@ -27,6 +29,7 @@ #include #include "memilio/compartments/feedback_simulation.h" #include "memilio/geography/regions.h" +#include "smm/parameters.h" namespace mio { @@ -410,11 +413,19 @@ class AsymmetricGraphSimulation : public GraphSimulationBase results(size, 0.0); + for (auto& n : Base::m_graph.edges()) { + assert(n.property.get_mobility_results().get_num_elements() == size); + for (auto result = n.property.get_mobility_results().begin(); + result != n.property.get_mobility_results().end(); ++result) { + for (int i = 0; i < size; i++) { + results[i] += (*result)[i]; + } + } + } + return results; + } + + auto exchanges_per_timestep() + { + const auto size = Base::m_graph.edges()[0].property.get_mobility_results().get_num_elements(); + std::vector timepoints; + // Collect all exchange timepoints + + for (auto& n : Base::m_graph.edges()) { + auto local_timepoints = n.property.get_mobility_results().get_time_points(); + for (auto t : local_timepoints) { + if (std::find(timepoints.begin(), timepoints.end(), t) == timepoints.end()) { + timepoints.push_back(t); + } + } + } + std::sort(timepoints.begin(), timepoints.end()); + auto results = TimeSeries::zero(timepoints.size(), size, timepoints); + for (auto& n : Base::m_graph.edges()) { + assert(n.property.get_mobility_results().get_num_elements() == size); + auto edge_result = n.property.get_mobility_results(); + // Add exchange data to big TimeSeries + for (Eigen::Index time_index = 0; time_index < edge_result.get_num_time_points(); ++time_index) { + auto time = edge_result.get_time(time_index); + auto index = results.get_index_of_time(time); + if (index == Eigen::Index(-1)) { + continue; + } + for (int i = 0; i < size; i++) { + results.get_value(index)[i] += edge_result.get_value(time_index)[i]; + } + } + } + return results; + } + + auto sum_nodes() + { + const auto size = Base::m_graph.nodes()[0].property.get_result().get_num_elements(); + std::vector results(size, 0.0); + for (auto& n : Base::m_graph.nodes()) { + assert(n.property.get_result().get_num_elements() == size); + for (int i = 0; i < size; i++) { + results[i] += n.property.get_result().get_last_value()[i]; + } + } + return results; + } + + auto statistics_per_timestep() + { + const auto size = Base::m_graph.nodes()[0].property.get_result().get_num_elements(); + std::vector timepoints; + // Collect all exchange timepoints => All simulations are stopped and write results at those + + for (auto& n : Base::m_graph.edges()) { + auto local_timepoints = n.property.get_mobility_results().get_time_points(); + for (auto t : local_timepoints) { + if (std::find(timepoints.begin(), timepoints.end(), t) == timepoints.end()) { + timepoints.push_back(t); + } + } + } + std::sort(timepoints.begin(), timepoints.end()); + auto results = TimeSeries::zero(timepoints.size(), size, timepoints); + for (auto& n : Base::m_graph.nodes()) { + assert(n.property.get_result().get_num_elements() == size); + auto node_timeseries = n.property.get_result(); + for (Eigen::Index time_index = 0; time_index < node_timeseries.get_num_time_points(); ++time_index) { + auto time = node_timeseries.get_time(time_index); + auto index = results.get_index_of_time(time); + if (index == Eigen::Index(-1)) { + continue; + } + for (int i = 0; i < size; i++) { + results.get_value(index)[i] += node_timeseries.get_value(time_index)[i]; + } + } + } + return results; + } + + auto statistics_per_timestep(std::vector node_indices) + { + assert(node_indices.size() > 0); + const auto size = Base::m_graph.nodes()[node_indices[0]].property.get_result().get_num_elements(); + std::vector timepoints; + // Collect all exchange timepoints => All simulations are stopped and write results at those + for (auto& n : Base::m_graph.edges()) { + auto local_timepoints = n.property.get_mobility_results().get_time_points(); + for (auto t : local_timepoints) { + if (std::find(timepoints.begin(), timepoints.end(), t) == timepoints.end()) { + timepoints.push_back(t); + } + } + } + std::sort(timepoints.begin(), timepoints.end()); + auto results = TimeSeries::zero(timepoints.size(), size, timepoints); + for (size_t node_index : node_indices) { + auto node_timeseries = Base::m_graph.nodes()[node_index].property.get_result(); + assert(node_timeseries.get_num_elements() == size); + for (Eigen::Index time_index = 0; time_index < node_timeseries.get_num_time_points(); ++time_index) { + auto time = node_timeseries.get_time(time_index); + auto index = results.get_index_of_time(time); + if (index == Eigen::Index(-1)) { + continue; + } + for (int i = 0; i < size; i++) { + results.get_value(index)[i] += node_timeseries.get_value(time_index)[i]; + } + } + } + return results; + } + + void apply_interventions() + { + // Set quarantine + // for (auto& n : Base::m_graph.nodes()) { + // if (n.property.get_result().get_last_value()[2] > 40) { + // mio::log_debug("Node {} is quarantined at time {}.", n.id, Base::m_t); + // n.property.set_quarantined(true); + // } + // else { + // mio::log_debug("Node {} is not quarantined at time {}.", n.id, Base::m_t); + // n.property.set_quarantined(false); + // } + // } + + // for (auto& n : Base::m_graph.nodes()) { + // if (n.property.get_result().get_last_value()[2] > 40) { + // n.property.get_model().get_parameters().get < mio::smm::AdoptionRates + // } // I don't know any smart way to access and modify the adoption rates here, I would need to know the template parameters. + // } + } + private: mio::MobilityParametersTimed m_parameters; }; From 06d33f3049f3c9c382cc129b8837cd58bd71baa5 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:15:51 +0200 Subject: [PATCH 082/169] CHG: Clean up and add new functionality to example --- cpp/examples/asymmetric_graph.cpp | 83 ++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 2c7292994c..74c41f7e56 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -48,26 +48,23 @@ int main(int /*argc*/, char** /*argv*/) const auto dt = 1.; //initial time step //total compartment sizes - double num_total = 10000, num_exp = 200, num_ins = 50, num_rec = 0; using Model = mio::smm::Model; - Model model; - auto home = mio::regions::Region(0); - - model.populations[{home, InfectionState::E}] = num_exp; - model.populations[{home, InfectionState::I}] = num_ins; - model.populations[{home, InfectionState::R}] = num_rec; - model.populations[{home, InfectionState::S}] = num_total - num_exp - num_ins - num_rec; + auto home = mio::regions::Region(0); std::vector> adoption_rates; adoption_rates.push_back({InfectionState::E, InfectionState::I, home, 0.2, {}}); - adoption_rates.push_back({InfectionState::I, InfectionState::R, home, 0.333, {}}); - adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.5}}}); - model.parameters.get>() = adoption_rates; + adoption_rates.push_back({InfectionState::I, InfectionState::R, home, 0.1, {}}); + adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.8}}}); + adoption_rates.push_back({InfectionState::S, InfectionState::D, home, 0.0, {}}); + adoption_rates.push_back({InfectionState::E, InfectionState::D, home, 0.0, {}}); + adoption_rates.push_back({InfectionState::I, InfectionState::D, home, 0.0, {}}); + adoption_rates.push_back({InfectionState::R, InfectionState::D, home, 0.0, {}}); mio::Graph>, mio::MobilityEdgeDirected> graph; + mio::log_info("Starting Graph generation"); io::CSVReader<4> farms("../../farms.csv"); farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); @@ -76,9 +73,10 @@ int main(int /*argc*/, char** /*argv*/) while (farms.read_row(farm_id, num_cows, latitude, longitude)) { Model curr_model; curr_model.populations[{home, InfectionState::S}] = num_cows; - curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::E}] = 2; curr_model.populations[{home, InfectionState::I}] = 0; curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; curr_model.parameters.get>() = adoption_rates; graph.add_node(farm_id, longitude, latitude, curr_model, t0); } @@ -86,11 +84,11 @@ int main(int /*argc*/, char** /*argv*/) auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; - interesting_indices.push_back({model.populations.get_flat_index({home, InfectionState::I})}); + interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); io::CSVReader<2> edges("../../edges.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); - int from, to; + size_t from, to; while (edges.read_row(from, to)) { graph.add_edge(from, to, interesting_indices); } @@ -100,13 +98,11 @@ int main(int /*argc*/, char** /*argv*/) auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); - // mio::log_info("Value: {}", nodes.begin()->get_latitude()); - auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors( - tree.inrange_indices_query(node.property.get_location(), {3.0, 5.0, 10.0})); + node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {10.0})); } mio::log_info("Neighbors set"); @@ -118,24 +114,55 @@ int main(int /*argc*/, char** /*argv*/) int date, num_animals, edge; while (exchanges.read_row(date, num_animals, from, to, edge)) { - sim.add_exchange(date, num_animals, edge); + sim.add_exchange(date, num_animals, from, to); } + mio::log_info("Exchanges added"); - mio::log_info("Number of exchanges: {}", sim.get_parameters().size()); + // #ifdef MEMILIO_ENABLE_OPENMP + // #pragma omp parallel for + // for (size_t i = 0; i < 100; ++i) { + // #endif - mio::log_info("Exchanges added"); + auto sim2(sim); + mio::log_info("new Simulation created"); - sim.advance(tmax); + sim2.advance(tmax); mio::log_info("Simulation finished"); - // std::cout << "First table" << std::endl; - // sim.get_graph().nodes()[0].property.get_result().print_table({"S", "E", "I", "R"}); + // #ifdef MEMILIO_ENABLE_OPENMP + // } + // #endif + + // sim2.get_graph().nodes()[28].property.get_result().print_table({"S", "E", "I", "R", "D"}); // std::cout << "Second Table" << std::endl; - // sim.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R"}); + // sim2.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R", "D"}); + + // auto& edge_1_0 = sim2.get_graph().edges()[1]; + // auto& results = edge_1_0.property.get_mobility_results(); + // results.print_table({"Commuter Sick", "Commuter Total"}); + + // auto exchange_results = sim2.sum_exchanges(); + // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); + // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); + + // sim2.exchanges_per_timestep().print_table({"Commuter Sick", "Commuter Total"}); + // auto num_time_points = sim2.exchanges_per_timestep().get_num_time_points(); + + // for (auto node : sim2.get_graph().nodes()) { + // if (node.property.get_result().get_num_time_points() < num_time_points) { + // mio::log_error("Node with inconsistent number of time points in results."); + // } + // } + + // exchange_results = sim2.sum_nodes(); + // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); + + // auto sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); + // // auto combined_results = sim2.combine_node_results(); + // // combined_results.print_table({"S", "E", "I", "R", "D"}); + // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); - auto& edge_1_0 = sim.get_graph().edges()[1]; - auto& results = edge_1_0.property.get_mobility_results(); - results.print_table({"Commuter Sick", "Commuter Total"}); + // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); return 0; } From 5176657c58ecec7bd3a6ee7f56cf26b961b09b82 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:17:12 +0200 Subject: [PATCH 083/169] FIX: remove wrong includes --- cpp/memilio/mobility/graph_simulation.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 5c465a9801..d66549afe2 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -21,7 +21,6 @@ #define MIO_MOBILITY_GRAPH_SIMULATION_H #include "memilio/config.h" -#include "memilio/epidemiology/adoption_rate.h" #include "memilio/mobility/graph.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" @@ -29,7 +28,6 @@ #include #include "memilio/compartments/feedback_simulation.h" #include "memilio/geography/regions.h" -#include "smm/parameters.h" namespace mio { From 47088f7873405f91261f09086e41ba95a0eaa616 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:30:37 +0200 Subject: [PATCH 084/169] [wip] remove old ParameterStudies from cpp --- cpp/examples/abm_minimal.cpp | 2 +- cpp/examples/ode_secir_read_graph.cpp | 18 +- cpp/memilio/compartments/parameter_studies.h | 360 +------------------ cpp/tests/test_parameter_studies.cpp | 35 +- cpp/tests/test_save_results.cpp | 28 +- 5 files changed, 59 insertions(+), 384 deletions(-) diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index df6392ec75..4cace851ba 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -237,7 +237,7 @@ int main() sim.get_result().print_table(outfile_run, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); std::cout << "Results written to " << name_run << std::endl; auto params = std::vector{}; - return std::move(interpolated_result); + return interpolated_result; }); if (ensemble.size() > 0) { diff --git a/cpp/examples/ode_secir_read_graph.cpp b/cpp/examples/ode_secir_read_graph.cpp index 8832aec976..09a88f46f5 100644 --- a/cpp/examples/ode_secir_read_graph.cpp +++ b/cpp/examples/ode_secir_read_graph.cpp @@ -112,8 +112,8 @@ int main(int argc, char** argv) std::cout << "Reading Mobility File..." << std::flush; auto read_mobility_result = mio::read_mobility_plain(filename); if (!read_mobility_result) { - std::cout << read_mobility_result.error().formatted_message() << '\n'; - std::cout << "Create the mobility file with MEmilio Epidata's getCommuterMobility.py file." << '\n'; + std::cout << "\n" << read_mobility_result.error().formatted_message() << "\n"; + std::cout << "Create the mobility file with MEmilio Epidata's getCommuterMobility.py file.\n"; return 0; } auto& commuter_mobility = read_mobility_result.value(); @@ -137,7 +137,8 @@ int main(int argc, char** argv) std::cout << "Writing Json Files..." << std::flush; auto write_status = mio::write_graph(graph, "graph_parameters"); if (!write_status) { - std::cout << "Error: " << write_status.error().formatted_message(); + std::cout << "\n" << write_status.error().formatted_message(); + return 0; } std::cout << "Done" << std::endl; @@ -145,13 +146,20 @@ int main(int argc, char** argv) auto graph_read_result = mio::read_graph>("graph_parameters"); if (!graph_read_result) { - std::cout << "Error: " << graph_read_result.error().formatted_message(); + std::cout << "\n" << graph_read_result.error().formatted_message(); + return 0; } std::cout << "Done" << std::endl; auto& graph_read = graph_read_result.value(); std::cout << "Running Simulations..." << std::flush; - auto study = mio::ParameterStudy>(graph_read, t0, tmax, 0.5, 2); + auto study = mio::make_parameter_study_graph_ode>(graph_read, t0, + tmax, 0.5, 2); + study.run([](auto&& g, auto t0_, auto dt_, auto) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy), t0_, dt_, + dt_); + }); std::cout << "Done" << std::endl; return 0; diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index 69ec36ec80..cf266afab3 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -44,351 +44,6 @@ namespace mio { -/** - * Class that performs multiple simulation runs with randomly sampled parameters. - * Can simulate mobility graphs with one simulation in each node or single simulations. - * @tparam S type of simulation that runs in one node of the graph. - */ -template > -class ParameterStudy -{ -public: - /** - * The type of simulation of a single node of the graph. - */ - using Simulation = S; - /** - * The Graph type that stores the parametes of the simulation. - * This is the input of ParameterStudies. - */ - using ParametersGraph = Graph>; - /** - * The Graph type that stores simulations and their results of each run. - * This is the output of ParameterStudies for each run. - */ - using SimulationGraph = mio::Graph, EdgeT>; - - /** - * create study for graph of compartment models. - * @param graph graph of parameters - * @param t0 start time of simulations - * @param tmax end time of simulations - * @param graph_sim_dt time step of graph simulation - * @param num_runs number of runs - */ - ParameterStudy(const ParametersGraph& graph, FP t0, FP tmax, FP graph_sim_dt, size_t num_runs) - : m_graph(graph) - , m_num_runs(num_runs) - , m_t0{t0} - , m_tmax{tmax} - , m_dt_graph_sim(graph_sim_dt) - { - } - - /** - * create study for graph of compartment models. - * Creates distributions for all parameters of the models in the graph. - * @param graph graph of parameters - * @param t0 start time of simulations - * @param tmax end time of simulations - * @param dev_rel relative deviation of the created distributions from the initial value. - * @param graph_sim_dt time step of graph simulation - * @param num_runs number of runs - */ - ParameterStudy(const ParametersGraph& graph, FP t0, FP tmax, FP dev_rel, FP graph_sim_dt, size_t num_runs) - : ParameterStudy(graph, t0, tmax, graph_sim_dt, num_runs) - { - for (auto& params_node : m_graph.nodes()) { - set_params_distributions_normal(params_node, t0, tmax, dev_rel); - } - } - - /** - * @brief Create study for single compartment model. - * @param model compartment model with initial values - * @param t0 start time of simulations - * @param tmax end time of simulations - * @param num_runs number of runs in ensemble run - */ - ParameterStudy(typename Simulation::Model const& model, FP t0, FP tmax, size_t num_runs) - : ParameterStudy({}, t0, tmax, tmax - t0, num_runs) - { - m_graph.add_node(0, model); - } - - /** - * @brief Carry out all simulations in the parameter study. - * Save memory and enable more runs by immediately processing and/or discarding the result. - * The result processing function is called when a run is finished. It receives the result of the run - * (a SimulationGraph object) and an ordered index. The values returned by the result processing function - * are gathered and returned as a list. - * This function is parallelized if memilio is configured with MEMILIO_ENABLE_MPI. - * The MPI processes each contribute a share of the runs. The sample function and result processing function - * are called in the same process that performs the run. The results returned by the result processing function are - * gathered at the root process and returned as a list by the root in the same order as if the programm - * were running sequentially. Processes other than the root return an empty list. - * @param sample_graph Function that receives the ParametersGraph and returns a sampled copy. - * @param result_processing_function Processing function for simulation results, e.g., output function. - * @returns At the root process, a list of values per run that have been returned from the result processing function. - * At all other processes, an empty list. - * @tparam SampleGraphFunction Callable type, accepts instance of ParametersGraph. - * @tparam HandleSimulationResultFunction Callable type, accepts instance of SimulationGraph and an index of type size_t. - */ - template - std::vector> - run(SampleGraphFunction sample_graph, HandleSimulationResultFunction result_processing_function) - { - int num_procs, rank; -#ifdef MEMILIO_ENABLE_MPI - MPI_Comm_size(mpi::get_world(), &num_procs); - MPI_Comm_rank(mpi::get_world(), &rank); -#else - num_procs = 1; - rank = 0; -#endif - - //The ParameterDistributions used for sampling parameters use thread_local_rng() - //So we set our own RNG to be used. - //Assume that sampling uses the thread_local_rng() and isn't multithreaded - m_rng.synchronize(); - thread_local_rng() = m_rng; - - auto run_distribution = distribute_runs(m_num_runs, num_procs); - auto start_run_idx = - std::accumulate(run_distribution.begin(), run_distribution.begin() + size_t(rank), size_t(0)); - auto end_run_idx = start_run_idx + run_distribution[size_t(rank)]; - - std::vector> ensemble_result; - ensemble_result.reserve(m_num_runs); - - for (size_t run_idx = start_run_idx; run_idx < end_run_idx; run_idx++) { - log(LogLevel::info, "ParameterStudies: run {}", run_idx); - - //prepare rng for this run by setting the counter to the right offset - //Add the old counter so that this call of run() produces different results - //from the previous call - auto run_rng_counter = m_rng.get_counter() + rng_totalsequence_counter( - static_cast(run_idx), Counter(0)); - thread_local_rng().set_counter(run_rng_counter); - - //sample - auto sim = create_sampled_simulation(sample_graph); - log(LogLevel::info, "ParameterStudies: Generated {} random numbers.", - (thread_local_rng().get_counter() - run_rng_counter).get()); - - //perform run - sim.advance(m_tmax); - - //handle result and store - ensemble_result.emplace_back(result_processing_function(std::move(sim).get_graph(), run_idx)); - } - - //Set the counter of our RNG so that future calls of run() produce different parameters. - m_rng.set_counter(m_rng.get_counter() + rng_totalsequence_counter(m_num_runs, Counter(0))); - -#ifdef MEMILIO_ENABLE_MPI - //gather results - if (rank == 0) { - for (int src_rank = 1; src_rank < num_procs; ++src_rank) { - int bytes_size; - MPI_Recv(&bytes_size, 1, MPI_INT, src_rank, 0, mpi::get_world(), MPI_STATUS_IGNORE); - ByteStream bytes(bytes_size); - MPI_Recv(bytes.data(), bytes.data_size(), MPI_BYTE, src_rank, 0, mpi::get_world(), MPI_STATUS_IGNORE); - - auto src_ensemble_results = deserialize_binary(bytes, Tag{}); - if (!src_ensemble_results) { - log_error("Error receiving ensemble results from rank {}.", src_rank); - } - std::copy(src_ensemble_results.value().begin(), src_ensemble_results.value().end(), - std::back_inserter(ensemble_result)); - } - } - else { - auto bytes = serialize_binary(ensemble_result); - auto bytes_size = int(bytes.data_size()); - MPI_Send(&bytes_size, 1, MPI_INT, 0, 0, mpi::get_world()); - MPI_Send(bytes.data(), bytes.data_size(), MPI_BYTE, 0, 0, mpi::get_world()); - ensemble_result.clear(); //only return root process - } -#endif - - return ensemble_result; - } - - /** - * @brief Carry out all simulations in the parameter study. - * Convenience function for a few number of runs, but can use more memory because it stores all runs until the end. - * Unlike the other overload, this function is not MPI-parallel. - * @return vector of SimulationGraph for each run. - */ - template - std::vector run(SampleGraphFunction sample_graph) - { - std::vector ensemble_result; - ensemble_result.reserve(m_num_runs); - - //The ParameterDistributions used for sampling parameters use thread_local_rng() - //So we set our own RNG to be used. - //Assume that sampling uses the thread_local_rng() and isn't multithreaded - thread_local_rng() = m_rng; - - for (size_t i = 0; i < m_num_runs; i++) { - log(LogLevel::info, "ParameterStudies: run {}", i); - - //prepare rng for this run by setting the counter to the right offset - //Add the old counter so that this call of run() produces different results - //from the previous call - auto run_rng_counter = m_rng.get_counter() + - rng_totalsequence_counter(static_cast(i), Counter(0)); - thread_local_rng().set_counter(run_rng_counter); - - auto sim = create_sampled_simulation(sample_graph); - log(LogLevel::info, "ParameterStudies: Generated {} random numbers.", - (thread_local_rng().get_counter() - run_rng_counter).get()); - - sim.advance(m_tmax); - - ensemble_result.emplace_back(std::move(sim).get_graph()); - } - - //Set the counter of our RNG so that future calls of run() produce different parameters. - m_rng.set_counter(m_rng.get_counter() + rng_totalsequence_counter(m_num_runs, Counter(0))); - - return ensemble_result; - } - - /** - * @brief sets the number of Monte Carlo runs - * @param[in] num_runs number of runs - */ - void set_num_runs(size_t num_runs) - { - m_num_runs = num_runs; - } - - /** - * @brief returns the number of Monte Carlo runs - */ - int get_num_runs() const - { - return static_cast(m_num_runs); - } - - /** - * @brief sets end point in simulation - * @param[in] tmax end point in simulation - */ - void set_tmax(FP tmax) - { - m_tmax = tmax; - } - - /** - * @brief returns end point in simulation - */ - FP get_tmax() const - { - return m_tmax; - } - - void set_t0(FP t0) - { - m_t0 = t0; - } - - /** - * @brief returns start point in simulation - */ - FP get_t0() const - { - return m_t0; - } - - /** - * Get the input model that the parameter study is run for. - * Use for single node simulations, use get_model_graph for graph simulations. - * @{ - */ - const typename Simulation::Model& get_model() const - { - return m_graph.nodes()[0].property; - } - typename Simulation::Model& get_model() - { - return m_graph.nodes()[0].property; - } - /** @} */ - - /** - * Get the input graph that the parameter study is run for. - * Use for graph simulations, use get_model for single node simulations. - * @{ - */ - const ParametersGraph& get_model_graph() const - { - return m_graph; - } - ParametersGraph& get_model_graph() - { - return m_graph; - } - /** @} */ - - RandomNumberGenerator& get_rng() - { - return m_rng; - } - -private: - //sample parameters and create simulation - template - GraphSimulation create_sampled_simulation(SampleGraphFunction sample_graph) - { - SimulationGraph sim_graph; - - auto sampled_graph = sample_graph(m_graph); - for (auto&& node : sampled_graph.nodes()) { - sim_graph.add_node(node.id, node.property, m_t0, m_dt_integration); - } - for (auto&& edge : sampled_graph.edges()) { - sim_graph.add_edge(edge.start_node_idx, edge.end_node_idx, edge.property); - } - - return make_mobility_sim(m_t0, m_dt_graph_sim, std::move(sim_graph)); - } - - std::vector distribute_runs(size_t num_runs, int num_procs) - { - //evenly distribute runs - //lower processes do one more run if runs are not evenly distributable - auto num_runs_local = num_runs / num_procs; //integer division! - auto remainder = num_runs % num_procs; - - std::vector run_distribution(num_procs); - std::fill(run_distribution.begin(), run_distribution.begin() + remainder, num_runs_local + 1); - std::fill(run_distribution.begin() + remainder, run_distribution.end(), num_runs_local); - - return run_distribution; - } - -private: - // Stores Graph with the names and ranges of all parameters - ParametersGraph m_graph; - - size_t m_num_runs; - - // Start time (should be the same for all simulations) - FP m_t0; - // End time (should be the same for all simulations) - FP m_tmax; - // time step of the graph - FP m_dt_graph_sim; - // adaptive time step of the integrator (will be corrected if too large/small) - FP m_dt_integration = 0.1; - // - RandomNumberGenerator m_rng; -}; - /** * Class that performs multiple simulation runs with randomly sampled parameters. * Can simulate mobility graphs with one simulation in each node or single simulations. @@ -442,7 +97,7 @@ class ParameterStudy2 * @param process_simulation_result A function that accepts S */ template - std::vector> + std::vector>> run(CreateSimulationFunction&& create_simulation, ProcessSimulationResultFunction&& process_simulation_result) { static_assert(std::is_invocable_r_v, @@ -469,7 +124,8 @@ class ParameterStudy2 std::accumulate(run_distribution.begin(), run_distribution.begin() + size_t(rank), size_t(0)); auto end_run_idx = start_run_idx + run_distribution[size_t(rank)]; - std::vector> ensemble_result; + std::vector>> + ensemble_result; ensemble_result.reserve(m_num_runs); for (size_t run_idx = start_run_idx; run_idx < end_run_idx; run_idx++) { @@ -536,10 +192,7 @@ class ParameterStudy2 template std::vector run(CreateSimulationFunction&& create_simulation) { - return run(std::forward(create_simulation), - [](Simulation&& sim, size_t) -> Simulation&& { - return sim; - }); + return run(std::forward(create_simulation), &result_forwarding_function); } /** @@ -597,6 +250,11 @@ class ParameterStudy2 return run_distribution; } + inline static Simulation&& result_forwarding_function(Simulation&& sim, size_t) + { + return std::move(sim); + } + private: // Stores Graph with the names and ranges of all parameters ParameterType m_parameters; diff --git a/cpp/tests/test_parameter_studies.cpp b/cpp/tests/test_parameter_studies.cpp index e0f46fbc5a..b7f63a6852 100644 --- a/cpp/tests/test_parameter_studies.cpp +++ b/cpp/tests/test_parameter_studies.cpp @@ -157,14 +157,17 @@ TEST(ParameterStudies, sample_graph) graph.add_node(1, model); graph.add_edge(0, 1, mio::MobilityParameters(Eigen::VectorXd::Constant(Eigen::Index(num_groups * 8), 1.0))); - auto study = mio::ParameterStudy>(graph, 0.0, 0.0, 0.5, 1); + auto study = mio::make_parameter_study_graph_ode>(graph, 0.0, 0.0, 0.5, 1); mio::log_rng_seeds(study.get_rng(), mio::LogLevel::warn); - auto results = study.run([](auto&& g) { - return draw_sample(g); + auto ensemble_results = study.run([](auto&& g, auto t0_, auto dt_, auto) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy), t0_, dt_, + dt_); }); - EXPECT_EQ(results[0].edges()[0].property.get_parameters().get_coefficients()[0].get_dampings().size(), 1); - for (auto& node : results[0].nodes()) { + auto& results = ensemble_results.at(0); + EXPECT_EQ(results.get_graph().edges()[0].property.get_parameters().get_coefficients()[0].get_dampings().size(), 1); + for (auto& node : results.get_graph().nodes()) { auto& result_model = node.property.get_simulation().get_model(); EXPECT_EQ(result_model.parameters.get>() .get_cont_freq_mat()[0] @@ -370,26 +373,24 @@ TEST(ParameterStudies, check_ensemble_run_result) mio::ContactMatrix(Eigen::MatrixXd::Constant((size_t)num_groups, (size_t)num_groups, fact * cont_freq)); mio::osecir::set_params_distributions_normal(model, t0, tmax, 0.2); - mio::ParameterStudy> parameter_study(model, t0, tmax, 1); + auto parameter_study = mio::make_parameter_study>(model, t0, tmax, 0.1, 1); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); // Run parameter study - parameter_study.set_num_runs(1); - auto graph_results = parameter_study.run([](auto&& g) { - return draw_sample(g); + auto ensemble_results = parameter_study.run([](auto&& model_, auto t0_, auto dt_, auto) { + auto copy = model_; + draw_sample(copy); + return mio::osecir::Simulation(copy, t0_, dt_); }); - std::vector> results; - for (size_t i = 0; i < graph_results.size(); i++) { - results.push_back(std::move(graph_results[i].nodes()[0].property.get_result())); - } + const mio::TimeSeries& results = ensemble_results.at(0).get_result(); - for (Eigen::Index i = 0; i < results[0].get_num_time_points(); i++) { + for (Eigen::Index i = 0; i < results.get_num_time_points(); i++) { std::vector total_at_ti((size_t)mio::osecir::InfectionState::Count, 0); - for (Eigen::Index j = 0; j < results[0][i].size(); j++) { // number of compartments per time step - EXPECT_GE(results[0][i][j], 0.0) << " day " << results[0].get_time(i) << " group " << j; - total_at_ti[static_cast(j) / (size_t)mio::osecir::InfectionState::Count] += results[0][i][j]; + for (Eigen::Index j = 0; j < results[i].size(); j++) { // number of compartments per time step + EXPECT_GE(results[i][j], 0.0) << " day " << results.get_time(i) << " group " << j; + total_at_ti[static_cast(j) / (size_t)mio::osecir::InfectionState::Count] += results[i][j]; } for (auto j = mio::AgeGroup(0); j < params.get_num_groups(); j++) { diff --git a/cpp/tests/test_save_results.cpp b/cpp/tests/test_save_results.cpp index a15c62d419..0fd07553aa 100644 --- a/cpp/tests/test_save_results.cpp +++ b/cpp/tests/test_save_results.cpp @@ -169,8 +169,9 @@ TEST(TestSaveResult, save_result_with_params) graph.add_edge(0, 1, mio::MobilityParameters(Eigen::VectorXd::Constant(Eigen::Index(num_groups * 10), 1.0))); - auto num_runs = 3; - auto parameter_study = mio::ParameterStudy>(graph, 0.0, 2.0, 0.5, num_runs); + auto num_runs = 3; + auto parameter_study = + mio::make_parameter_study_graph_ode>(graph, 0.0, 2.0, 0.5, num_runs); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); TempFileRegister tmp_file_register; @@ -183,10 +184,13 @@ TEST(TestSaveResult, save_result_with_params) ensemble_params.reserve(size_t(num_runs)); auto save_result_status = mio::IOResult(mio::success()); parameter_study.run( - [](auto&& g) { - return draw_sample(g); + [](auto&& g, auto t0_, auto dt_, auto) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy), + t0_, dt_, dt_); }, - [&](auto results_graph, auto run_idx) { + [&](auto&& results, auto run_idx) { + auto results_graph = results.get_graph(); ensemble_results.push_back(mio::interpolate_simulation_result(results_graph)); ensemble_params.emplace_back(); @@ -302,8 +306,9 @@ TEST(TestSaveResult, save_percentiles_and_sums) mio::MobilityParameters(Eigen::VectorXd::Constant(Eigen::Index(num_groups * 10), 1.0), indices_save_edges)); - auto num_runs = 3; - auto parameter_study = mio::ParameterStudy>(graph, 0.0, 2.0, 0.5, num_runs); + auto num_runs = 3; + auto parameter_study = + mio::make_parameter_study_graph_ode>(graph, 0.0, 2.0, 0.5, num_runs); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); TempFileRegister tmp_file_register; @@ -317,10 +322,13 @@ TEST(TestSaveResult, save_percentiles_and_sums) auto ensemble_edges = std::vector>>{}; ensemble_edges.reserve(size_t(num_runs)); parameter_study.run( - [](auto&& g) { - return draw_sample(g); + [](auto&& g, auto t0_, auto dt_, auto) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy), + t0_, dt_, dt_); }, - [&](auto results_graph, auto /*run_idx*/) { + [&](auto&& results, auto /*run_idx*/) { + auto results_graph = results.get_graph(); ensemble_results.push_back(mio::interpolate_simulation_result(results_graph)); ensemble_params.emplace_back(); From 26460422b623c0bc9a609a7d598c1d41dc9e9181 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:41:26 +0200 Subject: [PATCH 085/169] Fix up ode_secir_read_graph --- cpp/examples/ode_secir_read_graph.cpp | 51 +++++++++++---------------- cpp/memilio/io/cli.h | 3 ++ cpp/memilio/utils/base_dir.h | 8 +++-- cpp/memilio/utils/index.h | 16 ++++++--- cpp/memilio/utils/type_list.h | 6 ++++ 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/cpp/examples/ode_secir_read_graph.cpp b/cpp/examples/ode_secir_read_graph.cpp index 09a88f46f5..e2b9df4bd7 100644 --- a/cpp/examples/ode_secir_read_graph.cpp +++ b/cpp/examples/ode_secir_read_graph.cpp @@ -17,43 +17,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include "memilio/mobility/metapopulation_mobility_instant.h" -#include "memilio/io/mobility_io.h" #include "memilio/compartments/parameter_studies.h" +#include "memilio/io/cli.h" +#include "memilio/io/mobility_io.h" +#include "memilio/mobility/metapopulation_mobility_instant.h" +#include "memilio/utils/base_dir.h" + +#include "memilio/utils/stl_util.h" #include "ode_secir/parameter_space.h" #include "ode_secir/parameters_io.h" -#include -#include -std::string setup(int argc, char** argv, const std::string data_dir) -{ - if (argc == 2) { - std::cout << "Using file " << argv[1] << " in data/Germany/mobility." << std::endl; - return mio::path_join(data_dir, "Germany", "mobility", (std::string)argv[1]); - } - else { - if (argc > 2) { - mio::log_error("Too many arguments given."); - } - else { - mio::log_warning("No arguments given."); - } - auto mobility_file = "commuter_mobility_2022.txt"; - std::cout << "Using file " << mobility_file << " in data/Germany/mobility." << std::endl; - std::cout << "Usage: read_graph MOBILITY_FILE" - << "\n\n"; - std::cout - << "This example performs a simulation based on mobility data from the German Federal Employment Agency." - << std::endl; - return mio::path_join(data_dir, "Germany", "mobility", mobility_file); - } -} +#include int main(int argc, char** argv) { mio::set_log_level(mio::LogLevel::critical); - std::string data_dir = DATA_DIR; - std::string filename = setup(argc, argv, data_dir); + + auto parameters = + mio::cli::ParameterSetBuilder() + .add<"MobilityFile">( + mio::path_join(mio::base_dir(), "data", "Germany", "mobility", "commuter_mobility_2022.txt"), + {.description = "Create the mobility file with MEmilio Epidata's getCommuterMobility.py file."}) + .build(); + + auto result = mio::command_line_interface(argv[0], argc, argv, parameters, {"MobilityFile"}); + if (!result) { + std::cout << result.error().message(); + return result.error().code().value(); + } const auto t0 = 0.; const auto tmax = 10.; @@ -110,7 +101,7 @@ int main(int argc, char** argv) mio::osecir::set_params_distributions_normal(model, t0, tmax, 0.2); std::cout << "Reading Mobility File..." << std::flush; - auto read_mobility_result = mio::read_mobility_plain(filename); + auto read_mobility_result = mio::read_mobility_plain(parameters.get<"MobilityFile">()); if (!read_mobility_result) { std::cout << "\n" << read_mobility_result.error().formatted_message() << "\n"; std::cout << "Create the mobility file with MEmilio Epidata's getCommuterMobility.py file.\n"; diff --git a/cpp/memilio/io/cli.h b/cpp/memilio/io/cli.h index 1f0fefcf69..395f8b1ce7 100644 --- a/cpp/memilio/io/cli.h +++ b/cpp/memilio/io/cli.h @@ -293,6 +293,9 @@ class AbstractParameter : public DatalessParameter else { // deserialize failed // insert more information to the error message std::string msg = "While setting \"" + name + "\": " + param_result.error().message(); + if (param_result.error().message() == "Json value is not a string.") { + msg += " Try using escaped quotes (\\\") around your input strings."; + } return mio::failure(param_result.error().code(), msg); } }) diff --git a/cpp/memilio/utils/base_dir.h b/cpp/memilio/utils/base_dir.h index 0b396ceb27..d390dcd6ce 100644 --- a/cpp/memilio/utils/base_dir.h +++ b/cpp/memilio/utils/base_dir.h @@ -22,13 +22,15 @@ #include "memilio/config.h" +#include + namespace mio { /** - * @brief Returns path to the repo directory. -*/ -constexpr std::string mio::base_dir() + * @brief Returns the absolute path to the project directory. + */ +const static std::string base_dir() { return MEMILIO_BASE_DIR; } diff --git a/cpp/memilio/utils/index.h b/cpp/memilio/utils/index.h index 54e336c7c0..ac76fe3b38 100644 --- a/cpp/memilio/utils/index.h +++ b/cpp/memilio/utils/index.h @@ -227,12 +227,20 @@ class Index std::tuple...> indices; }; -template -struct type_at_index> : public type_at_index { +/// Specialization of type_at_index for Index. @see type_at_index. +template +struct type_at_index> : public type_at_index { }; -template -struct index_of_type> : public index_of_type { +/// Specialization of index_of_type for Index. @see index_of_type. +template +struct index_of_type> : public index_of_type { +}; + +/// Specialization of index_of_type for Index. Resolves ambiguity when using Index%s as items. @see index_of_type. +template +struct index_of_type, Index> { + static constexpr std::size_t value = 0; }; // retrieves the Index at the Ith position for a Index with more than one Tag diff --git a/cpp/memilio/utils/type_list.h b/cpp/memilio/utils/type_list.h index 6ff50a720a..43d02aea12 100644 --- a/cpp/memilio/utils/type_list.h +++ b/cpp/memilio/utils/type_list.h @@ -61,6 +61,12 @@ template struct index_of_type> : public index_of_type { }; +/// Specialization of index_of_type for TypeList. Resolves ambiguity when using TypeLists as items. @see index_of_type. +template +struct index_of_type, TypeList> { + static constexpr std::size_t value = 0; +}; + } // namespace mio #endif // MIO_UTILS_TYPE_LIST_H_ From 9b593f51f2b373a1b91acb42caf4ef3555613f20 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:27:50 +0200 Subject: [PATCH 086/169] patch python bindings to mostly emulate old ParameterStudy behaviour --- cpp/memilio/compartments/parameter_studies.h | 27 ++-- .../bindings/compartments/parameter_studies.h | 103 +++++++++++++++ .../simulation/bindings/models/osecir.cpp | 121 +++++++++--------- .../simulation/bindings/models/osecirvvs.cpp | 107 +++++++++------- .../simulation_test/test_parameter_study.py | 6 +- 5 files changed, 240 insertions(+), 124 deletions(-) create mode 100644 pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index cf266afab3..73a5d596a9 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -23,23 +23,14 @@ #include "memilio/io/binary_serializer.h" #include "memilio/mobility/graph_simulation.h" #include "memilio/utils/logging.h" -#include "memilio/utils/metaprogramming.h" #include "memilio/utils/miompi.h" #include "memilio/utils/random_number_generator.h" -#include "memilio/utils/time_series.h" #include "memilio/mobility/metapopulation_mobility_instant.h" -#include "memilio/compartments/simulation.h" #include #include -#include #include -#include -#include -#include -#include #include -#include namespace mio { @@ -218,6 +209,11 @@ class ParameterStudy2 { return m_t0; } + + Time get_dt() const + { + return m_dt; + } /** * Get the input graph that the parameter study is run for. * Use for graph simulations, use get_model for single node simulations. @@ -227,6 +223,10 @@ class ParameterStudy2 { return m_parameters; } + Parameters& get_parameters() + { + return m_parameters; + } /** @} */ RandomNumberGenerator& get_rng() @@ -255,7 +255,6 @@ class ParameterStudy2 return std::move(sim); } -private: // Stores Graph with the names and ranges of all parameters ParameterType m_parameters; @@ -271,7 +270,7 @@ class ParameterStudy2 RandomNumberGenerator m_rng; }; -template +template ParameterStudy2 make_parameter_study(const Parameters& global_parameters, Time t0, Time tmax, Step dt, size_t num_runs) { @@ -282,11 +281,11 @@ template auto make_parameter_study_graph_ode(const Graph>& global_parameters, FP t0, FP tmax, FP dt, size_t num_runs) { - using SimGraph = Graph, mio::MobilityEdge>; - using SimGraphSim = mio::GraphSimulation; + using SimGraph = Graph, MobilityEdge>; + using SimGraphSim = GraphSimulation; using Params = Graph>; - return ParameterStudy2{global_parameters, t0, tmax, dt, num_runs}; + return ParameterStudy2{global_parameters, t0, tmax, dt, num_runs}; } //sample parameters and create simulation diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h b/pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h new file mode 100644 index 0000000000..8cb60ffe12 --- /dev/null +++ b/pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h @@ -0,0 +1,103 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Rene Schmieding +* +* 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/compartments/parameter_studies.h" +#include "memilio/compartments/compartmental_model.h" + +#include "pybind11/pybind11.h" +#include "pybind11/stl_bind.h" + +#include +#include +#include +#include +#include + +namespace py = pybind11; + +namespace pymio +{ + +/* + * @brief bind ParameterStudy for any model + */ +template +void bind_GrapParameterStudy( + py::module_& m, std::string const& name, std::vector argnames, + std::function create_simulation = + [](const mio::Graph>& g, double t0, double dt, + size_t) { + using GraphSim = mio::GraphSimulation< + double, + mio::Graph>, mio::MobilityEdge>, + double, double>; + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy), t0, dt, dt) + }) +{ + assert(sizeof...(RunArgs) == argnames.size()); + using SimulationT = mio::GraphSimulation< + double, mio::Graph>, mio::MobilityEdge>, + double, double>; + using ParametersT = Graph < typename Sim::Model, MobilityParameters; + using StudyT = mio::ParameterStudy2; + using CreateSimulationFunctionT = std::function; + using ProcessSimulationResultFunctionT = std::function; + pymio::bind_class(m, name.c_str()) + .def(py::init(), py::arg("parameters"), py::arg("t0"), + py::arg("tmax"), py::arg("dt") py::arg("num_runs")) + .def_property_readonly("num_runs", &StudyT::get_num_runs) + .def_property_readonly("tmax", &StudyT::get_tmax) + .def_property_readonly("t0", &StudyT::get_t0) + .def_property_readonly("dt", &StudyT::get_dt) + .def_property("parameters", py::overload_cast<>(&StudyT::get_parameters), + py::return_value_policy::reference_internal) + .def_property_readonly("rng", &StudyT::get_rng, py::return_value_policy::reference_internal) + .def( + "run", + [](StudyT& self, const ProcessSimulationResultFunctionT& handle_result, RunArgs args...) { + self.run(create_simulation, [&handle_result](auto&& g, auto&& run_idx) { + //handle_result_function needs to return something + //we don't want to run an unknown python object through parameterstudies, so + //we just return 0 and ignore the list returned by run(). + //So python will behave slightly different than c++ + handle_result(std::move(g), run_idx); + return 0; + }); + }, + py::arg("handle_result_func")) + .def("run", + [](StudyT& self) { //default argument doesn't seem to work with functions + return self.run(create_simulation); + }) + .def( + "run_single", + [](StudyT& self, ProcessSimulationResultFunctionT handle_result) { + self.run(create_simulation, [&handle_result](auto&& r, auto&& run_idx) { + handle_result(std::move(r.nodes()[0].property.get_simulation()), run_idx); + return 0; + }); + }, + py::arg("handle_result_func")) + .def("run_single", [](StudyT& self) { + return filter_graph_results(self.run(create_simulation)); + }); +} + +} // namespace pymio diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp index 273da48f2d..5fc722ed8d 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp @@ -48,6 +48,7 @@ #include "pybind11/pybind11.h" #include "pybind11/stl_bind.h" #include "Eigen/Core" +#include #include namespace py = pybind11; @@ -58,88 +59,84 @@ namespace //select only the first node of the graph of each run, used for parameterstudy with single nodes template std::vector filter_graph_results( - std::vector, mio::MobilityEdge>>&& graph_results) + std::vector, mio::MobilityEdge>, + double, double>>&& graph_results) { std::vector results; results.reserve(graph_results.size()); for (auto i = size_t(0); i < graph_results.size(); ++i) { - results.emplace_back(std::move(graph_results[i].nodes()[0].property.get_simulation())); + results.emplace_back(std::move(graph_results[i].get_graph().nodes()[0].property.get_simulation())); } return std::move(results); } +/// Moves out graphs from GraphSimulation%s. Helps make the new ParameterStudy backwards compatible with older bindings. +template +std::vector extract_graph_from_graph_simulation(std::vector&& result_graph_sims) +{ + std::vector result_graphs; + result_graphs.reserve(result_graph_sims.size()); + for (const SimulationT& sim : result_graph_sims) { + result_graphs.emplace_back(std::move(sim.get_graph())); + } + return result_graphs; +} + /* * @brief bind ParameterStudy for any model */ -template +template void bind_ParameterStudy(py::module_& m, std::string const& name) { - pymio::bind_class, pymio::EnablePickling::Never>(m, name.c_str()) - .def(py::init(), py::arg("model"), py::arg("t0"), - py::arg("tmax"), py::arg("num_runs")) - .def(py::init>&, double, double, - double, size_t>(), - py::arg("model_graph"), py::arg("t0"), py::arg("tmax"), py::arg("dt"), py::arg("num_runs")) - .def_property("num_runs", &mio::ParameterStudy::get_num_runs, - &mio::ParameterStudy::set_num_runs) - .def_property("tmax", &mio::ParameterStudy::get_tmax, - &mio::ParameterStudy::set_tmax) - .def_property("t0", &mio::ParameterStudy::get_t0, - &mio::ParameterStudy::set_t0) - .def_property_readonly("model", py::overload_cast<>(&mio::ParameterStudy::get_model), - py::return_value_policy::reference_internal) - .def_property_readonly("model", - py::overload_cast<>(&mio::ParameterStudy::get_model, py::const_), + using GraphT = mio::Graph, mio::MobilityEdge>; + using SimulationT = mio::GraphSimulation; + using ParametersT = mio::Graph>; + using StudyT = mio::ParameterStudy2; + + const auto create_simulation = [](const ParametersT& g, double t0, double dt, size_t) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy), t0, dt, dt); + }; + + pymio::bind_class(m, name.c_str()) + .def(py::init(), py::arg("parameters"), py::arg("t0"), + py::arg("tmax"), py::arg("dt"), py::arg("num_runs")) + .def_property_readonly("num_runs", &StudyT::get_num_runs) + .def_property_readonly("tmax", &StudyT::get_tmax) + .def_property_readonly("t0", &StudyT::get_t0) + .def_property_readonly("dt", &StudyT::get_dt) + .def_property_readonly("model_graph", py::overload_cast<>(&StudyT::get_parameters), py::return_value_policy::reference_internal) - .def_property_readonly("model_graph", - py::overload_cast<>(&mio::ParameterStudy::get_model_graph), + .def_property_readonly("model_graph", py::overload_cast<>(&StudyT::get_parameters, py::const_), py::return_value_policy::reference_internal) - .def_property_readonly( - "model_graph", py::overload_cast<>(&mio::ParameterStudy::get_model_graph, py::const_), - py::return_value_policy::reference_internal) .def( "run", - [](mio::ParameterStudy& self, - std::function, mio::MobilityEdge>, - size_t)> - handle_result) { - self.run( - [](auto&& g) { - return draw_sample(g); - }, - [&handle_result](auto&& g, auto&& run_idx) { - //handle_result_function needs to return something - //we don't want to run an unknown python object through parameterstudies, so - //we just return 0 and ignore the list returned by run(). - //So python will behave slightly different than c++ - handle_result(std::move(g), run_idx); - return 0; - }); + [&create_simulation](StudyT& self, std::function handle_result) { + self.run(create_simulation, [&handle_result](auto&& g, auto&& run_idx) { + //handle_result_function needs to return something + //we don't want to run an unknown python object through parameterstudies, so + //we just return 0 and ignore the list returned by run(). + //So python will behave slightly different than c++ + handle_result(std::move(g.get_graph()), run_idx); + return 0; + }); }, py::arg("handle_result_func")) .def("run", - [](mio::ParameterStudy& self) { //default argument doesn't seem to work with functions - return self.run([](auto&& g) { - return draw_sample(g); - }); + [&create_simulation](StudyT& self) { //default argument doesn't seem to work with functions + return extract_graph_from_graph_simulation(self.run(create_simulation)); }) .def( "run_single", - [](mio::ParameterStudy& self, std::function handle_result) { - self.run( - [](auto&& g) { - return draw_sample(g); - }, - [&handle_result](auto&& r, auto&& run_idx) { - handle_result(std::move(r.nodes()[0].property.get_simulation()), run_idx); - return 0; - }); + [&create_simulation](StudyT& self, std::function handle_result) { + self.run(create_simulation, [&handle_result](auto&& r, auto&& run_idx) { + handle_result(std::move(r.get_graph().nodes()[0].property.get_simulation()), run_idx); + return 0; + }); }, py::arg("handle_result_func")) - .def("run_single", [](mio::ParameterStudy& self) { - return filter_graph_results(self.run([](auto&& g) { - return draw_sample(g); - })); + .def("run_single", [&create_simulation](StudyT& self) { + return filter_graph_results(self.run(create_simulation)); }); } @@ -287,11 +284,11 @@ PYBIND11_MODULE(_simulation_osecir, m) mio::osecir::InfectionState::InfectedSymptoms, mio::osecir::InfectionState::Recovered}; auto weights = std::vector{0., 0., 1.0, 1.0, 0.33, 0., 0.}; auto result = mio::set_edges, mio::MobilityParameters, - mio::MobilityCoefficientGroup, mio::osecir::InfectionState, - decltype(mio::read_mobility_plain)>(mobility_data_file, params_graph, - mobile_comp, contact_locations_size, - mio::read_mobility_plain, weights); + ContactLocation, mio::osecir::Model, mio::MobilityParameters, + mio::MobilityCoefficientGroup, mio::osecir::InfectionState, + decltype(mio::read_mobility_plain)>(mobility_data_file, params_graph, + mobile_comp, contact_locations_size, + mio::read_mobility_plain, weights); return pymio::check_and_throw(result); }, py::return_value_policy::move); diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp index 8e1e922404..7bb1210402 100755 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp @@ -52,95 +52,110 @@ namespace //select only the first node of the graph of each run, used for parameterstudy with single nodes template std::vector filter_graph_results( - std::vector, mio::MobilityEdge>>&& graph_results) + std::vector, mio::MobilityEdge>, + double, double>>&& graph_results) { std::vector results; results.reserve(graph_results.size()); for (auto i = size_t(0); i < graph_results.size(); ++i) { - results.emplace_back(std::move(graph_results[i].nodes()[0].property.get_simulation())); + results.emplace_back(std::move(graph_results[i].get_graph().nodes()[0].property.get_simulation())); } return std::move(results); } +/// Moves out graphs from GraphSimulation%s. Helps make the new ParameterStudy backwards compatible with older bindings. +template +std::vector extract_graph_from_graph_simulation(std::vector&& result_graph_sims) +{ + std::vector result_graphs; + result_graphs.reserve(result_graph_sims.size()); + for (const SimulationT& sim : result_graph_sims) { + result_graphs.emplace_back(std::move(sim.get_graph())); + } + return result_graphs; +} + /* * @brief bind ParameterStudy for any model */ -template +template void bind_ParameterStudy(py::module_& m, std::string const& name) { - pymio::bind_class, pymio::EnablePickling::Never>(m, name.c_str()) - .def(py::init(), py::arg("model"), py::arg("t0"), - py::arg("tmax"), py::arg("num_runs")) - .def(py::init>&, double, double, - double, size_t>(), - py::arg("model_graph"), py::arg("t0"), py::arg("tmax"), py::arg("dt"), py::arg("num_runs")) - .def_property("num_runs", &mio::ParameterStudy::get_num_runs, - &mio::ParameterStudy::set_num_runs) - .def_property("tmax", &mio::ParameterStudy::get_tmax, - &mio::ParameterStudy::set_tmax) - .def_property("t0", &mio::ParameterStudy::get_t0, - &mio::ParameterStudy::set_t0) - .def_property_readonly("model", py::overload_cast<>(&mio::ParameterStudy::get_model), - py::return_value_policy::reference_internal) - .def_property_readonly("model", - py::overload_cast<>(&mio::ParameterStudy::get_model, py::const_), + using GraphT = mio::Graph, mio::MobilityEdge>; + using SimulationT = mio::GraphSimulation; + using ParametersT = mio::Graph>; + using StudyT = mio::ParameterStudy2; + + pymio::bind_class(m, name.c_str()) + .def(py::init(), py::arg("parameters"), py::arg("t0"), + py::arg("tmax"), py::arg("dt"), py::arg("num_runs")) + .def_property_readonly("num_runs", &StudyT::get_num_runs) + .def_property_readonly("tmax", &StudyT::get_tmax) + .def_property_readonly("t0", &StudyT::get_t0) + .def_property_readonly("dt", &StudyT::get_dt) + + .def_property_readonly("model_graph", py::overload_cast<>(&StudyT::get_parameters), py::return_value_policy::reference_internal) - .def_property_readonly("model_graph", - py::overload_cast<>(&mio::ParameterStudy::get_model_graph), + .def_property_readonly("model_graph", py::overload_cast<>(&StudyT::get_parameters, py::const_), py::return_value_policy::reference_internal) - .def_property_readonly( - "model_graph", py::overload_cast<>(&mio::ParameterStudy::get_model_graph, py::const_), - py::return_value_policy::reference_internal) .def( "run", - [](mio::ParameterStudy& self, - std::function, mio::MobilityEdge>, - size_t)> + [](StudyT& self, + std::function, mio::MobilityEdge>, size_t)> handle_result, bool variant_high) { self.run( - [variant_high](auto&& g) { - return draw_sample(g, variant_high); + [variant_high](const ParametersT& g, double t0, double dt, size_t) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), + t0, dt, dt); }, [&handle_result](auto&& g, auto&& run_idx) { //handle_result_function needs to return something //we don't want to run an unknown python object through parameterstudies, so //we just return 0 and ignore the list returned by run(). //So python will behave slightly different than c++ - handle_result(std::move(g), run_idx); + handle_result(std::move(g.get_graph()), run_idx); return 0; }); }, py::arg("handle_result_func"), py::arg("variant_high")) .def( "run", - [](mio::ParameterStudy& self, + [](StudyT& self, bool variant_high) { //default argument doesn't seem to work with functions - return self.run([variant_high](auto&& g) { - return draw_sample(g, variant_high); - }); + return extract_graph_from_graph_simulation( + self.run([variant_high](const ParametersT& g, double t0, double dt, size_t) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), + t0, dt, dt); + })); }, py::arg("variant_high")) .def( "run_single", - [](mio::ParameterStudy& self, std::function handle_result, - bool variant_high) { + [](StudyT& self, std::function handle_result, bool variant_high) { self.run( - [variant_high](auto&& g) { - return draw_sample(g, variant_high); + [variant_high](const ParametersT& g, double t0, double dt, size_t) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), + t0, dt, dt); }, [&handle_result](auto&& r, auto&& run_idx) { - handle_result(std::move(r.nodes()[0].property.get_simulation()), run_idx); + handle_result(std::move(r.get_graph().nodes()[0].property.get_simulation()), run_idx); return 0; }); }, py::arg("handle_result_func"), py::arg("variant_high")) .def( "run_single", - [](mio::ParameterStudy& self, bool variant_high) { - return filter_graph_results(self.run([variant_high](auto&& g) { - return draw_sample(g, variant_high); - })); + [](StudyT& self, bool variant_high) { + return filter_graph_results( + self.run([variant_high](const ParametersT& g, double t0, double dt, size_t) { + auto copy = g; + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), + t0, dt, dt); + })); }, py::arg("variant_high")); } @@ -340,9 +355,9 @@ PYBIND11_MODULE(_simulation_osecirvvs, m) mio::osecirvvs::InfectionState::InfectedSymptomsImprovedImmunity}; auto weights = std::vector{0., 0., 1.0, 1.0, 0.33, 0., 0.}; auto result = mio::set_edges, - mio::MobilityParameters, mio::MobilityCoefficientGroup, - mio::osecirvvs::InfectionState, decltype(mio::read_mobility_plain)>( + ContactLocation, mio::osecirvvs::Model, + mio::MobilityParameters, mio::MobilityCoefficientGroup, + mio::osecirvvs::InfectionState, decltype(mio::read_mobility_plain)>( mobility_data_file, params_graph, mobile_comp, contact_locations_size, mio::read_mobility_plain, weights); return pymio::check_and_throw(result); diff --git a/pycode/memilio-simulation/memilio/simulation_test/test_parameter_study.py b/pycode/memilio-simulation/memilio/simulation_test/test_parameter_study.py index c636e77a7c..2c5a2c1414 100644 --- a/pycode/memilio-simulation/memilio/simulation_test/test_parameter_study.py +++ b/pycode/memilio-simulation/memilio/simulation_test/test_parameter_study.py @@ -85,12 +85,14 @@ def test_graph(self): def test_run(self): """ """ - model = self._get_model() + graph = osecir.ModelGraph() + graph.add_node(0, self._get_model()) t0 = 1 tmax = 10 + dt = 0.1 num_runs = 3 - study = osecir.ParameterStudy(model, t0, tmax, num_runs) + study = osecir.ParameterStudy(graph, t0, tmax, dt, num_runs) self.assertEqual(study.t0, t0) self.assertEqual(study.tmax, tmax) From df4f000e6ac3151e1f55ad5f05ab91079ec62fe2 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:46:10 +0200 Subject: [PATCH 087/169] explicit type conversion --- cpp/examples/abm_minimal.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index 4cace851ba..23e8e9ad51 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -33,6 +33,7 @@ #include #include +#include #include constexpr size_t num_age_groups = 4; @@ -227,7 +228,7 @@ int main() [](auto&&, auto t0_, auto, size_t run_idx) { // TODO: change the parameters around a bit? - auto sim = ResultSim(make_model(run_idx), t0_); + auto sim = ResultSim(make_model((uint32_t)run_idx), t0_); return sim; }, [](auto&& sim, auto&& run_idx) { From 2d278c0e389c2849f55b5e16b0fa79dd74bed14e Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:22:20 +0200 Subject: [PATCH 088/169] another explicit type conversion --- cpp/examples/ode_secir_parameter_study.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/examples/ode_secir_parameter_study.cpp b/cpp/examples/ode_secir_parameter_study.cpp index 751b0b6745..60981be83e 100644 --- a/cpp/examples/ode_secir_parameter_study.cpp +++ b/cpp/examples/ode_secir_parameter_study.cpp @@ -53,7 +53,7 @@ mio::IOResult write_single_run_result(const size_t run, const mio::osecir: std::vector> all_results; std::vector ids; - BOOST_OUTCOME_TRY(mio::save_result({sim.get_result()}, {0}, sim.get_model().parameters.get_num_groups().get(), + BOOST_OUTCOME_TRY(mio::save_result({sim.get_result()}, {0}, (int)sim.get_model().parameters.get_num_groups().get(), mio::path_join(abs_path, ("Results_run" + std::to_string(run) + ".h5")))); return mio::success(); From 5fa14f43451ebf81f482d5ccf6ef263e822e133c Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:15:45 +0200 Subject: [PATCH 089/169] improve coverage --- cpp/tests/test_io_cli.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cpp/tests/test_io_cli.cpp b/cpp/tests/test_io_cli.cpp index 1b70f67681..0721044ceb 100644 --- a/cpp/tests/test_io_cli.cpp +++ b/cpp/tests/test_io_cli.cpp @@ -301,18 +301,24 @@ TEST_F(TestCLI, test_get_param) TEST_F(TestCLI, test_set_param) { Params p; - auto set = mio::cli::details::AbstractSet::build(p).value(); - auto id = mio::cli::details::Identifier::make_raw("A"); + auto set = mio::cli::details::AbstractSet::build(p).value(); + auto id_A = mio::cli::details::Identifier::make_raw("A"); + auto id_B = mio::cli::details::Identifier::make_raw("B"); + auto wrong_id = mio::cli::details::Identifier::make_raw("NotAnOption"); // use normally - EXPECT_THAT(print_wrap(set.set_param(id, std::string("5.2"))), IsSuccess()); + EXPECT_THAT(print_wrap(set.set_param(id_A, std::string("5.2"))), IsSuccess()); EXPECT_EQ(p.get(), 5.2); + EXPECT_THAT(print_wrap(set.set_param(id_B, std::string("\"5.2\""))), IsSuccess()); + EXPECT_EQ(p.get(), "5.2"); // cause errors - auto result = set.set_param(id, std::string("definitely a double")); + auto result = set.set_param(id_A, std::string("definitely a double")); EXPECT_THAT(print_wrap(result), IsFailure(mio::StatusCode::InvalidType)); - EXPECT_EQ(result.error().formatted_message(), - "Invalid type: While setting \"A\": Json value is not a double.\n" - "* Line 1, Column 1\n Syntax error: value, object or array expected.\n"); - result = set.set_param(mio::cli::details::Identifier::make_raw("NotAnOption"), std::string()); + EXPECT_EQ(result.error().message(), "While setting \"A\": Json value is not a double.\n" + "* Line 1, Column 1\n Syntax error: value, object or array expected.\n"); + result = set.set_param(id_B, std::string("this is string is missing quotes")); + EXPECT_THAT(print_wrap(result), IsFailure(mio::StatusCode::InvalidType)); + EXPECT_THAT(result.error().message(), testing::HasSubstr("\\\"")); // check only for additional hint + result = set.set_param(wrong_id, std::string()); EXPECT_THAT(print_wrap(result), IsFailure(mio::StatusCode::KeyNotFound)); EXPECT_EQ(result.error().formatted_message(), "Key not found: Could not set parameter: No such option \"NotAnOption\"."); From 1e90200986aafec56c2a9b7ddb03cec65f5a973e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:57:00 +0200 Subject: [PATCH 090/169] CHG: minor possible improvements --- cpp/examples/asymmetric_graph.cpp | 14 +++++++++----- cpp/memilio/mobility/graph.h | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 74c41f7e56..0677ef884b 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -31,6 +31,7 @@ #include "smm/parameters.h" #include "thirdparty/csv.h" #include +#include enum class InfectionState { @@ -38,13 +39,14 @@ enum class InfectionState E, I, R, + D, Count }; int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; - const auto tmax = 100.; + const auto tmax = 1000.; const auto dt = 1.; //initial time step //total compartment sizes @@ -91,7 +93,9 @@ int main(int /*argc*/, char** /*argv*/) size_t from, to; while (edges.read_row(from, to)) { graph.add_edge(from, to, interesting_indices); + // graph.lazy_add_edge(from, to, interesting_indices); } + // graph.sort_edges(); mio::log_info("Graph generated"); @@ -102,7 +106,7 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {10.0})); + node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {2.0})); } mio::log_info("Neighbors set"); @@ -145,8 +149,7 @@ int main(int /*argc*/, char** /*argv*/) // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); - // sim2.exchanges_per_timestep().print_table({"Commuter Sick", "Commuter Total"}); - // auto num_time_points = sim2.exchanges_per_timestep().get_num_time_points(); + auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); // for (auto node : sim2.get_graph().nodes()) { // if (node.property.get_result().get_num_time_points() < num_time_points) { @@ -157,12 +160,13 @@ int main(int /*argc*/, char** /*argv*/) // exchange_results = sim2.sum_nodes(); // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); - // auto sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); + sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); // // auto combined_results = sim2.combine_node_results(); // // combined_results.print_table({"S", "E", "I", "R", "D"}); // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); + mio::log_info("Finished postprocessing"); return 0; } diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 0aa4cb56b7..d2e9d47c62 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -180,6 +180,28 @@ class Graph }); } + template + Edge& lazy_add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) + { + assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); + for (auto& edge : m_edges) { + if (edge.start_node_idx == start_node_idx && edge.end_node_idx == end_node_idx) { + return m_edges.back(); + } + } + m_edges.emplace_back(start_node_idx, end_node_idx, std::forward(args)...); + return m_edges.back(); + } + + Edge& sort_edges() + { + std::sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx + : e1.start_node_idx < e2.start_node_idx; + }); + return m_edges.back(); + } + /** * @brief range of nodes */ From f505d3509e1eeef8f0971a13d7acd978c4cd4e12 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:59:25 +0200 Subject: [PATCH 091/169] CHG: New compartmental model --- cpp/examples/asymmetric_graph.cpp | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 0677ef884b..443f0728a0 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -38,6 +38,8 @@ enum class InfectionState S, E, I, + INS, + ICS, R, D, Count @@ -46,22 +48,30 @@ enum class InfectionState int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; - const auto tmax = 1000.; + const auto tmax = 100.; const auto dt = 1.; //initial time step //total compartment sizes using Model = mio::smm::Model; auto home = mio::regions::Region(0); + auto S = InfectionState::S; + auto E = InfectionState::E; + auto I = InfectionState::I; + auto INS = InfectionState::INS; + auto ICS = InfectionState::ICS; + auto R = InfectionState::R; + auto D = InfectionState::D; std::vector> adoption_rates; - adoption_rates.push_back({InfectionState::E, InfectionState::I, home, 0.2, {}}); - adoption_rates.push_back({InfectionState::I, InfectionState::R, home, 0.1, {}}); - adoption_rates.push_back({InfectionState::S, InfectionState::E, home, 0.2, {{InfectionState::I, 0.8}}}); - adoption_rates.push_back({InfectionState::S, InfectionState::D, home, 0.0, {}}); - adoption_rates.push_back({InfectionState::E, InfectionState::D, home, 0.0, {}}); - adoption_rates.push_back({InfectionState::I, InfectionState::D, home, 0.0, {}}); - adoption_rates.push_back({InfectionState::R, InfectionState::D, home, 0.0, {}}); + // Adoption rates corresponding to our model, paramters are arbitrary + adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); + adoption_rates.push_back({E, I, home, 0.2, {}}); + adoption_rates.push_back({I, INS, home, 0.1, {}}); + adoption_rates.push_back({I, ICS, home, 0.1, {}}); + adoption_rates.push_back({ICS, D, home, 0.6, {}}); + adoption_rates.push_back({ICS, R, home, 0.4, {}}); + adoption_rates.push_back({INS, R, home, 0.5, {}}); mio::Graph>, mio::MobilityEdgeDirected> @@ -75,8 +85,10 @@ int main(int /*argc*/, char** /*argv*/) while (farms.read_row(farm_id, num_cows, latitude, longitude)) { Model curr_model; curr_model.populations[{home, InfectionState::S}] = num_cows; - curr_model.populations[{home, InfectionState::E}] = 2; + curr_model.populations[{home, InfectionState::E}] = 1; curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; curr_model.populations[{home, InfectionState::R}] = 0; curr_model.populations[{home, InfectionState::D}] = 0; curr_model.parameters.get>() = adoption_rates; From c6e292aaab4658f226be168d1be555bd3c9b79a4 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:24:04 +0200 Subject: [PATCH 092/169] move abm study, restore minimal abm --- cpp/examples/CMakeLists.txt | 4 + cpp/examples/abm_minimal.cpp | 165 +++---------------- cpp/examples/abm_parameter_study.cpp | 230 +++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 142 deletions(-) create mode 100644 cpp/examples/abm_parameter_study.cpp diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 39a12539e1..d303ac241e 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -108,6 +108,10 @@ add_executable(abm_minimal_example abm_minimal.cpp) target_link_libraries(abm_minimal_example PRIVATE memilio abm) target_compile_options(abm_minimal_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +add_executable(abm_parameter_study_example abm_parameter_study.cpp) +target_link_libraries(abm_parameter_study_example PRIVATE memilio abm) +target_compile_options(abm_parameter_study_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) + add_executable(abm_history_example abm_history_object.cpp) target_link_libraries(abm_history_example PRIVATE memilio abm) target_compile_options(abm_history_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index 23e8e9ad51..643a17b1df 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -17,76 +17,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include "abm/simulation.h" -#include "abm/common_abm_loggers.h" -#include #include "abm/household.h" #include "abm/lockdown_rules.h" #include "abm/model.h" -#include "abm/time.h" -#include "memilio/compartments/parameter_studies.h" -#include "memilio/data/analyze_result.h" -#include "memilio/io/result_io.h" -#include "memilio/utils/abstract_parameter_distribution.h" -#include "memilio/utils/miompi.h" -#include "memilio/utils/time_series.h" +#include "abm/common_abm_loggers.h" -#include -#include -#include #include -constexpr size_t num_age_groups = 4; - -// shim class to make the abm simulation look like an ode simulation -class ResultSim : public mio::abm::Simulation -{ -public: - using Model = mio::abm::Model; - - ResultSim(const Model& m, double t, double) - : mio::abm::Simulation(mio::abm::TimePoint{mio::abm::days(t).seconds()}, Model(m)) - { - history.log(*this); // set initial results - } - - ResultSim(const Model& m, mio::abm::TimePoint t) - : mio::abm::Simulation(t, Model(m)) - { - history.log(*this); // set initial results - } - - void advance(mio::abm::TimePoint tmax) - { - mio::abm::Simulation::advance(tmax, history); - } - - const mio::TimeSeries& get_result() const - { - return get<0>(history.get_log()); - } - - mio::History history{ - Eigen::Index(mio::abm::InfectionState::Count)}; -}; - -mio::abm::Model make_model(uint32_t run_idx) +int main() { - + // This is a minimal example with children and adults < 60 year old. + // We divided them into 4 different age groups, which are defined as follows: + mio::set_log_level(mio::LogLevel::warn); + size_t num_age_groups = 4; const auto age_group_0_to_4 = mio::AgeGroup(0); const auto age_group_5_to_14 = mio::AgeGroup(1); const auto age_group_15_to_34 = mio::AgeGroup(2); const auto age_group_35_to_59 = mio::AgeGroup(3); - // Create the model with 4 age groups. - auto model = mio::abm::Model(num_age_groups); - std::initializer_list seeds = {14159265, 243141u + run_idx}; - auto rng = mio::RandomNumberGenerator(); - rng.seed(seeds); - auto run_rng_counter = - mio::rng_totalsequence_counter(static_cast(run_idx), mio::Counter(0)); - rng.set_counter(run_rng_counter); - model.get_rng() = rng; + // Create the model with 4 age groups. + auto model = mio::abm::Model(num_age_groups); // Set same infection parameter for all age groups. For example, the incubation period is log normally distributed with parameters 4 and 1. model.parameters.get() = mio::ParameterDistributionLogNormal(4., 1.); @@ -100,7 +50,7 @@ mio::abm::Model make_model(uint32_t run_idx) model.parameters.check_constraints(); // There are 10 households for each household group. - int n_households = 100; + int n_households = 10; // For more than 1 family households we need families. These are parents and children and randoms (which are distributed like the data we have for these households). auto child = mio::abm::HouseholdMember(num_age_groups); // A child is 50/50% 0-4 or 5-14. @@ -171,9 +121,9 @@ mio::abm::Model make_model(uint32_t run_idx) for (auto& person : model.get_persons()) { mio::abm::InfectionState infection_state = mio::abm::InfectionState( mio::DiscreteDistribution::get_instance()(mio::thread_local_rng(), infection_distribution)); - auto person_rng = mio::abm::PersonalRandomNumberGenerator(person); + auto rng = mio::abm::PersonalRandomNumberGenerator(person); if (infection_state != mio::abm::InfectionState::Susceptible) { - person.add_new_infection(mio::abm::Infection(person_rng, mio::abm::VirusVariant::Wildtype, person.get_age(), + person.add_new_infection(mio::abm::Infection(rng, mio::abm::VirusVariant::Wildtype, person.get_age(), model.parameters, start_date, infection_state)); } } @@ -200,95 +150,26 @@ mio::abm::Model make_model(uint32_t run_idx) auto t_lockdown = mio::abm::TimePoint(0) + mio::abm::days(10); mio::abm::close_social_events(t_lockdown, 0.9, model.parameters); - return model; -} - -int main() -{ - mio::mpi::init(); - // This is a minimal example with children and adults < 60 year old. - // We divided them into 4 different age groups, which are defined as follows: - - mio::set_log_level(mio::LogLevel::warn); - // Set start and end time for the simulation. auto t0 = mio::abm::TimePoint(0); - auto tmax = t0 + mio::abm::days(5); - // auto sim = mio::abm::Simulation(t0, std::move(model)); - const size_t num_runs = 10; + auto tmax = t0 + mio::abm::days(10); + auto sim = mio::abm::Simulation(t0, std::move(model)); // Create a history object to store the time series of the infection states. mio::History historyTimeSeries{ Eigen::Index(mio::abm::InfectionState::Count)}; - mio::ParameterStudy2 study( - 1, t0, tmax, mio::abm::TimeSpan(0), num_runs); - - auto ensemble = study.run( - [](auto&&, auto t0_, auto, size_t run_idx) { - // TODO: change the parameters around a bit? - - auto sim = ResultSim(make_model((uint32_t)run_idx), t0_); - return sim; - }, - [](auto&& sim, auto&& run_idx) { - auto interpolated_result = mio::interpolate_simulation_result(sim.get_result()); - std::string name_run = "abm_minimal_run_" + std::to_string(run_idx) + ".txt"; - std::ofstream outfile_run(name_run); - sim.get_result().print_table(outfile_run, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); - std::cout << "Results written to " << name_run << std::endl; - auto params = std::vector{}; - return interpolated_result; - }); - - if (ensemble.size() > 0) { - auto ensemble_results = std::vector>>{}; - ensemble_results.reserve(ensemble.size()); - auto ensemble_params = std::vector>{}; - ensemble_params.reserve(ensemble.size()); - // int j = 0; - // std::cout << "## " << ensemble[0].first.get_value(0).transpose() << "\n"; - // for (auto&& run : ensemble) { - // std::cout << j++ << " " << run.first.get_last_value().transpose() << "\n"; - // ensemble_results.emplace_back(std::vector{std::move(run.first)}); - // ensemble_params.emplace_back(std::vector{std::move(run.second)}); - // } - - // auto ensemble_results_p05 = ensemble_percentile(ensemble_results, 0.05); - // auto ensemble_results_p25 = ensemble_percentile(ensemble_results, 0.25); - // auto ensemble_results_p50 = ensemble_percentile(ensemble_results, 0.50); - // auto ensemble_results_p75 = ensemble_percentile(ensemble_results, 0.75); - // auto ensemble_results_p95 = ensemble_percentile(ensemble_results, 0.95); - - // const boost::filesystem::path save_dir = "/Users/saschakorf/Nosynch/Arbeit/memilio/cpp/examples/results"; - - // if (!save_dir.empty()) { - - // mio::unused(save_result(ensemble_results_p05, {0}, num_age_groups, - // (save_dir / ("Results_" + std::string("p05") + ".h5")).string())); - // mio::unused(save_result(ensemble_results_p25, {0}, num_age_groups, - // (save_dir / ("Results_" + std::string("p25") + ".h5")).string())); - // mio::unused(save_result(ensemble_results_p50, {0}, num_age_groups, - // (save_dir / ("Results_" + std::string("p50") + ".h5")).string())); - // mio::unused(save_result(ensemble_results_p75, {0}, num_age_groups, - // (save_dir / ("Results_" + std::string("p75") + ".h5")).string())); - // mio::unused(save_result(ensemble_results_p95, {0}, num_age_groups, - // (save_dir / ("Results_" + std::string("p95") + ".h5")).string())); - // } - } // Run the simulation until tmax with the history object. - // sim.advance(tmax, historyTimeSeries); - - // // The results are written into the file "abm_minimal.txt" as a table with 9 columns. - // // The first column is Time. The other columns correspond to the number of people with a certain infection state at this Time: - // // Time = Time in days, S = Susceptible, E = Exposed, I_NS = InfectedNoSymptoms, I_Sy = InfectedSymptoms, I_Sev = InfectedSevere, - // // I_Crit = InfectedCritical, R = Recovered, D = Dead - // std::ofstream outfile("abm_minimal.txt"); - // std::get<0>(historyTimeSeries.get_log()) - // .print_table(outfile, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); - // std::cout << "Results written to abm_minimal.txt" << std::endl; - - mio::mpi::finalize(); + sim.advance(tmax, historyTimeSeries); + + // The results are written into the file "abm_minimal.txt" as a table with 9 columns. + // The first column is Time. The other columns correspond to the number of people with a certain infection state at this Time: + // Time = Time in days, S = Susceptible, E = Exposed, I_NS = InfectedNoSymptoms, I_Sy = InfectedSymptoms, I_Sev = InfectedSevere, + // I_Crit = InfectedCritical, R = Recovered, D = Dead + std::ofstream outfile("abm_minimal.txt"); + std::get<0>(historyTimeSeries.get_log()) + .print_table(outfile, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); + std::cout << "Results written to abm_minimal.txt" << std::endl; return 0; } diff --git a/cpp/examples/abm_parameter_study.cpp b/cpp/examples/abm_parameter_study.cpp new file mode 100644 index 0000000000..326cbdc12a --- /dev/null +++ b/cpp/examples/abm_parameter_study.cpp @@ -0,0 +1,230 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Rene Schmieding, Sascha Korf +* +* 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 "abm/result_simulation.h" +#include "abm/household.h" +#include "abm/lockdown_rules.h" +#include "abm/model.h" +#include "abm/time.h" + +#include "memilio/compartments/parameter_studies.h" +#include "memilio/data/analyze_result.h" +#include "memilio/io/io.h" +#include "memilio/io/result_io.h" +#include "memilio/utils/base_dir.h" +#include "memilio/utils/logging.h" +#include "memilio/utils/miompi.h" +#include "memilio/utils/random_number_generator.h" +#include "memilio/utils/stl_util.h" + +#include + +constexpr size_t num_age_groups = 4; + +/// An ABM setup taken from abm_minimal.cpp. +mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) +{ + + const auto age_group_0_to_4 = mio::AgeGroup(0); + const auto age_group_5_to_14 = mio::AgeGroup(1); + const auto age_group_15_to_34 = mio::AgeGroup(2); + const auto age_group_35_to_59 = mio::AgeGroup(3); + // Create the model with 4 age groups. + auto model = mio::abm::Model(num_age_groups); + model.get_rng() = rng; + + // Set same infection parameter for all age groups. For example, the incubation period is log normally distributed with parameters 4 and 1. + model.parameters.get() = mio::ParameterDistributionLogNormal(4., 1.); + + // Set the age group the can go to school is AgeGroup(1) (i.e. 5-14) + model.parameters.get() = false; + model.parameters.get()[age_group_5_to_14] = true; + // Set the age group the can go to work is AgeGroup(2) and AgeGroup(3) (i.e. 15-34 and 35-59) + model.parameters.get().set_multiple({age_group_15_to_34, age_group_35_to_59}, true); + + // Check if the parameters satisfy their contraints. + model.parameters.check_constraints(); + + // There are 10 households for each household group. + int n_households = 100; + + // For more than 1 family households we need families. These are parents and children and randoms (which are distributed like the data we have for these households). + auto child = mio::abm::HouseholdMember(num_age_groups); // A child is 50/50% 0-4 or 5-14. + child.set_age_weight(age_group_0_to_4, 1); + child.set_age_weight(age_group_5_to_14, 1); + + auto parent = mio::abm::HouseholdMember(num_age_groups); // A parent is 50/50% 15-34 or 35-59. + parent.set_age_weight(age_group_15_to_34, 1); + parent.set_age_weight(age_group_35_to_59, 1); + + // Two-person household with one parent and one child. + auto twoPersonHousehold_group = mio::abm::HouseholdGroup(); + auto twoPersonHousehold_full = mio::abm::Household(); + twoPersonHousehold_full.add_members(child, 1); + twoPersonHousehold_full.add_members(parent, 1); + twoPersonHousehold_group.add_households(twoPersonHousehold_full, n_households); + add_household_group_to_model(model, twoPersonHousehold_group); + + // Three-person household with two parent and one child. + auto threePersonHousehold_group = mio::abm::HouseholdGroup(); + auto threePersonHousehold_full = mio::abm::Household(); + threePersonHousehold_full.add_members(child, 1); + threePersonHousehold_full.add_members(parent, 2); + threePersonHousehold_group.add_households(threePersonHousehold_full, n_households); + add_household_group_to_model(model, threePersonHousehold_group); + + // Add one social event with 5 maximum contacts. + // Maximum contacs limit the number of people that a person can infect while being at this location. + auto event = model.add_location(mio::abm::LocationType::SocialEvent); + model.get_location(event).get_infection_parameters().set(5); + // Add hospital and ICU with 5 maximum contacs. + auto hospital = model.add_location(mio::abm::LocationType::Hospital); + model.get_location(hospital).get_infection_parameters().set(5); + auto icu = model.add_location(mio::abm::LocationType::ICU); + model.get_location(icu).get_infection_parameters().set(5); + // Add one supermarket, maximum constacts are assumed to be 20. + auto shop = model.add_location(mio::abm::LocationType::BasicsShop); + model.get_location(shop).get_infection_parameters().set(20); + // At every school, the maximum contacts are 20. + auto school = model.add_location(mio::abm::LocationType::School); + model.get_location(school).get_infection_parameters().set(20); + // At every workplace, maximum contacts are 20. + auto work = model.add_location(mio::abm::LocationType::Work); + model.get_location(work).get_infection_parameters().set(20); + + // Increase aerosol transmission for all locations + model.parameters.get() = 10.0; + // Increase contact rate for all people between 15 and 34 (i.e. people meet more often in the same location) + model.get_location(work) + .get_infection_parameters() + .get()[{age_group_15_to_34, age_group_15_to_34}] = 10.0; + + // People can get tested at work (and do this with 0.5 probability) from time point 0 to day 10. + auto validity_period = mio::abm::days(1); + auto probability = 0.5; + auto start_date = mio::abm::TimePoint(0); + auto end_date = mio::abm::TimePoint(0) + mio::abm::days(10); + auto test_type = mio::abm::TestType::Antigen; + auto test_parameters = model.parameters.get()[test_type]; + auto testing_criteria_work = mio::abm::TestingCriteria(); + auto testing_scheme_work = mio::abm::TestingScheme(testing_criteria_work, validity_period, start_date, end_date, + test_parameters, probability); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::Work, testing_scheme_work); + + // Assign infection state to each person. + // The infection states are chosen randomly with the following distribution + std::vector infection_distribution{0.5, 0.3, 0.05, 0.05, 0.05, 0.05, 0.0, 0.0}; + for (auto& person : model.get_persons()) { + mio::abm::InfectionState infection_state = mio::abm::InfectionState( + mio::DiscreteDistribution::get_instance()(mio::thread_local_rng(), infection_distribution)); + auto person_rng = mio::abm::PersonalRandomNumberGenerator(person); + if (infection_state != mio::abm::InfectionState::Susceptible) { + person.add_new_infection(mio::abm::Infection(person_rng, mio::abm::VirusVariant::Wildtype, person.get_age(), + model.parameters, start_date, infection_state)); + } + } + + // Assign locations to the people + for (auto& person : model.get_persons()) { + const auto id = person.get_id(); + //assign shop and event + model.assign_location(id, event); + model.assign_location(id, shop); + //assign hospital and ICU + model.assign_location(id, hospital); + model.assign_location(id, icu); + //assign work/school to people depending on their age + if (person.get_age() == age_group_5_to_14) { + model.assign_location(id, school); + } + if (person.get_age() == age_group_15_to_34 || person.get_age() == age_group_35_to_59) { + model.assign_location(id, work); + } + } + + // During the lockdown, social events are closed for 90% of people. + auto t_lockdown = mio::abm::TimePoint(0) + mio::abm::days(10); + mio::abm::close_social_events(t_lockdown, 0.9, model.parameters); + + return model; +} + +int main() +{ + mio::mpi::init(); + + mio::set_log_level(mio::LogLevel::warn); + + // Set start and end time for the simulation. + auto t0 = mio::abm::TimePoint(0); + auto tmax = t0 + mio::abm::days(5); + // auto sim = mio::abm::Simulation(t0, std::move(model)); + const size_t num_runs = 10; + + // Create a parameter study. We currently do not use Parameters or dt, so we set them to 0. + mio::ParameterStudy2, int, mio::abm::TimePoint, mio::abm::TimeSpan> + study(0, t0, tmax, mio::abm::TimeSpan(0), num_runs); + + // Optional: set seeds to get reproducable results + // study.get_rng().seed({12341234, 53456, 63451, 5232576, 84586, 52345}); + + const std::string result_dir = mio::path_join(mio::base_dir(), "example_results"); + if (!mio::create_directory(result_dir)) { + mio::log_error("Could not create result directory \"{}\".", result_dir); + return 1; + } + + auto ensemble_results = study.run( + [](auto, auto t0_, auto, size_t) { + auto sim = mio::abm::ResultSimulation(make_model(mio::thread_local_rng()), t0_); + return sim; + }, + [result_dir](auto&& sim, auto&& run_idx) { + auto interpolated_result = mio::interpolate_simulation_result(sim.get_result()); + std::string outpath = mio::path_join(result_dir, "abm_minimal_run_" + std::to_string(run_idx) + ".txt"); + std::ofstream outfile_run(outpath); + sim.get_result().print_table(outfile_run, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4); + std::cout << "Results written to " << outpath << std::endl; + auto params = std::vector{}; + return std::vector{interpolated_result}; + }); + + if (ensemble_results.size() > 0) { + auto ensemble_results_p05 = ensemble_percentile(ensemble_results, 0.05); + auto ensemble_results_p25 = ensemble_percentile(ensemble_results, 0.25); + auto ensemble_results_p50 = ensemble_percentile(ensemble_results, 0.50); + auto ensemble_results_p75 = ensemble_percentile(ensemble_results, 0.75); + auto ensemble_results_p95 = ensemble_percentile(ensemble_results, 0.95); + + mio::unused(save_result(ensemble_results_p05, {0}, num_age_groups, + mio::path_join(result_dir, "Results_" + std::string("p05") + ".h5"))); + mio::unused(save_result(ensemble_results_p25, {0}, num_age_groups, + mio::path_join(result_dir, "Results_" + std::string("p25") + ".h5"))); + mio::unused(save_result(ensemble_results_p50, {0}, num_age_groups, + mio::path_join(result_dir, "Results_" + std::string("p50") + ".h5"))); + mio::unused(save_result(ensemble_results_p75, {0}, num_age_groups, + mio::path_join(result_dir, "Results_" + std::string("p75") + ".h5"))); + mio::unused(save_result(ensemble_results_p95, {0}, num_age_groups, + mio::path_join(result_dir, "Results_" + std::string("p95") + ".h5"))); + } + + mio::mpi::finalize(); + + return 0; +} From 3af580cc798784582b9f163f99e607b1b6e75647 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Thu, 23 Oct 2025 18:52:35 +0200 Subject: [PATCH 093/169] clean up examples, improve mpi handling --- cpp/examples/abm_parameter_study.cpp | 4 +- cpp/examples/ode_secir_parameter_study.cpp | 26 ++-- cpp/examples/ode_secir_read_graph.cpp | 2 +- cpp/memilio/compartments/parameter_studies.h | 154 +++++++++++++------ cpp/memilio/io/io.h | 19 +++ cpp/memilio/utils/miompi.cpp | 3 + cpp/models/abm/CMakeLists.txt | 1 + cpp/models/abm/result_simulation.h | 65 ++++++++ 8 files changed, 212 insertions(+), 62 deletions(-) create mode 100644 cpp/models/abm/result_simulation.h diff --git a/cpp/examples/abm_parameter_study.cpp b/cpp/examples/abm_parameter_study.cpp index 326cbdc12a..183c830c28 100644 --- a/cpp/examples/abm_parameter_study.cpp +++ b/cpp/examples/abm_parameter_study.cpp @@ -62,7 +62,7 @@ mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) model.parameters.check_constraints(); // There are 10 households for each household group. - int n_households = 100; + int n_households = 10; // For more than 1 family households we need families. These are parents and children and randoms (which are distributed like the data we have for these households). auto child = mio::abm::HouseholdMember(num_age_groups); // A child is 50/50% 0-4 or 5-14. @@ -175,7 +175,7 @@ int main() auto t0 = mio::abm::TimePoint(0); auto tmax = t0 + mio::abm::days(5); // auto sim = mio::abm::Simulation(t0, std::move(model)); - const size_t num_runs = 10; + const size_t num_runs = 3; // Create a parameter study. We currently do not use Parameters or dt, so we set them to 0. mio::ParameterStudy2, int, mio::abm::TimePoint, mio::abm::TimeSpan> diff --git a/cpp/examples/ode_secir_parameter_study.cpp b/cpp/examples/ode_secir_parameter_study.cpp index 60981be83e..f8f05a2fc8 100644 --- a/cpp/examples/ode_secir_parameter_study.cpp +++ b/cpp/examples/ode_secir_parameter_study.cpp @@ -17,13 +17,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include "memilio/compartments/simulation.h" #include "memilio/config.h" +#include "memilio/utils/base_dir.h" +#include "memilio/utils/miompi.h" +#include "memilio/utils/stl_util.h" #include "ode_secir/model.h" #include "ode_secir/parameters_io.h" #include "ode_secir/parameter_space.h" #include "memilio/compartments/parameter_studies.h" -#include "memilio/mobility/metapopulation_mobility_instant.h" #include "memilio/io/result_io.h" /** @@ -35,8 +36,8 @@ */ mio::IOResult write_single_run_result(const size_t run, const mio::osecir::Simulation& sim) { - std::string abs_path; - BOOST_OUTCOME_TRY(auto&& created, mio::create_directory("results", abs_path)); + std::string abs_path = mio::path_join(mio::base_dir(), "example_results"); + BOOST_OUTCOME_TRY(auto&& created, mio::create_directory(abs_path)); if (run == 0) { std::cout << "Results are stored in " << abs_path << '\n'; @@ -54,14 +55,15 @@ mio::IOResult write_single_run_result(const size_t run, const mio::osecir: std::vector ids; BOOST_OUTCOME_TRY(mio::save_result({sim.get_result()}, {0}, (int)sim.get_model().parameters.get_num_groups().get(), - mio::path_join(abs_path, ("Results_run" + std::to_string(run) + ".h5")))); + mio::path_join(abs_path, "Results_run" + std::to_string(run) + ".h5"))); return mio::success(); } int main() { - mio::set_log_level(mio::LogLevel::debug); + mio::mpi::init(); + mio::set_log_level(mio::LogLevel::warn); ScalarType t0 = 0; ScalarType tmax = 50; @@ -123,7 +125,7 @@ int main() } //create study - auto num_runs = size_t(1); + auto num_runs = size_t(3); // mio::ParameterStudy2, mio::osecir::Model, ScalarType> // parameter_study(model, t0, tmax, dt, num_runs); @@ -141,9 +143,15 @@ int main() if (!write_result_status) { std::cout << "Error writing result: " << write_result_status.error().formatted_message(); } - return 0; //Result handler must return something, but only meaningful when using MPI. + return sim.get_result(); //Result handler must return something, but only meaningful when using MPI. }; - parameter_study.run(sample_graph, handle_result); + + // Optional: set seeds to get reproducable results + // parameter_study.get_rng().seed({1456, 157456, 521346, 35345, 6875, 6435}); + + auto result = parameter_study.run(sample_graph, handle_result); + + mio::mpi::finalize(); return 0; } diff --git a/cpp/examples/ode_secir_read_graph.cpp b/cpp/examples/ode_secir_read_graph.cpp index e2b9df4bd7..86fe53a405 100644 --- a/cpp/examples/ode_secir_read_graph.cpp +++ b/cpp/examples/ode_secir_read_graph.cpp @@ -146,7 +146,7 @@ int main(int argc, char** argv) std::cout << "Running Simulations..." << std::flush; auto study = mio::make_parameter_study_graph_ode>(graph_read, t0, tmax, 0.5, 2); - study.run([](auto&& g, auto t0_, auto dt_, auto) { + study.run_serial([](auto&& g, auto t0_, auto dt_, auto) { auto copy = g; return mio::make_sampled_graph_simulation(draw_sample(copy), t0_, dt_, dt_); diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index 73a5d596a9..69a89485b4 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -21,8 +21,10 @@ #define MIO_COMPARTMENTS_PARAMETER_STUDIES_H #include "memilio/io/binary_serializer.h" +#include "memilio/io/io.h" #include "memilio/mobility/graph_simulation.h" #include "memilio/utils/logging.h" +#include "memilio/utils/metaprogramming.h" #include "memilio/utils/miompi.h" #include "memilio/utils/random_number_generator.h" #include "memilio/mobility/metapopulation_mobility_instant.h" @@ -82,41 +84,24 @@ class ParameterStudy2 // // TODO how is this supposed to work wrt. model? is this just a special case where ParameterType=Model? // } - /** - * @brief - * @param sample_simulation A function that accepts ParameterType and returns an instance of SimulationType. - * @param process_simulation_result A function that accepts S - */ - template +private: + template std::vector>> - run(CreateSimulationFunction&& create_simulation, ProcessSimulationResultFunction&& process_simulation_result) + run_impl(size_t start_run_idx, size_t end_run_idx, CreateSimulationFunction&& create_simulation, + ProcessSimulationResultFunction&& process_simulation_result) { + assert(start_run_idx <= end_run_idx); static_assert(std::is_invocable_r_v, "Incorrect Type for create_simulation."); static_assert(std::is_invocable_v, "Incorrect Type for process_simulation_result."); - int num_procs, rank; -#ifdef MEMILIO_ENABLE_MPI - MPI_Comm_size(mpi::get_world(), &num_procs); - MPI_Comm_rank(mpi::get_world(), &rank); -#else - num_procs = 1; - rank = 0; -#endif - //The ParameterDistributions used for sampling parameters use thread_local_rng() - //So we set our own RNG to be used. - //Assume that sampling uses the thread_local_rng() and isn't multithreaded - m_rng.synchronize(); - thread_local_rng() = m_rng; + using ProcessedResultT = + std::decay_t>; - auto run_distribution = distribute_runs(m_num_runs, num_procs); - auto start_run_idx = - std::accumulate(run_distribution.begin(), run_distribution.begin() + size_t(rank), size_t(0)); - auto end_run_idx = start_run_idx + run_distribution[size_t(rank)]; + thread_local_rng() = m_rng; - std::vector>> - ensemble_result; + std::vector ensemble_result; ensemble_result.reserve(m_num_runs); for (size_t run_idx = start_run_idx; run_idx < end_run_idx; run_idx++) { @@ -145,6 +130,76 @@ class ParameterStudy2 //Set the counter of our RNG so that future calls of run() produce different parameters. m_rng.set_counter(m_rng.get_counter() + rng_totalsequence_counter(m_num_runs, Counter(0))); + return ensemble_result; + } + +public: + template + std::vector>> + run_serial(CreateSimulationFunction&& create_simulation, + ProcessSimulationResultFunction&& process_simulation_result) + { + return run_impl(0, m_num_runs, std::forward(create_simulation), + std::forward(process_simulation_result)); + } + + template + std::vector run_serial(CreateSimulationFunction&& create_simulation) + { + return run_serial(std::forward(create_simulation), + [](Simulation&& sim, size_t) -> Simulation&& { + return std::move(sim); + }); + } + + /** + * @brief run the sim + * @param sample_simulation A function that accepts ParameterType and returns an instance of SimulationType. + * @param process_simulation_result A function that accepts S + * side effect: sets seed and count of thread_local_rng + */ + template + std::vector>> + run(CreateSimulationFunction&& create_simulation, ProcessSimulationResultFunction&& process_simulation_result) + { + static_assert(std::is_invocable_r_v, + "Incorrect Type for create_simulation."); + static_assert(std::is_invocable_v, + "Incorrect Type for process_simulation_result."); + + using ProcessedResultT = + std::decay_t>; + int num_procs, rank; + +#ifdef MEMILIO_ENABLE_MPI + MPI_Comm_size(mpi::get_world(), &num_procs); + MPI_Comm_rank(mpi::get_world(), &rank); + + static_assert(is_serializable && + is_deserializable, + "The result type of the processing function must be serializable for usage with MPI. " + "Change the process_simulation_result argument of ParameterStudy::run, or use run_serial."); +#else + num_procs = 1; + rank = 0; +#endif + + //The ParameterDistributions used for sampling parameters use thread_local_rng() + //So we set our own RNG to be used. + //Assume that sampling uses the thread_local_rng() and isn't multithreaded + m_rng.synchronize(); + // Note that this overwrites seed and counter of thread_local_rng, but it does not replace it. + thread_local_rng() = m_rng; + + auto run_distribution = distribute_runs(m_num_runs, num_procs); + auto start_run_idx = + std::accumulate(run_distribution.begin(), run_distribution.begin() + size_t(rank), size_t(0)); + auto end_run_idx = start_run_idx + run_distribution[size_t(rank)]; + + std::vector ensemble_result = + run_impl(start_run_idx, end_run_idx, std::forward(create_simulation), + std::forward(process_simulation_result)); + #ifdef MEMILIO_ENABLE_MPI //gather results if (rank == 0) { @@ -183,11 +238,14 @@ class ParameterStudy2 template std::vector run(CreateSimulationFunction&& create_simulation) { - return run(std::forward(create_simulation), &result_forwarding_function); + return run(std::forward(create_simulation), + [](Simulation&& sim, size_t) -> Simulation&& { + return std::move(sim); + }); } /** - * @brief returns the number of Monte Carlo runs + * @brief Return the number of total runs that the study will make. */ size_t get_num_runs() const { @@ -195,7 +253,7 @@ class ParameterStudy2 } /** - * @brief returns end point in simulation + * @brief Return the final time point for simulations. */ Time get_tmax() const { @@ -203,20 +261,23 @@ class ParameterStudy2 } /** - * @brief returns start point in simulation + * @brief Return the initial time point for simulations. */ Time get_t0() const { return m_t0; } + /** + * @brief Return the initial step sized used by simulations. + */ Time get_dt() const { return m_dt; } + /** - * Get the input graph that the parameter study is run for. - * Use for graph simulations, use get_model for single node simulations. + * @brief Get the input parameters that each simulation in the study is created from. * @{ */ const Parameters& get_parameters() const @@ -229,12 +290,18 @@ class ParameterStudy2 } /** @} */ + /** + * @brief Access the study's random number generator. + */ RandomNumberGenerator& get_rng() { return m_rng; } private: + /** + * @brief Create a vector with the number of runs each process should make. + */ std::vector distribute_runs(size_t num_runs, int num_procs) { assert(num_procs > 0); @@ -250,24 +317,11 @@ class ParameterStudy2 return run_distribution; } - inline static Simulation&& result_forwarding_function(Simulation&& sim, size_t) - { - return std::move(sim); - } - - // Stores Graph with the names and ranges of all parameters - ParameterType m_parameters; - - size_t m_num_runs; - - // Start time (should be the same for all simulations) - Time m_t0; - // End time (should be the same for all simulations) - Time m_tmax; - // adaptive time step of the integrator (will be corrected if too large/small) - Step m_dt; - // - RandomNumberGenerator m_rng; + ParameterType m_parameters; ///< Stores parameters used to create a simulation for each run. + size_t m_num_runs; ///< Total number of runs (i.e. simulations) to do when calling "run". + Time m_t0, m_tmax; ///< Start and end time for the simulations. + Step m_dt; ///< Initial step size of the simulation. Some integrators may adapt their step size during simulation. + RandomNumberGenerator m_rng; ///< The random number generator used by the study. }; template diff --git a/cpp/memilio/io/io.h b/cpp/memilio/io/io.h index fdcf9424d4..13c2d30045 100644 --- a/cpp/memilio/io/io.h +++ b/cpp/memilio/io/io.h @@ -857,6 +857,15 @@ void serialize(IOContext& io, const T& t) serialize_internal(io, t); } +namespace details +{ +template +using is_serializable_expr_t = decltype(serialize(std::declval(), std::declval())); +} + +template +static constexpr bool is_serializable = is_expression_valid::value; + /** * Restores an object from the data stored in an IO context. * There must be provided for the type T either a free function `deserialize_internal(io, tag)` @@ -881,6 +890,16 @@ IOResult deserialize(IOContext& io, Tag tag) return deserialize_internal(io, tag); } +namespace details +{ +template +using is_deserializable_expr_t = + decltype(std::declval>() = deserialize(std::declval(), std::declval>())); +} + +template +static constexpr bool is_deserializable = is_expression_valid::value; + /** * @brief Returns the current working directory name */ diff --git a/cpp/memilio/utils/miompi.cpp b/cpp/memilio/utils/miompi.cpp index afbfcff117..8c1079d538 100644 --- a/cpp/memilio/utils/miompi.cpp +++ b/cpp/memilio/utils/miompi.cpp @@ -18,6 +18,7 @@ * limitations under the License. */ #include "memilio/utils/miompi.h" +#include "memilio/utils/logging.h" #ifdef MEMILIO_ENABLE_MPI #include @@ -41,6 +42,8 @@ void init() { #ifdef MEMILIO_ENABLE_MPI MPI_Init(nullptr, nullptr); +#else + mio::log_debug("Using mio::mpi::init without MPI being enabled."); #endif } diff --git a/cpp/models/abm/CMakeLists.txt b/cpp/models/abm/CMakeLists.txt index 50ce4258f6..ae04b68b2d 100644 --- a/cpp/models/abm/CMakeLists.txt +++ b/cpp/models/abm/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(abm location_id.h household.cpp household.h + result_simulation.h simulation.cpp simulation.h person.cpp diff --git a/cpp/models/abm/result_simulation.h b/cpp/models/abm/result_simulation.h new file mode 100644 index 0000000000..01e4b6713b --- /dev/null +++ b/cpp/models/abm/result_simulation.h @@ -0,0 +1,65 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Rene Schmieding +* +* 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 "abm/common_abm_loggers.h" +#include "abm/simulation.h" + +namespace mio +{ +namespace abm +{ + +/// @brief Simulation holding its own History to provide a get_result member. Can be used for a ParameterStudy. +template +class ResultSimulation : public Simulation +{ +public: + using Model = M; + + /// @brief Create a simulation, copying the model. + ResultSimulation(const Model& m, TimePoint t) + : Simulation(t, Model(m)) + { + history.log(*this); // set initial results + } + + /** + * @brief Run the simulation until the given time point. + * @param tmax Final time point for the simualtion. + */ + void advance(TimePoint tmax) + { + Simulation::advance(tmax, history); + } + + /** + * @brief Return the simulation result aggregated by infection states. + */ + const mio::TimeSeries& get_result() const + { + return get<0>(history.get_log()); + } + + mio::History history{ + Eigen::Index(InfectionState::Count)}; ///< History used to create the result TimeSeries. +}; + +} // namespace abm +} // namespace mio From 437162200ce03de3f9b3f891770b569afd8441a2 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Thu, 23 Oct 2025 19:05:59 +0200 Subject: [PATCH 094/169] add hdf5 dep to abm study example --- cpp/examples/CMakeLists.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index d303ac241e..fa12445425 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -108,9 +108,11 @@ add_executable(abm_minimal_example abm_minimal.cpp) target_link_libraries(abm_minimal_example PRIVATE memilio abm) target_compile_options(abm_minimal_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) -add_executable(abm_parameter_study_example abm_parameter_study.cpp) -target_link_libraries(abm_parameter_study_example PRIVATE memilio abm) -target_compile_options(abm_parameter_study_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +if(MEMILIO_HAS_HDF5) + add_executable(abm_parameter_study_example abm_parameter_study.cpp) + target_link_libraries(abm_parameter_study_example PRIVATE memilio abm) + target_compile_options(abm_parameter_study_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +endif() add_executable(abm_history_example abm_history_object.cpp) target_link_libraries(abm_history_example PRIVATE memilio abm) From fe9ef05ddbc5a72cd832f0bd0af174105cfaba14 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:14:53 +0200 Subject: [PATCH 095/169] improve non-PS coverage --- cpp/tests/test_abm_simulation.cpp | 29 +++++++++++++++++++++++++++++ cpp/tests/test_utils.cpp | 13 +++++++++++++ 2 files changed, 42 insertions(+) diff --git a/cpp/tests/test_abm_simulation.cpp b/cpp/tests/test_abm_simulation.cpp index 53c372fe7c..856456baf8 100644 --- a/cpp/tests/test_abm_simulation.cpp +++ b/cpp/tests/test_abm_simulation.cpp @@ -18,10 +18,14 @@ * limitations under the License. */ #include "abm/location_type.h" +#include "abm/simulation.h" +#include "abm/result_simulation.h" +#include "abm/time.h" #include "abm_helpers.h" #include "abm/common_abm_loggers.h" #include "matchers.h" #include "memilio/io/history.h" +#include #include TEST(TestSimulation, advance_random) @@ -143,3 +147,28 @@ TEST(TestSimulation, advanceWithCommonHistory) 3); // Check if all persons are in the delta-logger Mobility helper entry 0, 3 persons EXPECT_EQ(logMobilityInfoDelta[1].size(), 3); // Check if all persons are in the delta-log first entry, 3 persons } + +TEST(TestSimulation, ResultSimulation) +{ + // run a ResultSimulation on a minimal setup + auto model = mio::abm::Model(num_age_groups); + auto location = model.add_location(mio::abm::LocationType::Home); + auto person = model.add_person(location, age_group_15_to_34); + + model.assign_location(person, location); + + const auto t0 = mio::abm::TimePoint(0) + mio::abm::hours(100); + const auto tmax = t0 + mio::abm::hours(50); + auto sim = mio::abm::ResultSimulation(std::move(model), t0); + + // run simulation. expect one timepopint per day, but nothing to change in the results + sim.advance(tmax); + const size_t N = (size_t)(tmax - t0).hours() + 1; + ASSERT_EQ(sim.get_result().get_num_time_points(), N); + EXPECT_THAT(sim.get_result().get_times(), ElementsAreLinspace(t0.days(), tmax.days(), N)); + for (const auto& tp : sim.get_result()) { + EXPECT_EQ(tp.sum(), 1.0); + } + EXPECT_EQ(sim.get_result().get_value(0)[(Eigen::Index)mio::abm::InfectionState::Susceptible], 1.0); + EXPECT_EQ(sim.get_result().get_value(N - 1)[(Eigen::Index)mio::abm::InfectionState::Susceptible], 1.0); +} diff --git a/cpp/tests/test_utils.cpp b/cpp/tests/test_utils.cpp index cbffefdfff..f5283a81b8 100644 --- a/cpp/tests/test_utils.cpp +++ b/cpp/tests/test_utils.cpp @@ -17,6 +17,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "memilio/utils/base_dir.h" #include "memilio/utils/index.h" #include "memilio/utils/index_range.h" #include "memilio/utils/logging.h" @@ -27,6 +28,8 @@ #include #include +#include + template struct CategoryTag : public mio::Index> { CategoryTag(size_t value) @@ -160,3 +163,13 @@ TEST(TestUtils, RedirectLogger) logger.release(); } + +TEST(TestUtils, base_dir) +{ + auto base_dir = boost::filesystem::path(mio::base_dir()); + // check that the path exists + EXPECT_TRUE(boost::filesystem::exists(base_dir)); + // check that the path is correct, by sampling some fixed paths from project files + EXPECT_TRUE(boost::filesystem::exists(base_dir / "cpp" / "memilio")); + EXPECT_TRUE(boost::filesystem::exists(base_dir / "pycode" / "memilio-epidata")); +} From fcde395ae246b1331d270250349cfe2b48984bd9 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:49:25 +0200 Subject: [PATCH 096/169] remove Simulation template from ParameterStudy2. this allows deduction of template arguments from the constructor, making the make_parameter_study functions obsolete. the simulation type is now deduced from the create_simulation argument, and checks on invocability have been moved to clean up deduced types. Add documentation. --- cpp/examples/abm_parameter_study.cpp | 8 +- cpp/examples/ode_secir_parameter_study.cpp | 14 +- .../ode_secir_parameter_study_graph.cpp | 19 +- cpp/examples/ode_secir_read_graph.cpp | 9 +- cpp/memilio/compartments/parameter_studies.h | 294 +++++++++--------- cpp/models/abm/result_simulation.h | 7 +- cpp/tests/test_parameter_studies.cpp | 17 +- cpp/tests/test_save_results.cpp | 14 +- .../bindings/compartments/parameter_studies.h | 103 ------ .../simulation/bindings/models/osecir.cpp | 44 +-- .../simulation/bindings/models/osecirvvs.cpp | 67 ++-- 11 files changed, 223 insertions(+), 373 deletions(-) delete mode 100644 pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h diff --git a/cpp/examples/abm_parameter_study.cpp b/cpp/examples/abm_parameter_study.cpp index 183c830c28..f0a2558d6f 100644 --- a/cpp/examples/abm_parameter_study.cpp +++ b/cpp/examples/abm_parameter_study.cpp @@ -177,9 +177,8 @@ int main() // auto sim = mio::abm::Simulation(t0, std::move(model)); const size_t num_runs = 3; - // Create a parameter study. We currently do not use Parameters or dt, so we set them to 0. - mio::ParameterStudy2, int, mio::abm::TimePoint, mio::abm::TimeSpan> - study(0, t0, tmax, mio::abm::TimeSpan(0), num_runs); + // Create a parameter study. The ABM currently does not use parameters or dt, so we set them both to 0. + mio::ParameterStudy2 study(0, t0, tmax, mio::abm::TimeSpan(0), num_runs); // Optional: set seeds to get reproducable results // study.get_rng().seed({12341234, 53456, 63451, 5232576, 84586, 52345}); @@ -192,8 +191,7 @@ int main() auto ensemble_results = study.run( [](auto, auto t0_, auto, size_t) { - auto sim = mio::abm::ResultSimulation(make_model(mio::thread_local_rng()), t0_); - return sim; + return mio::abm::ResultSimulation(make_model(mio::thread_local_rng()), t0_); }, [result_dir](auto&& sim, auto&& run_idx) { auto interpolated_result = mio::interpolate_simulation_result(sim.get_result()); diff --git a/cpp/examples/ode_secir_parameter_study.cpp b/cpp/examples/ode_secir_parameter_study.cpp index f8f05a2fc8..0d2ff9c7d7 100644 --- a/cpp/examples/ode_secir_parameter_study.cpp +++ b/cpp/examples/ode_secir_parameter_study.cpp @@ -69,6 +69,7 @@ int main() ScalarType tmax = 50; ScalarType dt = 0.1; + // set up model with parameters ScalarType cont_freq = 10; // see Polymod study ScalarType num_total_t0 = 10000, num_exp_t0 = 100, num_inf_t0 = 50, num_car_t0 = 50, num_hosp_t0 = 20, @@ -124,15 +125,11 @@ int main() return -1; } - //create study + // create study auto num_runs = size_t(3); - // mio::ParameterStudy2, mio::osecir::Model, ScalarType> - // parameter_study(model, t0, tmax, dt, num_runs); + mio::ParameterStudy2 parameter_study(model, t0, tmax, dt, num_runs); - auto parameter_study = - mio::make_parameter_study>(model, t0, tmax, dt, num_runs); - - //run study + // set up for run auto sample_graph = [](const auto& model_, ScalarType t0_, ScalarType dt_, size_t) { mio::osecir::Model copy = model_; mio::osecir::draw_sample(copy); @@ -143,12 +140,13 @@ int main() if (!write_result_status) { std::cout << "Error writing result: " << write_result_status.error().formatted_message(); } - return sim.get_result(); //Result handler must return something, but only meaningful when using MPI. + return 0; // Result handler must return something. }; // Optional: set seeds to get reproducable results // parameter_study.get_rng().seed({1456, 157456, 521346, 35345, 6875, 6435}); + // run study auto result = parameter_study.run(sample_graph, handle_result); mio::mpi::finalize(); diff --git a/cpp/examples/ode_secir_parameter_study_graph.cpp b/cpp/examples/ode_secir_parameter_study_graph.cpp index c3cb5b7564..b0f8202f0b 100644 --- a/cpp/examples/ode_secir_parameter_study_graph.cpp +++ b/cpp/examples/ode_secir_parameter_study_graph.cpp @@ -20,11 +20,9 @@ #include "memilio/compartments/parameter_studies.h" #include "memilio/config.h" -#include "memilio/geography/regions.h" #include "memilio/io/epi_data.h" #include "memilio/io/result_io.h" #include "memilio/io/mobility_io.h" -#include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_instant.h" #include "memilio/utils/logging.h" #include "memilio/utils/miompi.h" @@ -33,11 +31,8 @@ #include "ode_secir/model.h" #include "ode_secir/parameters_io.h" #include "ode_secir/parameter_space.h" -#include "boost/filesystem.hpp" -#include "memilio/utils/stl_util.h" #include #include -#include /** * Set a value and distribution of an UncertainValue. @@ -267,17 +262,7 @@ int main() 2, 1, Eigen::VectorX::Constant(num_age_groups * (size_t)mio::osecir::InfectionState::Count, 0.2), indices_save_edges); - //run parameter study - // auto parameter_study = mio::ParameterStudy2< - // mio::GraphSimulation>, - // mio::MobilityEdge>, - // ScalarType, ScalarType>, - // mio::Graph, mio::MobilityParameters>, ScalarType>{ - // params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)}; - - auto parameter_study = mio::make_parameter_study_graph_ode>( - params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)); + mio::ParameterStudy2 parameter_study(params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)); if (mio::mpi::is_root()) { printf("Seeds: "); @@ -291,7 +276,7 @@ int main() auto ensemble = parameter_study.run( [](auto&& graph, ScalarType t0, ScalarType dt, size_t) { auto copy = graph; - return mio::make_sampled_graph_simulation( + return mio::make_sampled_graph_simulation>( mio::osecir::draw_sample(copy), t0, dt, dt); }, [&](auto&& results_sim, auto&& run_id) { diff --git a/cpp/examples/ode_secir_read_graph.cpp b/cpp/examples/ode_secir_read_graph.cpp index 86fe53a405..e282ef5468 100644 --- a/cpp/examples/ode_secir_read_graph.cpp +++ b/cpp/examples/ode_secir_read_graph.cpp @@ -18,12 +18,14 @@ * limitations under the License. */ #include "memilio/compartments/parameter_studies.h" +#include "memilio/config.h" #include "memilio/io/cli.h" #include "memilio/io/mobility_io.h" #include "memilio/mobility/metapopulation_mobility_instant.h" #include "memilio/utils/base_dir.h" #include "memilio/utils/stl_util.h" +#include "ode_secir/model.h" #include "ode_secir/parameter_space.h" #include "ode_secir/parameters_io.h" @@ -144,12 +146,11 @@ int main(int argc, char** argv) auto& graph_read = graph_read_result.value(); std::cout << "Running Simulations..." << std::flush; - auto study = mio::make_parameter_study_graph_ode>(graph_read, t0, - tmax, 0.5, 2); + mio::ParameterStudy2 study(graph_read, t0, tmax, 0.5, 2); study.run_serial([](auto&& g, auto t0_, auto dt_, auto) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy), t0_, dt_, - dt_); + return mio::make_sampled_graph_simulation>(draw_sample(copy), t0_, + dt_, dt_); }); std::cout << "Done" << std::endl; diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index 69a89485b4..f3eb730b66 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -31,38 +31,53 @@ #include #include +#include #include #include +#include +#include namespace mio { /** - * Class that performs multiple simulation runs with randomly sampled parameters. - * Can simulate mobility graphs with one simulation in each node or single simulations. - * @tparam S type of simulation that runs in one node of the graph. + * @brief Class used to performs multiple simulation runs with randomly sampled parameters. + * @tparam ParameterType The parameters used to create simulations. + * @tparam TimeType The type used for time, e.g. double or TimePoint. + * @tparam StepType The type used for time steps, e.g. double or TimeStep. */ -template +template class ParameterStudy2 { public: - using Simulation = SimulationType; using Parameters = ParameterType; using Time = TimeType; using Step = StepType; - // TODO: replacement for "set_params_distributions_normal". Maybe a special ctor for UncertainParameterSet? +private: + template + requires std::is_invocable_v + using SimulationT = std::decay_t>; + + template + requires std::is_invocable_v, size_t> + using ProcessedResultT = std::decay_t< + std::invoke_result_t, size_t>>; + +public: + // TODO: replacement for "set_params_distributions_normal"? Maybe a special ctor for UncertainParameterSet? /** - * create study for graph of compartment models. - * @param graph graph of parameters - * @param t0 start time of simulations - * @param tmax end time of simulations - * @param graph_sim_dt time step of graph simulation - * @param num_runs number of runs + * @brief Create a parameter study with some parameters. + * The simulation type is determined when calling any "run" member function. + * @param parameters The parameters used to create simulations. + * @param t0 Start time of simulations. + * @param tmax End time of simulations. + * @param dt Initial time step of simulations. + * @param num_runs Number of simulations that will be created and run. */ - ParameterStudy2(const Parameters& global_parameters, Time t0, Time tmax, Step dt, size_t num_runs) - : m_parameters(global_parameters) + ParameterStudy2(const Parameters& parameters, Time t0, Time tmax, Step dt, size_t num_runs) + : m_parameters(parameters) , m_num_runs(num_runs) , m_t0{t0} , m_tmax{tmax} @@ -70,72 +85,25 @@ class ParameterStudy2 { } - // /** - // * @brief Create study for single compartment model. - // * @param model compartment model with initial values - // * @param t0 start time of simulations - // * @param tmax end time of simulations - // * @param num_runs number of runs in ensemble run - // */ - // template >> - // ParameterStudy2(typename Simulation::Model const& model, Time t0, Time tmax, Step dt, size_t num_runs) - // : ParameterStudy2(model, t0, tmax, dt, num_runs) - // { - // // TODO how is this supposed to work wrt. model? is this just a special case where ParameterType=Model? - // } - -private: - template - std::vector>> - run_impl(size_t start_run_idx, size_t end_run_idx, CreateSimulationFunction&& create_simulation, - ProcessSimulationResultFunction&& process_simulation_result) - { - assert(start_run_idx <= end_run_idx); - static_assert(std::is_invocable_r_v, - "Incorrect Type for create_simulation."); - static_assert(std::is_invocable_v, - "Incorrect Type for process_simulation_result."); - - using ProcessedResultT = - std::decay_t>; - - thread_local_rng() = m_rng; - - std::vector ensemble_result; - ensemble_result.reserve(m_num_runs); - - for (size_t run_idx = start_run_idx; run_idx < end_run_idx; run_idx++) { - log(LogLevel::info, "ParameterStudies: run {}", run_idx); - - //prepare rng for this run by setting the counter to the right offset - //Add the old counter so that this call of run() produces different results - //from the previous call - auto run_rng_counter = m_rng.get_counter() + rng_totalsequence_counter( - static_cast(run_idx), Counter(0)); - thread_local_rng().set_counter(run_rng_counter); - - //sample - Simulation sim = - create_simulation(std::as_const(m_parameters), std::as_const(m_t0), std::as_const(m_dt), run_idx); - log(LogLevel::info, "ParameterStudies: Generated {} random numbers.", - (thread_local_rng().get_counter() - run_rng_counter).get()); - - //perform run - sim.advance(m_tmax); - - //handle result and store - ensemble_result.emplace_back(process_simulation_result(std::move(sim), run_idx)); - } - - //Set the counter of our RNG so that future calls of run() produce different parameters. - m_rng.set_counter(m_rng.get_counter() + rng_totalsequence_counter(m_num_runs, Counter(0))); - - return ensemble_result; - } - -public: + /** + * @brief Run all simulations in serial. + * @param[in] create_simulation A callable sampling the study's parameters and return a simulation. + * @param[in] process_simulation_result (Optional) A callable that takes the simulation and processes its result. + * @return A vector that contains (processed) simulation results for each run. + * + * Important side effect: Calling this function overwrites seed and counter of thread_local_rng(). + * Use this RNG when sampling parameters in create_simulation. + * + * The function signature for create_simulation is + * `SimulationT(const Parameters& study_parameters, Time t0, Step dt, size_t run_idx)`, + * where SimulationT is some kind of simulation. + * The function signature for process_simulation_result is + * `ProcessedResultT(SimulationT&&, size_t run_index)`, + * where ProcessedResultT is a (de)serializable result. + * @{ + */ template - std::vector>> + std::vector> run_serial(CreateSimulationFunction&& create_simulation, ProcessSimulationResultFunction&& process_simulation_result) { @@ -144,41 +112,45 @@ class ParameterStudy2 } template - std::vector run_serial(CreateSimulationFunction&& create_simulation) + std::vector> run_serial(CreateSimulationFunction&& create_simulation) { - return run_serial(std::forward(create_simulation), - [](Simulation&& sim, size_t) -> Simulation&& { - return std::move(sim); - }); + return run_serial( + std::forward(create_simulation), + [](SimulationT&& sim, size_t) -> SimulationT&& { + return std::move(sim); + }); } + /** @} */ /** - * @brief run the sim - * @param sample_simulation A function that accepts ParameterType and returns an instance of SimulationType. - * @param process_simulation_result A function that accepts S - * side effect: sets seed and count of thread_local_rng + * @brief Run all simulations distributed over multiple MPI ranks. + * @param[in] create_simulation A callable sampling the study's parameters and return a simulation. + * @param[in] process_simulation_result A callable that takes the simulation and processes its result. + * @return A vector that contains processed simulation results for each run. + * + * Important: Do not forget to use mio::mpi::init and finalize when using this function! + * + * Important side effect: Calling this function overwrites seed and counter of thread_local_rng(). + * Use this RNG when sampling parameters in create_simulation. + * + * The function signature for create_simulation is + * `SimulationT(const Parameters& study_parameters, Time t0, Step dt, size_t run_idx)`, + * where SimulationT is some kind of simulation. + * The function signature for process_simulation_result is + * `ProcessedResultT(SimulationT&&, size_t run_index)`, + * where ProcessedResultT is a (de)serializable result. */ template - std::vector>> + std::vector> run(CreateSimulationFunction&& create_simulation, ProcessSimulationResultFunction&& process_simulation_result) { - static_assert(std::is_invocable_r_v, - "Incorrect Type for create_simulation."); - static_assert(std::is_invocable_v, - "Incorrect Type for process_simulation_result."); - - using ProcessedResultT = - std::decay_t>; + using EnsembleResultT = + std::vector>; int num_procs, rank; #ifdef MEMILIO_ENABLE_MPI MPI_Comm_size(mpi::get_world(), &num_procs); MPI_Comm_rank(mpi::get_world(), &rank); - - static_assert(is_serializable && - is_deserializable, - "The result type of the processing function must be serializable for usage with MPI. " - "Change the process_simulation_result argument of ParameterStudy::run, or use run_serial."); #else num_procs = 1; rank = 0; @@ -188,15 +160,13 @@ class ParameterStudy2 //So we set our own RNG to be used. //Assume that sampling uses the thread_local_rng() and isn't multithreaded m_rng.synchronize(); - // Note that this overwrites seed and counter of thread_local_rng, but it does not replace it. - thread_local_rng() = m_rng; - auto run_distribution = distribute_runs(m_num_runs, num_procs); - auto start_run_idx = + std::vector run_distribution = distribute_runs(m_num_runs, num_procs); + size_t start_run_idx = std::accumulate(run_distribution.begin(), run_distribution.begin() + size_t(rank), size_t(0)); - auto end_run_idx = start_run_idx + run_distribution[size_t(rank)]; + size_t end_run_idx = start_run_idx + run_distribution[size_t(rank)]; - std::vector ensemble_result = + EnsembleResultT ensemble_result = run_impl(start_run_idx, end_run_idx, std::forward(create_simulation), std::forward(process_simulation_result)); @@ -209,7 +179,7 @@ class ParameterStudy2 ByteStream bytes(bytes_size); MPI_Recv(bytes.data(), bytes.data_size(), MPI_BYTE, src_rank, 0, mpi::get_world(), MPI_STATUS_IGNORE); - auto src_ensemble_results = deserialize_binary(bytes, Tag{}); + IOResult src_ensemble_results = deserialize_binary(bytes, Tag{}); if (!src_ensemble_results) { log_error("Error receiving ensemble results from rank {}.", src_rank); } @@ -218,8 +188,8 @@ class ParameterStudy2 } } else { - auto bytes = serialize_binary(ensemble_result); - auto bytes_size = int(bytes.data_size()); + ByteStream bytes = serialize_binary(ensemble_result); + int bytes_size = int(bytes.data_size()); MPI_Send(&bytes_size, 1, MPI_INT, 0, 0, mpi::get_world()); MPI_Send(bytes.data(), bytes.data_size(), MPI_BYTE, 0, 0, mpi::get_world()); ensemble_result.clear(); //only return root process @@ -230,16 +200,17 @@ class ParameterStudy2 } /** - * @brief Carry out all simulations in the parameter study. - * Convenience function for a few number of runs, but can use more memory because it stores all runs until the end. - * Unlike the other overload, this function is not MPI-parallel. - * @return vector of SimulationGraph for each run. + * @brief Carry out all simulations in the parameter study. @see run. + * Convenience function for a few number of runs, but can use more memory because it stores every single simulaiton. + * If this function causes errors, they may be caused by the simulation not being serializable. In that case, try + * using run_serial, provide a processing function, or make the simulation serializable. + * @return Vector of SimulationGraph for each run. */ template - std::vector run(CreateSimulationFunction&& create_simulation) + std::vector> run(CreateSimulationFunction&& create_simulation) { return run(std::forward(create_simulation), - [](Simulation&& sim, size_t) -> Simulation&& { + [](SimulationT&& sim, size_t) -> SimulationT&& { return std::move(sim); }); } @@ -299,16 +270,71 @@ class ParameterStudy2 } private: + /** + * @brief Main loop creating and running simulations. + * @param[in] start_run_idx, end_run_idx Range of indices. Performs one run for each index. + * @param[in] create_simulation A callable sampling the study's parameters and return a simulation. + * @param[in] process_simulation_result A callable that takes the simulation and processes its result. + * @return A vector that contains processed simulation results for each run. + * + * Important side effect: Calling this function overwrites seed and counter of thread_local_rng(). + * Use this RNG when sampling parameters in create_simulation. + */ + template + std::vector> + run_impl(size_t start_run_idx, size_t end_run_idx, CreateSimulationFunction&& create_simulation, + ProcessSimulationResultFunction&& process_simulation_result) + { + assert(start_run_idx <= end_run_idx); + + // Note that this overwrites seed and counter of thread_local_rng, but it does not replace it. + thread_local_rng() = m_rng; + + std::vector> ensemble_result; + ensemble_result.reserve(m_num_runs); + + for (size_t run_idx = start_run_idx; run_idx < end_run_idx; run_idx++) { + log(LogLevel::info, "ParameterStudies: run {}", run_idx); + + //prepare rng for this run by setting the counter to the right offset + //Add the old counter so that this call of run() produces different results + //from the previous call + Counter run_rng_counter = + m_rng.get_counter() + + rng_totalsequence_counter(static_cast(run_idx), Counter(0)); + thread_local_rng().set_counter(run_rng_counter); + + //sample + SimulationT sim = + create_simulation(std::as_const(m_parameters), std::as_const(m_t0), std::as_const(m_dt), run_idx); + log(LogLevel::info, "ParameterStudies: Generated {} random numbers.", + (thread_local_rng().get_counter() - run_rng_counter).get()); + + //perform run + sim.advance(m_tmax); + + //handle result and store + ensemble_result.emplace_back(process_simulation_result(std::move(sim), run_idx)); + } + + //Set the counter of our RNG so that future calls of run() produce different parameters. + m_rng.set_counter(m_rng.get_counter() + rng_totalsequence_counter(m_num_runs, Counter(0))); + + return ensemble_result; + } + /** * @brief Create a vector with the number of runs each process should make. + * @param num_runs The total number of runs. + * @param num_procs The total number of processes, i.e. the size of MPI_Comm. */ std::vector distribute_runs(size_t num_runs, int num_procs) { assert(num_procs > 0); //evenly distribute runs //lower processes do one more run if runs are not evenly distributable - auto num_runs_local = num_runs / num_procs; //integer division! - auto remainder = num_runs % num_procs; + size_t num_runs_local = num_runs / num_procs; //integer division! + size_t remainder = num_runs % num_procs; std::vector run_distribution(num_procs); std::fill(run_distribution.begin(), run_distribution.begin() + remainder, num_runs_local + 1); @@ -324,31 +350,14 @@ class ParameterStudy2 RandomNumberGenerator m_rng; ///< The random number generator used by the study. }; -template -ParameterStudy2 make_parameter_study(const Parameters& global_parameters, Time t0, - Time tmax, Step dt, size_t num_runs) -{ - return {global_parameters, t0, tmax, dt, num_runs}; -} - -template -auto make_parameter_study_graph_ode(const Graph>& global_parameters, FP t0, - FP tmax, FP dt, size_t num_runs) -{ - using SimGraph = Graph, MobilityEdge>; - using SimGraphSim = GraphSimulation; - using Params = Graph>; - - return ParameterStudy2{global_parameters, t0, tmax, dt, num_runs}; -} - //sample parameters and create simulation -template -GraphSim make_sampled_graph_simulation( - const Graph>& sampled_graph, - FP t0, FP dt_node_sim, FP dt_graph_sim) +template +auto make_sampled_graph_simulation(const Graph>& sampled_graph, FP t0, + FP dt_node_sim, FP dt_graph_sim) { - typename GraphSim::Graph sim_graph; + using SimGraph = Graph, MobilityEdge>; + + SimGraph sim_graph; for (auto&& node : sampled_graph.nodes()) { sim_graph.add_node(node.id, node.property, t0, dt_node_sim); @@ -357,8 +366,7 @@ GraphSim make_sampled_graph_simulation( sim_graph.add_edge(edge.start_node_idx, edge.end_node_idx, edge.property); } - return make_mobility_sim(t0, dt_graph_sim, - std::move(sim_graph)); + return make_mobility_sim(t0, dt_graph_sim, std::move(sim_graph)); } } // namespace mio diff --git a/cpp/models/abm/result_simulation.h b/cpp/models/abm/result_simulation.h index 01e4b6713b..5a51a6e99e 100644 --- a/cpp/models/abm/result_simulation.h +++ b/cpp/models/abm/result_simulation.h @@ -33,11 +33,10 @@ class ResultSimulation : public Simulation public: using Model = M; - /// @brief Create a simulation, copying the model. - ResultSimulation(const Model& m, TimePoint t) - : Simulation(t, Model(m)) + /// @brief Create a simulation, moving the model. + ResultSimulation(Model&& m, TimePoint t) + : Simulation(t, std::move(m)) { - history.log(*this); // set initial results } /** diff --git a/cpp/tests/test_parameter_studies.cpp b/cpp/tests/test_parameter_studies.cpp index b7f63a6852..2d633d667a 100644 --- a/cpp/tests/test_parameter_studies.cpp +++ b/cpp/tests/test_parameter_studies.cpp @@ -17,6 +17,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "memilio/config.h" #include "memilio/utils/parameter_distributions.h" #include "ode_secir/model.h" #include "ode_secir/parameter_space.h" @@ -157,12 +158,12 @@ TEST(ParameterStudies, sample_graph) graph.add_node(1, model); graph.add_edge(0, 1, mio::MobilityParameters(Eigen::VectorXd::Constant(Eigen::Index(num_groups * 8), 1.0))); - auto study = mio::make_parameter_study_graph_ode>(graph, 0.0, 0.0, 0.5, 1); + mio::ParameterStudy2 study(graph, 0.0, 0.0, 0.5, 1); mio::log_rng_seeds(study.get_rng(), mio::LogLevel::warn); - auto ensemble_results = study.run([](auto&& g, auto t0_, auto dt_, auto) { + auto ensemble_results = study.run_serial([](auto&& g, auto t0_, auto dt_, auto) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy), t0_, dt_, - dt_); + return mio::make_sampled_graph_simulation>(draw_sample(copy), t0_, + dt_, dt_); }); auto& results = ensemble_results.at(0); @@ -373,11 +374,11 @@ TEST(ParameterStudies, check_ensemble_run_result) mio::ContactMatrix(Eigen::MatrixXd::Constant((size_t)num_groups, (size_t)num_groups, fact * cont_freq)); mio::osecir::set_params_distributions_normal(model, t0, tmax, 0.2); - auto parameter_study = mio::make_parameter_study>(model, t0, tmax, 0.1, 1); + mio::ParameterStudy2 parameter_study(model, t0, tmax, 0.1, 1); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); // Run parameter study - auto ensemble_results = parameter_study.run([](auto&& model_, auto t0_, auto dt_, auto) { + auto ensemble_results = parameter_study.run_serial([](auto&& model_, auto t0_, auto dt_, auto) { auto copy = model_; draw_sample(copy); return mio::osecir::Simulation(copy, t0_, dt_); @@ -399,3 +400,7 @@ TEST(ParameterStudies, check_ensemble_run_result) } } } + +// TEST(ParameterStudies, run_mocks) { +// mio::ParameterStudy2 +// } diff --git a/cpp/tests/test_save_results.cpp b/cpp/tests/test_save_results.cpp index 0fd07553aa..4f87787fb9 100644 --- a/cpp/tests/test_save_results.cpp +++ b/cpp/tests/test_save_results.cpp @@ -170,8 +170,7 @@ TEST(TestSaveResult, save_result_with_params) mio::MobilityParameters(Eigen::VectorXd::Constant(Eigen::Index(num_groups * 10), 1.0))); auto num_runs = 3; - auto parameter_study = - mio::make_parameter_study_graph_ode>(graph, 0.0, 2.0, 0.5, num_runs); + mio::ParameterStudy2 parameter_study(graph, 0.0, 2.0, 0.5, num_runs); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); TempFileRegister tmp_file_register; @@ -186,8 +185,8 @@ TEST(TestSaveResult, save_result_with_params) parameter_study.run( [](auto&& g, auto t0_, auto dt_, auto) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy), - t0_, dt_, dt_); + return mio::make_sampled_graph_simulation>(draw_sample(copy), t0_, + dt_, dt_); }, [&](auto&& results, auto run_idx) { auto results_graph = results.get_graph(); @@ -307,8 +306,7 @@ TEST(TestSaveResult, save_percentiles_and_sums) indices_save_edges)); auto num_runs = 3; - auto parameter_study = - mio::make_parameter_study_graph_ode>(graph, 0.0, 2.0, 0.5, num_runs); + mio::ParameterStudy2 parameter_study(graph, 0.0, 2.0, 0.5, num_runs); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); TempFileRegister tmp_file_register; @@ -324,8 +322,8 @@ TEST(TestSaveResult, save_percentiles_and_sums) parameter_study.run( [](auto&& g, auto t0_, auto dt_, auto) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy), - t0_, dt_, dt_); + return mio::make_sampled_graph_simulation>(draw_sample(copy), t0_, + dt_, dt_); }, [&](auto&& results, auto /*run_idx*/) { auto results_graph = results.get_graph(); diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h b/pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h deleted file mode 100644 index 8cb60ffe12..0000000000 --- a/pycode/memilio-simulation/memilio/simulation/bindings/compartments/parameter_studies.h +++ /dev/null @@ -1,103 +0,0 @@ -/* -* Copyright (C) 2020-2025 MEmilio -* -* Authors: Rene Schmieding -* -* 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/compartments/parameter_studies.h" -#include "memilio/compartments/compartmental_model.h" - -#include "pybind11/pybind11.h" -#include "pybind11/stl_bind.h" - -#include -#include -#include -#include -#include - -namespace py = pybind11; - -namespace pymio -{ - -/* - * @brief bind ParameterStudy for any model - */ -template -void bind_GrapParameterStudy( - py::module_& m, std::string const& name, std::vector argnames, - std::function create_simulation = - [](const mio::Graph>& g, double t0, double dt, - size_t) { - using GraphSim = mio::GraphSimulation< - double, - mio::Graph>, mio::MobilityEdge>, - double, double>; - auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy), t0, dt, dt) - }) -{ - assert(sizeof...(RunArgs) == argnames.size()); - using SimulationT = mio::GraphSimulation< - double, mio::Graph>, mio::MobilityEdge>, - double, double>; - using ParametersT = Graph < typename Sim::Model, MobilityParameters; - using StudyT = mio::ParameterStudy2; - using CreateSimulationFunctionT = std::function; - using ProcessSimulationResultFunctionT = std::function; - pymio::bind_class(m, name.c_str()) - .def(py::init(), py::arg("parameters"), py::arg("t0"), - py::arg("tmax"), py::arg("dt") py::arg("num_runs")) - .def_property_readonly("num_runs", &StudyT::get_num_runs) - .def_property_readonly("tmax", &StudyT::get_tmax) - .def_property_readonly("t0", &StudyT::get_t0) - .def_property_readonly("dt", &StudyT::get_dt) - .def_property("parameters", py::overload_cast<>(&StudyT::get_parameters), - py::return_value_policy::reference_internal) - .def_property_readonly("rng", &StudyT::get_rng, py::return_value_policy::reference_internal) - .def( - "run", - [](StudyT& self, const ProcessSimulationResultFunctionT& handle_result, RunArgs args...) { - self.run(create_simulation, [&handle_result](auto&& g, auto&& run_idx) { - //handle_result_function needs to return something - //we don't want to run an unknown python object through parameterstudies, so - //we just return 0 and ignore the list returned by run(). - //So python will behave slightly different than c++ - handle_result(std::move(g), run_idx); - return 0; - }); - }, - py::arg("handle_result_func")) - .def("run", - [](StudyT& self) { //default argument doesn't seem to work with functions - return self.run(create_simulation); - }) - .def( - "run_single", - [](StudyT& self, ProcessSimulationResultFunctionT handle_result) { - self.run(create_simulation, [&handle_result](auto&& r, auto&& run_idx) { - handle_result(std::move(r.nodes()[0].property.get_simulation()), run_idx); - return 0; - }); - }, - py::arg("handle_result_func")) - .def("run_single", [](StudyT& self) { - return filter_graph_results(self.run(create_simulation)); - }); -} - -} // namespace pymio diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp index 5fc722ed8d..6d4e71dd9d 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp @@ -49,6 +49,7 @@ #include "pybind11/stl_bind.h" #include "Eigen/Core" #include +#include #include namespace py = pybind11; @@ -56,32 +57,6 @@ namespace py = pybind11; namespace { -//select only the first node of the graph of each run, used for parameterstudy with single nodes -template -std::vector filter_graph_results( - std::vector, mio::MobilityEdge>, - double, double>>&& graph_results) -{ - std::vector results; - results.reserve(graph_results.size()); - for (auto i = size_t(0); i < graph_results.size(); ++i) { - results.emplace_back(std::move(graph_results[i].get_graph().nodes()[0].property.get_simulation())); - } - return std::move(results); -} - -/// Moves out graphs from GraphSimulation%s. Helps make the new ParameterStudy backwards compatible with older bindings. -template -std::vector extract_graph_from_graph_simulation(std::vector&& result_graph_sims) -{ - std::vector result_graphs; - result_graphs.reserve(result_graph_sims.size()); - for (const SimulationT& sim : result_graph_sims) { - result_graphs.emplace_back(std::move(sim.get_graph())); - } - return result_graphs; -} - /* * @brief bind ParameterStudy for any model */ @@ -91,11 +66,11 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) using GraphT = mio::Graph, mio::MobilityEdge>; using SimulationT = mio::GraphSimulation; using ParametersT = mio::Graph>; - using StudyT = mio::ParameterStudy2; + using StudyT = mio::ParameterStudy2; const auto create_simulation = [](const ParametersT& g, double t0, double dt, size_t) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy), t0, dt, dt); + return mio::make_sampled_graph_simulation(draw_sample(copy), t0, dt, dt); }; pymio::bind_class(m, name.c_str()) @@ -112,7 +87,7 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) .def( "run", [&create_simulation](StudyT& self, std::function handle_result) { - self.run(create_simulation, [&handle_result](auto&& g, auto&& run_idx) { + self.run_serial(create_simulation, [&handle_result](auto&& g, auto&& run_idx) { //handle_result_function needs to return something //we don't want to run an unknown python object through parameterstudies, so //we just return 0 and ignore the list returned by run(). @@ -124,19 +99,24 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) py::arg("handle_result_func")) .def("run", [&create_simulation](StudyT& self) { //default argument doesn't seem to work with functions - return extract_graph_from_graph_simulation(self.run(create_simulation)); + return self.run_serial(create_simulation, [](SimulationT&& result, size_t) { + return std::move(result.get_graph()); + }); }) .def( "run_single", [&create_simulation](StudyT& self, std::function handle_result) { - self.run(create_simulation, [&handle_result](auto&& r, auto&& run_idx) { + self.run_serial(create_simulation, [&handle_result](auto&& r, auto&& run_idx) { handle_result(std::move(r.get_graph().nodes()[0].property.get_simulation()), run_idx); return 0; }); }, py::arg("handle_result_func")) .def("run_single", [&create_simulation](StudyT& self) { - return filter_graph_results(self.run(create_simulation)); + return self.run_serial(create_simulation, [](SimulationT&& result, size_t) { + //select only the first node of the graph of each run, used for parameterstudy with single nodes + return std::move(result.get_graph().nodes()[0].property.get_simulation()); + }); }); } diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp index 7bb1210402..1a86208697 100755 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp @@ -49,31 +49,6 @@ namespace py = pybind11; namespace { -//select only the first node of the graph of each run, used for parameterstudy with single nodes -template -std::vector filter_graph_results( - std::vector, mio::MobilityEdge>, - double, double>>&& graph_results) -{ - std::vector results; - results.reserve(graph_results.size()); - for (auto i = size_t(0); i < graph_results.size(); ++i) { - results.emplace_back(std::move(graph_results[i].get_graph().nodes()[0].property.get_simulation())); - } - return std::move(results); -} - -/// Moves out graphs from GraphSimulation%s. Helps make the new ParameterStudy backwards compatible with older bindings. -template -std::vector extract_graph_from_graph_simulation(std::vector&& result_graph_sims) -{ - std::vector result_graphs; - result_graphs.reserve(result_graph_sims.size()); - for (const SimulationT& sim : result_graph_sims) { - result_graphs.emplace_back(std::move(sim.get_graph())); - } - return result_graphs; -} /* * @brief bind ParameterStudy for any model @@ -84,7 +59,7 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) using GraphT = mio::Graph, mio::MobilityEdge>; using SimulationT = mio::GraphSimulation; using ParametersT = mio::Graph>; - using StudyT = mio::ParameterStudy2; + using StudyT = mio::ParameterStudy2; pymio::bind_class(m, name.c_str()) .def(py::init(), py::arg("parameters"), py::arg("t0"), @@ -93,7 +68,6 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) .def_property_readonly("tmax", &StudyT::get_tmax) .def_property_readonly("t0", &StudyT::get_t0) .def_property_readonly("dt", &StudyT::get_dt) - .def_property_readonly("model_graph", py::overload_cast<>(&StudyT::get_parameters), py::return_value_policy::reference_internal) .def_property_readonly("model_graph", py::overload_cast<>(&StudyT::get_parameters, py::const_), @@ -104,11 +78,11 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) std::function, mio::MobilityEdge>, size_t)> handle_result, bool variant_high) { - self.run( + self.run_serial( [variant_high](const ParametersT& g, double t0, double dt, size_t) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), - t0, dt, dt); + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), t0, dt, + dt); }, [&handle_result](auto&& g, auto&& run_idx) { //handle_result_function needs to return something @@ -124,22 +98,25 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) "run", [](StudyT& self, bool variant_high) { //default argument doesn't seem to work with functions - return extract_graph_from_graph_simulation( - self.run([variant_high](const ParametersT& g, double t0, double dt, size_t) { + return self.run_serial( + [variant_high](const ParametersT& g, double t0, double dt, size_t) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), - t0, dt, dt); - })); + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), t0, dt, + dt); + }, + [](SimulationT&& result, size_t) { + return std::move(result.get_graph()); + }); }, py::arg("variant_high")) .def( "run_single", [](StudyT& self, std::function handle_result, bool variant_high) { - self.run( + self.run_serial( [variant_high](const ParametersT& g, double t0, double dt, size_t) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), - t0, dt, dt); + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), t0, dt, + dt); }, [&handle_result](auto&& r, auto&& run_idx) { handle_result(std::move(r.get_graph().nodes()[0].property.get_simulation()), run_idx); @@ -150,12 +127,16 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) .def( "run_single", [](StudyT& self, bool variant_high) { - return filter_graph_results( - self.run([variant_high](const ParametersT& g, double t0, double dt, size_t) { + return self.run_serial( + [variant_high](const ParametersT& g, double t0, double dt, size_t) { auto copy = g; - return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), - t0, dt, dt); - })); + return mio::make_sampled_graph_simulation(draw_sample(copy, variant_high), t0, dt, + dt); + }, + [](SimulationT&& result, size_t) { + //select only the first node of the graph of each run, used for parameterstudy with single nodes + return std::move(result.get_graph().nodes()[0].property.get_simulation()); + }); }, py::arg("variant_high")); } From 63b7a6d50650d9b50f77aa62a7a7b1a5908e91d4 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:52:47 +0200 Subject: [PATCH 097/169] remove developement artifacts --- cpp/memilio/io/io.h | 19 ------------------- cpp/memilio/utils/custom_index_array.h | 13 ------------- 2 files changed, 32 deletions(-) diff --git a/cpp/memilio/io/io.h b/cpp/memilio/io/io.h index 13c2d30045..fdcf9424d4 100644 --- a/cpp/memilio/io/io.h +++ b/cpp/memilio/io/io.h @@ -857,15 +857,6 @@ void serialize(IOContext& io, const T& t) serialize_internal(io, t); } -namespace details -{ -template -using is_serializable_expr_t = decltype(serialize(std::declval(), std::declval())); -} - -template -static constexpr bool is_serializable = is_expression_valid::value; - /** * Restores an object from the data stored in an IO context. * There must be provided for the type T either a free function `deserialize_internal(io, tag)` @@ -890,16 +881,6 @@ IOResult deserialize(IOContext& io, Tag tag) return deserialize_internal(io, tag); } -namespace details -{ -template -using is_deserializable_expr_t = - decltype(std::declval>() = deserialize(std::declval(), std::declval>())); -} - -template -static constexpr bool is_deserializable = is_expression_valid::value; - /** * @brief Returns the current working directory name */ diff --git a/cpp/memilio/utils/custom_index_array.h b/cpp/memilio/utils/custom_index_array.h index e23991db03..d17d496275 100644 --- a/cpp/memilio/utils/custom_index_array.h +++ b/cpp/memilio/utils/custom_index_array.h @@ -23,8 +23,6 @@ #include "memilio/math/eigen_util.h" #include "memilio/utils/index.h" #include "memilio/utils/stl_util.h" -#include -#include namespace { @@ -92,17 +90,6 @@ std::enable_if_t<(I < (Index::size - 1)), std::pair> flatten_ind return {val + (size_t)mio::get(indices) * prod, prod * (size_t)mio::get(dimensions)}; } -template -void assign_from_vector(Index& index, const std::vector& values) -{ - assert(values.size() == sizeof...(Tags)); - - if constexpr (I < sizeof...(Tags)) { - get(index) = values[I]; - assign_from_vector(index, values); - } -} - template struct is_random_access_iterator : std::is_base_of::iterator_category, std::random_access_iterator_tag> { From 9bae572c2c12b9141468b9eed763bd73ac602ac5 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:41:08 +0100 Subject: [PATCH 098/169] small cleanup, add test --- cpp/memilio/compartments/parameter_studies.h | 42 +++---------- cpp/tests/test_parameter_studies.cpp | 66 +++++++++++++++++++- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index f3eb730b66..f87610521e 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -199,49 +199,25 @@ class ParameterStudy2 return ensemble_result; } - /** - * @brief Carry out all simulations in the parameter study. @see run. - * Convenience function for a few number of runs, but can use more memory because it stores every single simulaiton. - * If this function causes errors, they may be caused by the simulation not being serializable. In that case, try - * using run_serial, provide a processing function, or make the simulation serializable. - * @return Vector of SimulationGraph for each run. - */ - template - std::vector> run(CreateSimulationFunction&& create_simulation) - { - return run(std::forward(create_simulation), - [](SimulationT&& sim, size_t) -> SimulationT&& { - return std::move(sim); - }); - } - - /** - * @brief Return the number of total runs that the study will make. - */ + /// @brief Return the number of total runs that the study will make. size_t get_num_runs() const { return m_num_runs; } - /** - * @brief Return the final time point for simulations. - */ + /// @brief Return the final time point for simulations. Time get_tmax() const { return m_tmax; } - /** - * @brief Return the initial time point for simulations. - */ + /// @brief Return the initial time point for simulations. Time get_t0() const { return m_t0; } - /** - * @brief Return the initial step sized used by simulations. - */ + /// @brief Return the initial step sized used by simulations. Time get_dt() const { return m_dt; @@ -261,9 +237,7 @@ class ParameterStudy2 } /** @} */ - /** - * @brief Access the study's random number generator. - */ + /// @brief Access the study's random number generator. RandomNumberGenerator& get_rng() { return m_rng; @@ -324,11 +298,13 @@ class ParameterStudy2 } /** - * @brief Create a vector with the number of runs each process should make. + * @brief Distribute a number of runs over a number of processes. + * Processes with low ranks get additional runs, if the number is not evenly divisible. * @param num_runs The total number of runs. * @param num_procs The total number of processes, i.e. the size of MPI_Comm. + * @return A vector of size num_procs with the number of runs each process should make. */ - std::vector distribute_runs(size_t num_runs, int num_procs) + static std::vector distribute_runs(size_t num_runs, int num_procs) { assert(num_procs > 0); //evenly distribute runs diff --git a/cpp/tests/test_parameter_studies.cpp b/cpp/tests/test_parameter_studies.cpp index 2d633d667a..5140f3b54c 100644 --- a/cpp/tests/test_parameter_studies.cpp +++ b/cpp/tests/test_parameter_studies.cpp @@ -18,13 +18,16 @@ * limitations under the License. */ #include "memilio/config.h" +#include "memilio/utils/miompi.h" #include "memilio/utils/parameter_distributions.h" #include "ode_secir/model.h" #include "ode_secir/parameter_space.h" #include "memilio/compartments/parameter_studies.h" #include "memilio/mobility/metapopulation_mobility_instant.h" #include "memilio/utils/random_number_generator.h" +#include #include +#include #include TEST(ParameterStudies, sample_from_secir_params) @@ -401,6 +404,63 @@ TEST(ParameterStudies, check_ensemble_run_result) } } -// TEST(ParameterStudies, run_mocks) { -// mio::ParameterStudy2 -// } +namespace +{ + +struct MockStudyParams { + const int init, run; +}; + +struct MockStudySim { + MockStudySim(const MockStudyParams& p_, double t0_, double dt_) + : p(p_) + , t0(t0_) + , dt(dt_) + { + } + void advance(double t) + { + tmax = t; + } + + MockStudyParams p; + double t0, dt; + double tmax = 0; +}; + +} // namespace + +TEST(ParameterStudies, mocked_run) +{ + // run a very simple study, that works with mpi + const double t0 = 20, tmax = 21, dt = 22; + const MockStudyParams params{23, -1}; + const size_t num_runs = 5; // enough to notice MPI effects + const auto make_sim = [&](auto&& params_, auto t0_, auto dt_, auto i_) { + MockStudyParams cp{params_.init, (int)i_}; + return MockStudySim(cp, t0_, dt_); + }; + const auto process_sim = [&](MockStudySim&& s, size_t i) { + return s.tmax + i; + }; + const double process_sim_result = (num_runs * tmax) + num_runs * (num_runs - 1) / 2.; + mio::ParameterStudy2 study(params, t0, tmax, dt, num_runs); + // case: run_serial without processing; expect created simulations in order + auto result_serial = study.run_serial(make_sim); + EXPECT_EQ(result_serial.size(), num_runs); + for (int i = 0; const auto& sim : result_serial) { + EXPECT_EQ(sim.t0, t0); + EXPECT_EQ(sim.dt, dt); + EXPECT_EQ(sim.tmax, tmax); + EXPECT_EQ(sim.p.init, params.init); + EXPECT_EQ(sim.p.run, i++); + } + // case: run and run_serial with processing; expect the same (unordered) result for both, on all ranks + // Note: currently the tests are not make use of MPI, so we expect the same result from each rank + auto result_serial_processed = study.run_serial(make_sim, process_sim); + auto result_parallel = study.run(make_sim, process_sim); + for (const auto& result : {result_serial_processed, result_parallel}) { + EXPECT_EQ(result.size(), num_runs); + EXPECT_EQ(std::accumulate(result.begin(), result.end(), 0.0), process_sim_result); + } +} From 651f1baea26ce4667480f66e46d81fbfb5f5c02a Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:35:06 +0100 Subject: [PATCH 099/169] change name back to ParameterStudy --- cpp/examples/abm_parameter_study.cpp | 2 +- cpp/examples/ode_secir_parameter_study.cpp | 2 +- cpp/examples/ode_secir_parameter_study_graph.cpp | 2 +- cpp/examples/ode_secir_read_graph.cpp | 2 +- cpp/memilio/compartments/parameter_studies.h | 4 ++-- cpp/tests/test_parameter_studies.cpp | 6 +++--- cpp/tests/test_save_results.cpp | 4 ++-- .../memilio/simulation/bindings/models/osecir.cpp | 2 +- .../memilio/simulation/bindings/models/osecirvvs.cpp | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cpp/examples/abm_parameter_study.cpp b/cpp/examples/abm_parameter_study.cpp index f0a2558d6f..4d609f323c 100644 --- a/cpp/examples/abm_parameter_study.cpp +++ b/cpp/examples/abm_parameter_study.cpp @@ -178,7 +178,7 @@ int main() const size_t num_runs = 3; // Create a parameter study. The ABM currently does not use parameters or dt, so we set them both to 0. - mio::ParameterStudy2 study(0, t0, tmax, mio::abm::TimeSpan(0), num_runs); + mio::ParameterStudy study(0, t0, tmax, mio::abm::TimeSpan(0), num_runs); // Optional: set seeds to get reproducable results // study.get_rng().seed({12341234, 53456, 63451, 5232576, 84586, 52345}); diff --git a/cpp/examples/ode_secir_parameter_study.cpp b/cpp/examples/ode_secir_parameter_study.cpp index 0d2ff9c7d7..7450226ef4 100644 --- a/cpp/examples/ode_secir_parameter_study.cpp +++ b/cpp/examples/ode_secir_parameter_study.cpp @@ -127,7 +127,7 @@ int main() // create study auto num_runs = size_t(3); - mio::ParameterStudy2 parameter_study(model, t0, tmax, dt, num_runs); + mio::ParameterStudy parameter_study(model, t0, tmax, dt, num_runs); // set up for run auto sample_graph = [](const auto& model_, ScalarType t0_, ScalarType dt_, size_t) { diff --git a/cpp/examples/ode_secir_parameter_study_graph.cpp b/cpp/examples/ode_secir_parameter_study_graph.cpp index b0f8202f0b..f17d019f71 100644 --- a/cpp/examples/ode_secir_parameter_study_graph.cpp +++ b/cpp/examples/ode_secir_parameter_study_graph.cpp @@ -262,7 +262,7 @@ int main() 2, 1, Eigen::VectorX::Constant(num_age_groups * (size_t)mio::osecir::InfectionState::Count, 0.2), indices_save_edges); - mio::ParameterStudy2 parameter_study(params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)); + mio::ParameterStudy parameter_study(params_graph, 0.0, num_days_sim, 0.5, size_t(num_runs)); if (mio::mpi::is_root()) { printf("Seeds: "); diff --git a/cpp/examples/ode_secir_read_graph.cpp b/cpp/examples/ode_secir_read_graph.cpp index e282ef5468..41cdf89b24 100644 --- a/cpp/examples/ode_secir_read_graph.cpp +++ b/cpp/examples/ode_secir_read_graph.cpp @@ -146,7 +146,7 @@ int main(int argc, char** argv) auto& graph_read = graph_read_result.value(); std::cout << "Running Simulations..." << std::flush; - mio::ParameterStudy2 study(graph_read, t0, tmax, 0.5, 2); + mio::ParameterStudy study(graph_read, t0, tmax, 0.5, 2); study.run_serial([](auto&& g, auto t0_, auto dt_, auto) { auto copy = g; return mio::make_sampled_graph_simulation>(draw_sample(copy), t0_, diff --git a/cpp/memilio/compartments/parameter_studies.h b/cpp/memilio/compartments/parameter_studies.h index f87610521e..359c30bedf 100644 --- a/cpp/memilio/compartments/parameter_studies.h +++ b/cpp/memilio/compartments/parameter_studies.h @@ -47,7 +47,7 @@ namespace mio * @tparam StepType The type used for time steps, e.g. double or TimeStep. */ template -class ParameterStudy2 +class ParameterStudy { public: using Parameters = ParameterType; @@ -76,7 +76,7 @@ class ParameterStudy2 * @param dt Initial time step of simulations. * @param num_runs Number of simulations that will be created and run. */ - ParameterStudy2(const Parameters& parameters, Time t0, Time tmax, Step dt, size_t num_runs) + ParameterStudy(const Parameters& parameters, Time t0, Time tmax, Step dt, size_t num_runs) : m_parameters(parameters) , m_num_runs(num_runs) , m_t0{t0} diff --git a/cpp/tests/test_parameter_studies.cpp b/cpp/tests/test_parameter_studies.cpp index 5140f3b54c..01e9f6e6a1 100644 --- a/cpp/tests/test_parameter_studies.cpp +++ b/cpp/tests/test_parameter_studies.cpp @@ -161,7 +161,7 @@ TEST(ParameterStudies, sample_graph) graph.add_node(1, model); graph.add_edge(0, 1, mio::MobilityParameters(Eigen::VectorXd::Constant(Eigen::Index(num_groups * 8), 1.0))); - mio::ParameterStudy2 study(graph, 0.0, 0.0, 0.5, 1); + mio::ParameterStudy study(graph, 0.0, 0.0, 0.5, 1); mio::log_rng_seeds(study.get_rng(), mio::LogLevel::warn); auto ensemble_results = study.run_serial([](auto&& g, auto t0_, auto dt_, auto) { auto copy = g; @@ -377,7 +377,7 @@ TEST(ParameterStudies, check_ensemble_run_result) mio::ContactMatrix(Eigen::MatrixXd::Constant((size_t)num_groups, (size_t)num_groups, fact * cont_freq)); mio::osecir::set_params_distributions_normal(model, t0, tmax, 0.2); - mio::ParameterStudy2 parameter_study(model, t0, tmax, 0.1, 1); + mio::ParameterStudy parameter_study(model, t0, tmax, 0.1, 1); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); // Run parameter study @@ -444,7 +444,7 @@ TEST(ParameterStudies, mocked_run) return s.tmax + i; }; const double process_sim_result = (num_runs * tmax) + num_runs * (num_runs - 1) / 2.; - mio::ParameterStudy2 study(params, t0, tmax, dt, num_runs); + mio::ParameterStudy study(params, t0, tmax, dt, num_runs); // case: run_serial without processing; expect created simulations in order auto result_serial = study.run_serial(make_sim); EXPECT_EQ(result_serial.size(), num_runs); diff --git a/cpp/tests/test_save_results.cpp b/cpp/tests/test_save_results.cpp index 4f87787fb9..f706327ce6 100644 --- a/cpp/tests/test_save_results.cpp +++ b/cpp/tests/test_save_results.cpp @@ -170,7 +170,7 @@ TEST(TestSaveResult, save_result_with_params) mio::MobilityParameters(Eigen::VectorXd::Constant(Eigen::Index(num_groups * 10), 1.0))); auto num_runs = 3; - mio::ParameterStudy2 parameter_study(graph, 0.0, 2.0, 0.5, num_runs); + mio::ParameterStudy parameter_study(graph, 0.0, 2.0, 0.5, num_runs); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); TempFileRegister tmp_file_register; @@ -306,7 +306,7 @@ TEST(TestSaveResult, save_percentiles_and_sums) indices_save_edges)); auto num_runs = 3; - mio::ParameterStudy2 parameter_study(graph, 0.0, 2.0, 0.5, num_runs); + mio::ParameterStudy parameter_study(graph, 0.0, 2.0, 0.5, num_runs); mio::log_rng_seeds(parameter_study.get_rng(), mio::LogLevel::warn); TempFileRegister tmp_file_register; diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp index 6d4e71dd9d..881c93b088 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecir.cpp @@ -66,7 +66,7 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) using GraphT = mio::Graph, mio::MobilityEdge>; using SimulationT = mio::GraphSimulation; using ParametersT = mio::Graph>; - using StudyT = mio::ParameterStudy2; + using StudyT = mio::ParameterStudy; const auto create_simulation = [](const ParametersT& g, double t0, double dt, size_t) { auto copy = g; diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp index 1a86208697..9c276310b8 100755 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp @@ -59,7 +59,7 @@ void bind_ParameterStudy(py::module_& m, std::string const& name) using GraphT = mio::Graph, mio::MobilityEdge>; using SimulationT = mio::GraphSimulation; using ParametersT = mio::Graph>; - using StudyT = mio::ParameterStudy2; + using StudyT = mio::ParameterStudy; pymio::bind_class(m, name.c_str()) .def(py::init(), py::arg("parameters"), py::arg("t0"), From dab1c992aa3c028686e0440a7f20cf4de6eb8871 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:15:17 +0100 Subject: [PATCH 100/169] CHG: Add timers --- cpp/examples/asymmetric_graph.cpp | 62 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 443f0728a0..21f6803e4f 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -25,6 +25,7 @@ #include "memilio/mobility/graph.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" +#include "memilio/timer/auto_timer.h" #include "memilio/utils/parameter_distributions.h" #include "memilio/utils/random_number_generator.h" #include "smm/simulation.h" @@ -77,44 +78,47 @@ int main(int /*argc*/, char** /*argv*/) mio::MobilityEdgeDirected> graph; mio::log_info("Starting Graph generation"); - - io::CSVReader<4> farms("../../farms.csv"); - farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); - int farm_id, num_cows; - double latitude, longitude; - while (farms.read_row(farm_id, num_cows, latitude, longitude)) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows; - curr_model.populations[{home, InfectionState::E}] = 1; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - graph.add_node(farm_id, longitude, latitude, curr_model, t0); + { + mio::timing::AutoTimer<"Graph Nodes Generation"> timer; + io::CSVReader<4> farms("/home/kilian/Documents/projects/memilio-asymmetric-graph/farms.csv"); + farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); + int farm_id, num_cows; + double latitude, longitude; + while (farms.read_row(farm_id, num_cows, latitude, longitude)) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows; + curr_model.populations[{home, InfectionState::E}] = 1; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph.add_node(farm_id, longitude, latitude, curr_model, t0); + } } - + mio::log_info("Nodes added to Graph"); auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); - - io::CSVReader<2> edges("../../edges.csv"); - edges.read_header(io::ignore_extra_column, "from", "to"); - size_t from, to; - while (edges.read_row(from, to)) { - graph.add_edge(from, to, interesting_indices); - // graph.lazy_add_edge(from, to, interesting_indices); + { + mio::timing::AutoTimer<"Graph Edges Generation"> timer; + io::CSVReader<2> edges("/home/kilian/Documents/projects/memilio-asymmetric-graph/edges.csv"); + edges.read_header(io::ignore_extra_column, "from", "to"); + size_t from, to; + while (edges.read_row(from, to)) { + graph.add_edge(from, to, interesting_indices); + // graph.lazy_add_edge(from, to, interesting_indices); + } + // graph.sort_edges(); } - // graph.sort_edges(); - mio::log_info("Graph generated"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); - auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + auto tree = mio::geo::RTree({nodes.begin(), nodes.end()}); mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { @@ -122,13 +126,13 @@ int main(int /*argc*/, char** /*argv*/) } mio::log_info("Neighbors set"); - auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - io::CSVReader<5> exchanges("../../trade.csv"); + io::CSVReader<5> exchanges("/home/kilian/Documents/projects/memilio-asymmetric-graph/trade.csv"); exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); int date, num_animals, edge; + size_t from, to; while (exchanges.read_row(date, num_animals, from, to, edge)) { sim.add_exchange(date, num_animals, from, to); } From b5048d83d18e62f0d3ddcaeb7f3389cf205659bc Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:17:41 +0100 Subject: [PATCH 101/169] CHG: Add timers --- cpp/examples/asymmetric_graph.cpp | 60 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 443f0728a0..22a54b3035 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -25,6 +25,7 @@ #include "memilio/mobility/graph.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" +#include "memilio/timer/auto_timer.h" #include "memilio/utils/parameter_distributions.h" #include "memilio/utils/random_number_generator.h" #include "smm/simulation.h" @@ -77,44 +78,47 @@ int main(int /*argc*/, char** /*argv*/) mio::MobilityEdgeDirected> graph; mio::log_info("Starting Graph generation"); - - io::CSVReader<4> farms("../../farms.csv"); - farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); - int farm_id, num_cows; - double latitude, longitude; - while (farms.read_row(farm_id, num_cows, latitude, longitude)) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows; - curr_model.populations[{home, InfectionState::E}] = 1; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - graph.add_node(farm_id, longitude, latitude, curr_model, t0); + { + mio::timing::AutoTimer<"Graph Nodes Generation"> timer; + io::CSVReader<4> farms("../../farms.csv"); + farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); + int farm_id, num_cows; + double latitude, longitude; + while (farms.read_row(farm_id, num_cows, latitude, longitude)) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows; + curr_model.populations[{home, InfectionState::E}] = 1; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph.add_node(farm_id, longitude, latitude, curr_model, t0); + } } - + mio::log_info("Nodes added to Graph"); auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); - - io::CSVReader<2> edges("../../edges.csv"); - edges.read_header(io::ignore_extra_column, "from", "to"); - size_t from, to; - while (edges.read_row(from, to)) { - graph.add_edge(from, to, interesting_indices); - // graph.lazy_add_edge(from, to, interesting_indices); + { + mio::timing::AutoTimer<"Graph Edges Generation"> timer; + io::CSVReader<2> edges("../../edges.csv"); + edges.read_header(io::ignore_extra_column, "from", "to"); + size_t from, to; + while (edges.read_row(from, to)) { + graph.add_edge(from, to, interesting_indices); + // graph.lazy_add_edge(from, to, interesting_indices); + } + // graph.sort_edges(); } - // graph.sort_edges(); - mio::log_info("Graph generated"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); - auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + auto tree = mio::geo::RTree({nodes.begin(), nodes.end()}); mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { @@ -122,13 +126,13 @@ int main(int /*argc*/, char** /*argv*/) } mio::log_info("Neighbors set"); - auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); io::CSVReader<5> exchanges("../../trade.csv"); exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); int date, num_animals, edge; + size_t from, to; while (exchanges.read_row(date, num_animals, from, to, edge)) { sim.add_exchange(date, num_animals, from, to); } From 99e42d4c948affd79890c5544da1a49bcf38ac46 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:20:56 +0100 Subject: [PATCH 102/169] . --- cpp/examples/asymmetric_graph.cpp | 125 ++++++++++++++---------------- 1 file changed, 57 insertions(+), 68 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index e5e3570941..156642831c 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -80,11 +80,7 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; -<<<<<<< HEAD io::CSVReader<4> farms("../../farms.csv"); -======= - io::CSVReader<4> farms("/home/kilian/Documents/projects/memilio-asymmetric-graph/farms.csv"); ->>>>>>> dab1c992aa3c028686e0440a7f20cf4de6eb8871 farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); int farm_id, num_cows; double latitude, longitude; @@ -108,89 +104,82 @@ int main(int /*argc*/, char** /*argv*/) interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); { mio::timing::AutoTimer<"Graph Edges Generation"> timer; -<<<<<<< HEAD io::CSVReader<2> edges("../../edges.csv"); -======= - io::CSVReader<2> edges("/home/kilian/Documents/projects/memilio-asymmetric-graph/edges.csv"); ->>>>>>> dab1c992aa3c028686e0440a7f20cf4de6eb8871 edges.read_header(io::ignore_extra_column, "from", "to"); - size_t from, to; - while (edges.read_row(from, to)) { - graph.add_edge(from, to, interesting_indices); - // graph.lazy_add_edge(from, to, interesting_indices); - } - // graph.sort_edges(); + // graph.lazy_add_edge(from, to, interesting_indices); } - mio::log_info("Graph generated"); + // graph.sort_edges(); +} +mio::log_info("Graph generated"); - auto nodes = graph.nodes() | std::views::transform([](const auto& node) { - return &node.property; - }); - auto tree = mio::geo::RTree({nodes.begin(), nodes.end()}); - mio::log_info("RTree generated"); +auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); +auto tree = mio::geo::RTree({nodes.begin(), nodes.end()}); +mio::log_info("RTree generated"); - for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {2.0})); - } +for (auto& node : graph.nodes()) { + node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {2.0})); +} - mio::log_info("Neighbors set"); - auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); +mio::log_info("Neighbors set"); +auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - io::CSVReader<5> exchanges("/home/kilian/Documents/projects/memilio-asymmetric-graph/trade.csv"); - exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); +io::CSVReader<5> exchanges("../../trade.csv"); +exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); - int date, num_animals, edge; - size_t from, to; - while (exchanges.read_row(date, num_animals, from, to, edge)) { - sim.add_exchange(date, num_animals, from, to); - } - mio::log_info("Exchanges added"); +int date, num_animals, edge; +size_t from, to; +while (exchanges.read_row(date, num_animals, from, to, edge)) { + sim.add_exchange(date, num_animals, from, to); +} +mio::log_info("Exchanges added"); - // #ifdef MEMILIO_ENABLE_OPENMP - // #pragma omp parallel for - // for (size_t i = 0; i < 100; ++i) { - // #endif +// #ifdef MEMILIO_ENABLE_OPENMP +// #pragma omp parallel for +// for (size_t i = 0; i < 100; ++i) { +// #endif - auto sim2(sim); - mio::log_info("new Simulation created"); +auto sim2(sim); +mio::log_info("new Simulation created"); - sim2.advance(tmax); - mio::log_info("Simulation finished"); +sim2.advance(tmax); +mio::log_info("Simulation finished"); - // #ifdef MEMILIO_ENABLE_OPENMP - // } - // #endif +// #ifdef MEMILIO_ENABLE_OPENMP +// } +// #endif - // sim2.get_graph().nodes()[28].property.get_result().print_table({"S", "E", "I", "R", "D"}); - // std::cout << "Second Table" << std::endl; - // sim2.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R", "D"}); +// sim2.get_graph().nodes()[28].property.get_result().print_table({"S", "E", "I", "R", "D"}); +// std::cout << "Second Table" << std::endl; +// sim2.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R", "D"}); - // auto& edge_1_0 = sim2.get_graph().edges()[1]; - // auto& results = edge_1_0.property.get_mobility_results(); - // results.print_table({"Commuter Sick", "Commuter Total"}); +// auto& edge_1_0 = sim2.get_graph().edges()[1]; +// auto& results = edge_1_0.property.get_mobility_results(); +// results.print_table({"Commuter Sick", "Commuter Total"}); - // auto exchange_results = sim2.sum_exchanges(); - // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); - // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); +// auto exchange_results = sim2.sum_exchanges(); +// mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); +// mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); - auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); +auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); - // for (auto node : sim2.get_graph().nodes()) { - // if (node.property.get_result().get_num_time_points() < num_time_points) { - // mio::log_error("Node with inconsistent number of time points in results."); - // } - // } +// for (auto node : sim2.get_graph().nodes()) { +// if (node.property.get_result().get_num_time_points() < num_time_points) { +// mio::log_error("Node with inconsistent number of time points in results."); +// } +// } - // exchange_results = sim2.sum_nodes(); - // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); +// exchange_results = sim2.sum_nodes(); +// mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); - sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); - // // auto combined_results = sim2.combine_node_results(); - // // combined_results.print_table({"S", "E", "I", "R", "D"}); - // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); +sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); +// // auto combined_results = sim2.combine_node_results(); +// // combined_results.print_table({"S", "E", "I", "R", "D"}); +// // auto ioresult = combined_results.export_csv("Simulation_results.csv"); - // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); - mio::log_info("Finished postprocessing"); +// sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); +mio::log_info("Finished postprocessing"); - return 0; +return 0; } From 187b36a3209826d838b9bbe668379bd223f6250b Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:28:34 +0100 Subject: [PATCH 103/169] FIX --- cpp/examples/asymmetric_graph.cpp | 117 +++++++++++++++--------------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 156642831c..22a54b3035 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -106,80 +106,83 @@ int main(int /*argc*/, char** /*argv*/) mio::timing::AutoTimer<"Graph Edges Generation"> timer; io::CSVReader<2> edges("../../edges.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); - // graph.lazy_add_edge(from, to, interesting_indices); + size_t from, to; + while (edges.read_row(from, to)) { + graph.add_edge(from, to, interesting_indices); + // graph.lazy_add_edge(from, to, interesting_indices); + } + // graph.sort_edges(); } - // graph.sort_edges(); -} -mio::log_info("Graph generated"); + mio::log_info("Graph generated"); -auto nodes = graph.nodes() | std::views::transform([](const auto& node) { - return &node.property; - }); -auto tree = mio::geo::RTree({nodes.begin(), nodes.end()}); -mio::log_info("RTree generated"); + auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + auto tree = mio::geo::RTree({nodes.begin(), nodes.end()}); + mio::log_info("RTree generated"); -for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {2.0})); -} + for (auto& node : graph.nodes()) { + node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {2.0})); + } -mio::log_info("Neighbors set"); -auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); + mio::log_info("Neighbors set"); + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); -io::CSVReader<5> exchanges("../../trade.csv"); -exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); + io::CSVReader<5> exchanges("../../trade.csv"); + exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); -int date, num_animals, edge; -size_t from, to; -while (exchanges.read_row(date, num_animals, from, to, edge)) { - sim.add_exchange(date, num_animals, from, to); -} -mio::log_info("Exchanges added"); + int date, num_animals, edge; + size_t from, to; + while (exchanges.read_row(date, num_animals, from, to, edge)) { + sim.add_exchange(date, num_animals, from, to); + } + mio::log_info("Exchanges added"); -// #ifdef MEMILIO_ENABLE_OPENMP -// #pragma omp parallel for -// for (size_t i = 0; i < 100; ++i) { -// #endif + // #ifdef MEMILIO_ENABLE_OPENMP + // #pragma omp parallel for + // for (size_t i = 0; i < 100; ++i) { + // #endif -auto sim2(sim); -mio::log_info("new Simulation created"); + auto sim2(sim); + mio::log_info("new Simulation created"); -sim2.advance(tmax); -mio::log_info("Simulation finished"); + sim2.advance(tmax); + mio::log_info("Simulation finished"); -// #ifdef MEMILIO_ENABLE_OPENMP -// } -// #endif + // #ifdef MEMILIO_ENABLE_OPENMP + // } + // #endif -// sim2.get_graph().nodes()[28].property.get_result().print_table({"S", "E", "I", "R", "D"}); -// std::cout << "Second Table" << std::endl; -// sim2.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R", "D"}); + // sim2.get_graph().nodes()[28].property.get_result().print_table({"S", "E", "I", "R", "D"}); + // std::cout << "Second Table" << std::endl; + // sim2.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R", "D"}); -// auto& edge_1_0 = sim2.get_graph().edges()[1]; -// auto& results = edge_1_0.property.get_mobility_results(); -// results.print_table({"Commuter Sick", "Commuter Total"}); + // auto& edge_1_0 = sim2.get_graph().edges()[1]; + // auto& results = edge_1_0.property.get_mobility_results(); + // results.print_table({"Commuter Sick", "Commuter Total"}); -// auto exchange_results = sim2.sum_exchanges(); -// mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); -// mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); + // auto exchange_results = sim2.sum_exchanges(); + // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); + // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); -auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); + auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); -// for (auto node : sim2.get_graph().nodes()) { -// if (node.property.get_result().get_num_time_points() < num_time_points) { -// mio::log_error("Node with inconsistent number of time points in results."); -// } -// } + // for (auto node : sim2.get_graph().nodes()) { + // if (node.property.get_result().get_num_time_points() < num_time_points) { + // mio::log_error("Node with inconsistent number of time points in results."); + // } + // } -// exchange_results = sim2.sum_nodes(); -// mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); + // exchange_results = sim2.sum_nodes(); + // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); -sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); -// // auto combined_results = sim2.combine_node_results(); -// // combined_results.print_table({"S", "E", "I", "R", "D"}); -// // auto ioresult = combined_results.export_csv("Simulation_results.csv"); + sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); + // // auto combined_results = sim2.combine_node_results(); + // // combined_results.print_table({"S", "E", "I", "R", "D"}); + // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); -// sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); -mio::log_info("Finished postprocessing"); + // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); + mio::log_info("Finished postprocessing"); -return 0; + return 0; } From bdc7cd6e2a8ddb12bf14ae41a27e302497a0e8f5 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:31:51 +0100 Subject: [PATCH 104/169] CHG: Add -Wno-shadow --- cpp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 3ce345b3fd..4e4a1e1a8c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -135,7 +135,7 @@ endif() # exclude some warnings we accept if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") string(APPEND MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS - "-Wno-unknown-warning;-Wno-pragmas;-Wno-deprecated-copy;-Wno-expansion-to-defined;") + "-Wno-unknown-warning;-Wno-pragmas;-Wno-deprecated-copy;-Wno-expansion-to-defined;-Wno-shadow;") elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") string(APPEND MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS "-Wno-unknown-warning-option;-Wno-deprecated;-Wno-gnu-zero-variadic-macro-arguments;") From ea3acdea0b170f83cd0ff48f2766a18c9578f8cf Mon Sep 17 00:00:00 2001 From: Sascha <51127093+xsaschako@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:33:05 +0100 Subject: [PATCH 105/169] update parameters to run --- cpp/CMakeLists.txt | 2 +- cpp/examples/abm_parameter_study.cpp | 10 +++++----- cpp/models/abm/common_abm_loggers.h | 17 +++++++++-------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 3ce345b3fd..8eb526845d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -18,7 +18,7 @@ option(MEMILIO_SANITIZE_ADDRESS "Enable address sanitizer." OFF) option(MEMILIO_SANITIZE_UNDEFINED "Enable undefined behavior sanitizer." OFF) option(MEMILIO_BUILD_SHARED_LIBS "Build memilio as a shared library." ON) option(MEMILIO_BUILD_STATIC_LIBS "Build memilio as a static library." ON) -option(MEMILIO_ENABLE_MPI "Build memilio with MPI." OFF) +option(MEMILIO_ENABLE_MPI "Build memilio with MPI." ON) option(MEMILIO_ENABLE_OPENMP "Enable Multithreading with OpenMP." OFF) option(MEMILIO_ENABLE_WARNINGS "Build memilio with warnings." ON) option(MEMILIO_ENABLE_WARNINGS_AS_ERRORS "Build memilio with warnings as errors." ON) diff --git a/cpp/examples/abm_parameter_study.cpp b/cpp/examples/abm_parameter_study.cpp index 4d609f323c..cf822c8265 100644 --- a/cpp/examples/abm_parameter_study.cpp +++ b/cpp/examples/abm_parameter_study.cpp @@ -62,7 +62,7 @@ mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) model.parameters.check_constraints(); // There are 10 households for each household group. - int n_households = 10; + int n_households = 100000; // For more than 1 family households we need families. These are parents and children and randoms (which are distributed like the data we have for these households). auto child = mio::abm::HouseholdMember(num_age_groups); // A child is 50/50% 0-4 or 5-14. @@ -119,7 +119,7 @@ mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) auto validity_period = mio::abm::days(1); auto probability = 0.5; auto start_date = mio::abm::TimePoint(0); - auto end_date = mio::abm::TimePoint(0) + mio::abm::days(10); + auto end_date = mio::abm::TimePoint(0) + mio::abm::days(40); auto test_type = mio::abm::TestType::Antigen; auto test_parameters = model.parameters.get()[test_type]; auto testing_criteria_work = mio::abm::TestingCriteria(); @@ -159,7 +159,7 @@ mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) } // During the lockdown, social events are closed for 90% of people. - auto t_lockdown = mio::abm::TimePoint(0) + mio::abm::days(10); + auto t_lockdown = mio::abm::TimePoint(0) + mio::abm::days(40); mio::abm::close_social_events(t_lockdown, 0.9, model.parameters); return model; @@ -173,9 +173,9 @@ int main() // Set start and end time for the simulation. auto t0 = mio::abm::TimePoint(0); - auto tmax = t0 + mio::abm::days(5); + auto tmax = t0 + mio::abm::days(60); // auto sim = mio::abm::Simulation(t0, std::move(model)); - const size_t num_runs = 3; + const size_t num_runs = 128; // Create a parameter study. The ABM currently does not use parameters or dt, so we set them both to 0. mio::ParameterStudy study(0, t0, tmax, mio::abm::TimeSpan(0), num_runs); diff --git a/cpp/models/abm/common_abm_loggers.h b/cpp/models/abm/common_abm_loggers.h index 0d490437f0..ca7db0f52e 100644 --- a/cpp/models/abm/common_abm_loggers.h +++ b/cpp/models/abm/common_abm_loggers.h @@ -166,7 +166,8 @@ struct LogDataForMobility : mio::LogAlways { * @brief Logger to log the TimeSeries of the number of Person%s in an #InfectionState. */ struct LogInfectionState : mio::LogAlways { - using Type = std::pair>; + using Type = std::pair; + /** * @brief Log the TimeSeries of the number of Person%s in an #InfectionState. * @param[in] sim The simulation of the abm. @@ -174,15 +175,15 @@ struct LogInfectionState : mio::LogAlways { */ static Type log(const mio::abm::Simulation<>& sim) { + Eigen::VectorXd sum = Eigen::VectorXd::Zero(Eigen::Index(mio::abm::InfectionState::Count)); + auto curr_time = sim.get_time(); - Eigen::VectorX sum = - Eigen::VectorX::Zero(Eigen::Index(mio::abm::InfectionState::Count)); - auto curr_time = sim.get_time(); - PRAGMA_OMP(for) - for (auto& location : sim.get_model().get_locations()) { + // Otherwise log accordingly + for (auto&& person : sim.get_model().get_persons()) { for (uint32_t inf_state = 0; inf_state < (int)mio::abm::InfectionState::Count; inf_state++) { - sum[inf_state] += sim.get_model().get_subpopulation(location.get_id(), curr_time, - mio::abm::InfectionState(inf_state)); + if (person.get_infection_state(curr_time) == mio::abm::InfectionState(inf_state)) { + sum[inf_state] += 1; + } } } return std::make_pair(curr_time, sum); From fe1fb5cd8a001d19a1c7a5396acd9b0fb87d61b7 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:57:53 +0100 Subject: [PATCH 106/169] CHG: Add more timers --- cpp/memilio/mobility/graph.h | 2 ++ cpp/memilio/utils/stl_util.h | 3 +++ 2 files changed, 5 insertions(+) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index d2e9d47c62..b2eddb299b 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -22,6 +22,7 @@ #include #include +#include "memilio/timer/auto_timer.h" #include "memilio/utils/stl_util.h" #include "memilio/epidemiology/age_group.h" #include "memilio/utils/date.h" @@ -170,6 +171,7 @@ class Graph template Edge& add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) { + mio::timing::AutoTimer<"Graph.add_edge()"> timer; assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); return *insert_sorted_replace(m_edges, Edge(start_node_idx, end_node_idx, std::forward(args)...), diff --git a/cpp/memilio/utils/stl_util.h b/cpp/memilio/utils/stl_util.h index f1deb46c04..88764bc662 100644 --- a/cpp/memilio/utils/stl_util.h +++ b/cpp/memilio/utils/stl_util.h @@ -21,6 +21,7 @@ #define STL_UTIL_H #include "memilio/utils/metaprogramming.h" +#include "memilio/timer/auto_timer.h" #include #include @@ -67,6 +68,7 @@ inline std::ostream& set_ostream_format(std::ostream& out, size_t width, size_t template typename std::vector::iterator insert_sorted_replace(std::vector& vec, T const& item, Pred pred) { + // mio::timing::AutoTimer<"insert_sorted_replace"> timer; auto bounds = std::equal_range(begin(vec), end(vec), item, pred); auto lb = bounds.first; auto ub = bounds.second; @@ -76,6 +78,7 @@ typename std::vector::iterator insert_sorted_replace(std::vector& vec, T c return lb; } else { + // mio::timing::AutoTimer<"insert_sorted_replace_vec_insert"> timer; return vec.insert(lb, item); } } From f23f5ff5b3f0f06134a3cbb0589e61843f84aa2d Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:03:37 +0100 Subject: [PATCH 107/169] Revert "update parameters to run" This reverts commit ea3acdea0b170f83cd0ff48f2766a18c9578f8cf. --- cpp/CMakeLists.txt | 2 +- cpp/examples/abm_parameter_study.cpp | 10 +++++----- cpp/models/abm/common_abm_loggers.h | 17 ++++++++--------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 8eb526845d..3ce345b3fd 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -18,7 +18,7 @@ option(MEMILIO_SANITIZE_ADDRESS "Enable address sanitizer." OFF) option(MEMILIO_SANITIZE_UNDEFINED "Enable undefined behavior sanitizer." OFF) option(MEMILIO_BUILD_SHARED_LIBS "Build memilio as a shared library." ON) option(MEMILIO_BUILD_STATIC_LIBS "Build memilio as a static library." ON) -option(MEMILIO_ENABLE_MPI "Build memilio with MPI." ON) +option(MEMILIO_ENABLE_MPI "Build memilio with MPI." OFF) option(MEMILIO_ENABLE_OPENMP "Enable Multithreading with OpenMP." OFF) option(MEMILIO_ENABLE_WARNINGS "Build memilio with warnings." ON) option(MEMILIO_ENABLE_WARNINGS_AS_ERRORS "Build memilio with warnings as errors." ON) diff --git a/cpp/examples/abm_parameter_study.cpp b/cpp/examples/abm_parameter_study.cpp index cf822c8265..4d609f323c 100644 --- a/cpp/examples/abm_parameter_study.cpp +++ b/cpp/examples/abm_parameter_study.cpp @@ -62,7 +62,7 @@ mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) model.parameters.check_constraints(); // There are 10 households for each household group. - int n_households = 100000; + int n_households = 10; // For more than 1 family households we need families. These are parents and children and randoms (which are distributed like the data we have for these households). auto child = mio::abm::HouseholdMember(num_age_groups); // A child is 50/50% 0-4 or 5-14. @@ -119,7 +119,7 @@ mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) auto validity_period = mio::abm::days(1); auto probability = 0.5; auto start_date = mio::abm::TimePoint(0); - auto end_date = mio::abm::TimePoint(0) + mio::abm::days(40); + auto end_date = mio::abm::TimePoint(0) + mio::abm::days(10); auto test_type = mio::abm::TestType::Antigen; auto test_parameters = model.parameters.get()[test_type]; auto testing_criteria_work = mio::abm::TestingCriteria(); @@ -159,7 +159,7 @@ mio::abm::Model make_model(const mio::RandomNumberGenerator& rng) } // During the lockdown, social events are closed for 90% of people. - auto t_lockdown = mio::abm::TimePoint(0) + mio::abm::days(40); + auto t_lockdown = mio::abm::TimePoint(0) + mio::abm::days(10); mio::abm::close_social_events(t_lockdown, 0.9, model.parameters); return model; @@ -173,9 +173,9 @@ int main() // Set start and end time for the simulation. auto t0 = mio::abm::TimePoint(0); - auto tmax = t0 + mio::abm::days(60); + auto tmax = t0 + mio::abm::days(5); // auto sim = mio::abm::Simulation(t0, std::move(model)); - const size_t num_runs = 128; + const size_t num_runs = 3; // Create a parameter study. The ABM currently does not use parameters or dt, so we set them both to 0. mio::ParameterStudy study(0, t0, tmax, mio::abm::TimeSpan(0), num_runs); diff --git a/cpp/models/abm/common_abm_loggers.h b/cpp/models/abm/common_abm_loggers.h index ca7db0f52e..0d490437f0 100644 --- a/cpp/models/abm/common_abm_loggers.h +++ b/cpp/models/abm/common_abm_loggers.h @@ -166,8 +166,7 @@ struct LogDataForMobility : mio::LogAlways { * @brief Logger to log the TimeSeries of the number of Person%s in an #InfectionState. */ struct LogInfectionState : mio::LogAlways { - using Type = std::pair; - + using Type = std::pair>; /** * @brief Log the TimeSeries of the number of Person%s in an #InfectionState. * @param[in] sim The simulation of the abm. @@ -175,15 +174,15 @@ struct LogInfectionState : mio::LogAlways { */ static Type log(const mio::abm::Simulation<>& sim) { - Eigen::VectorXd sum = Eigen::VectorXd::Zero(Eigen::Index(mio::abm::InfectionState::Count)); - auto curr_time = sim.get_time(); - // Otherwise log accordingly - for (auto&& person : sim.get_model().get_persons()) { + Eigen::VectorX sum = + Eigen::VectorX::Zero(Eigen::Index(mio::abm::InfectionState::Count)); + auto curr_time = sim.get_time(); + PRAGMA_OMP(for) + for (auto& location : sim.get_model().get_locations()) { for (uint32_t inf_state = 0; inf_state < (int)mio::abm::InfectionState::Count; inf_state++) { - if (person.get_infection_state(curr_time) == mio::abm::InfectionState(inf_state)) { - sum[inf_state] += 1; - } + sum[inf_state] += sim.get_model().get_subpopulation(location.get_id(), curr_time, + mio::abm::InfectionState(inf_state)); } } return std::make_pair(curr_time, sum); From 67f5af29b633215b184132fd4d5171fc75b64d6a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:55:18 +0100 Subject: [PATCH 108/169] CHG: add more timers --- cpp/memilio/mobility/graph.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index b2eddb299b..b0c5ec96a0 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -185,6 +185,7 @@ class Graph template Edge& lazy_add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) { + mio::timing::AutoTimer<"Graph.lazy_add_edge()"> timer; assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); for (auto& edge : m_edges) { if (edge.start_node_idx == start_node_idx && edge.end_node_idx == end_node_idx) { @@ -197,6 +198,7 @@ class Graph Edge& sort_edges() { + mio::timing::AutoTimer<"Graph.sort_edges()"> timer; std::sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) { return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx : e1.start_node_idx < e2.start_node_idx; From 70eee6bf059d877f9669b05a6e3aef6847503a74 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:05:27 +0100 Subject: [PATCH 109/169] Clean up merge --- cpp/examples/asymmetric_graph.cpp | 8 +- cpp/memilio/geography/locations.h | 120 -------- cpp/memilio/geography/tree.h | 277 ------------------ .../metapopulation_mobility_asymmetric.h | 2 +- 4 files changed, 6 insertions(+), 401 deletions(-) delete mode 100644 cpp/memilio/geography/locations.h delete mode 100644 cpp/memilio/geography/tree.h diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 22a54b3035..30a49ef4ad 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -18,8 +18,9 @@ * limitations under the License. */ #include "memilio/config.h" -#include "memilio/geography/locations.h" -#include "memilio/geography/tree.h" +#include "memilio/geography/geolocation.h" +#include "memilio/geography/rtree.h" +#include "memilio/geography/distance.h" #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" @@ -122,7 +123,8 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors(tree.inrange_indices_query(node.property.get_location(), {2.0})); + node.property.set_regional_neighbors( + tree.inrange_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); } mio::log_info("Neighbors set"); diff --git a/cpp/memilio/geography/locations.h b/cpp/memilio/geography/locations.h deleted file mode 100644 index 8cbfcd6b62..0000000000 --- a/cpp/memilio/geography/locations.h +++ /dev/null @@ -1,120 +0,0 @@ -/* -* Copyright (C) 2020-2025 MEmilio -* -* Authors: Kilian Volmer -* -* 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 LOCATIONS_H -#define LOCATIONS_H - -#include "memilio/io/default_serialize.h" -#include -#include -namespace mio -{ -namespace geo -{ - -const double pi = 3.141592653589793238462643383279502884; -const double earth_radius = 6371; -const double radians = pi / 180.0; - -class GeographicalLocation -{ - -public: - GeographicalLocation(double lat, double lon) - : latitude(lat) - , longitude(lon) - { - } - GeographicalLocation(std::pair coordinates) - : latitude(coordinates.first) - , longitude(coordinates.second) - { - } - - GeographicalLocation() = default; - - /** - * @brief Compare two GeographicalLocation%s. - */ - bool operator==(const GeographicalLocation& other) const - { - return (latitude == other.latitude && longitude == other.longitude); - } - - bool operator!=(const GeographicalLocation& other) const - { - return !(latitude == other.latitude && longitude == other.longitude); - } - - /// This method is used by the default serialization feature. - auto default_serialize() - { - return Members("GeographicalLocation").add("latitude", latitude).add("longitude", longitude); - } - - /* - * @brief Calculate the distance between two geographical locations - * @param other The other geographical location. - * @return The distance between the two locations in kilometers. - * - * Uses the haversine formula (https://en.wikipedia.org/wiki/Haversine_formula) to calculate the distance between - * two geographical locations. Uses an earth radius of 6375 km (https://en.wikipedia.org/wiki/Earth_radius). - * The math functions use radians, whereas coordinates are given in degrees. - */ - double distance(const GeographicalLocation& other) const - { - double delta_latitude = (latitude - other.latitude) * radians; - double delta_longitude = (longitude - other.longitude) * radians; - double first_part = sin(delta_latitude * 0.5) * sin(delta_latitude * 0.5); - double second_part = cos(latitude * radians) * cos(other.latitude * radians) * sin(delta_longitude * 0.5) * - sin(delta_longitude * 0.5); - double distance = 2.0 * earth_radius * asin(sqrt(first_part + second_part)); - return distance; - } - - auto get_latitude() const -> double - { - return latitude; - } - - auto get_longitude() const -> double - { - return longitude; - } - - auto set_latitude(double lat) -> void - { - latitude = lat; - } - - auto set_longitude(double lon) -> void - { - longitude = lon; - } - -private: - double latitude; - double longitude; -}; - -} // namespace geo - -} // namespace mio - -#endif // LOCATIONS_H diff --git a/cpp/memilio/geography/tree.h b/cpp/memilio/geography/tree.h deleted file mode 100644 index 3166bdb6bd..0000000000 --- a/cpp/memilio/geography/tree.h +++ /dev/null @@ -1,277 +0,0 @@ -/* -* Copyright (C) 2020-2025 MEmilio -* -* Authors: Kilian Volmer -* -* 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 TREE_H -#define TREE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bg = boost::geometry; -namespace bgi = bg::index; -namespace bgsb = bg::strategy::buffer; - -namespace mio -{ -namespace geo -{ -using Point = bg::model::point>; -typedef std::pair Node; - -template -concept IsSphericalLocation = requires(const Location& loc) { - std::is_floating_point_v && std::is_floating_point_v; -}; - -template -concept IsSphericalLocationIterator = std::input_iterator && IsSphericalLocation())>; - -template -concept IsSphericalLocationIt = std::indirectly_readable && requires(const Iter& loc) { - std::is_floating_point_vget_latitude())> && - std::is_floating_point_vget_longitude())>; -}; - -/** - * @brief R-Tree for spatial queries of geographical locations on the sphere - * - * Wraps the Boost::geometry::index::rtree. Can be initialized with a vector of geographical location data or a range. - * The provided location data needs to provide get_latitude() and get_longitude(). - */ - -class RTree -{ -public: - /** - * @brief Construct a new RTree object without data - * - */ - RTree() = default; - - /** - * @brief Construct a new RTree object with data given in a vector - * - * @param locations A vector of geographical locations, they need to provide get_latitude() and get_longitude(). - */ - template - RTree(const std::vector& locations) - : rtree{} - { - for (size_t index = 0; index < locations.size(); index++) { - Point point(locations[index].get_longitude(), locations[index].get_latitude()); - rtree.insert(Node(point, index)); - } - } - - /** - * @brief Construct a new RTree object with data given in a range - * - * @param first The beginning of the range - * @param last The end of the range - * The provided location data needs to provide get_latitude() and get_longitude(). - */ - template - RTree(Iter first, Iter last) - : rtree{} - { - size_t index = 0; - while (first != last) { - Point point(first->get_longitude(), first->get_latitude()); - rtree.insert(Node(point, index)); - ++first; - ++index; - } - } - /** - * @brief Construct a new RTree object with data given in a range - * - * @param first The beginning of the range - * @param last The end of the range - * The provided location data needs to provide get_latitude() and get_longitude(). - */ - template - RTree(Iter first, Iter last) - : rtree{} - { - size_t index = 0; - while (first != last) { - Point point((*first)->get_longitude(), (*first)->get_latitude()); - rtree.insert(Node(point, index)); - ++first; - ++index; - } - } - /** - * @brief Return the number of data points stored in the RTree - * - * @return size of the tree - */ - auto size() const - { - return rtree.size(); - } - - /** - * @brief Return the indices of the k nearest neighbours of a given location - * - * @param location Midpoint for the query, provides get_latitude() and get_longitude() - * @param number The number of nearest neighbours to find - * @return Vector with indices of the nearest neighbours - */ - auto nearest_neighbor_indices(const IsSphericalLocation auto& location, size_t number) const - { - Point point(location.get_longitude(), location.get_latitude()); - std::vector indices; - bgi::query(rtree, bgi::nearest(point, number), back_inserter_second_element>{indices}); - return indices; - } - - /** - * @brief Return the indices of the points within a given radius where the circle is approximated by a polygon - * - * @param location Midpoint for the query, provides get_latitude() and get_longitude() - * @param radius The radius of the query - * @return Vector with indices of the points found - */ - auto inrange_indices_approximate(const IsSphericalLocation auto& location, double radius) const - { - auto radius_in_meter = 1000 * radius; - auto circle = create_circle_approximation(location, radius_in_meter); - std::vector indices; - bgi::query(rtree, bgi::covered_by(circle), back_inserter_second_element>{indices}); - return indices; - } - - /** - * @brief Return the indices of the points within a given radius for multiple radii at the same time - * - * @param location Midpoint for the query, provides get_latitude() and get_longitude() - * @param radii Vector containing the radii of the query - * @return Vector of vectors with indices of the points found - */ - auto inrange_indices_query(const IsSphericalLocation auto& location, std::vector radii) const - { - auto max_radius = std::max_element(radii.begin(), radii.end()); - auto radius_in_meter = 1000 * (*max_radius); - auto circle = create_circle_approximation(location, radius_in_meter); - std::vector nodes; - bgi::query(rtree, bgi::covered_by(circle), std::back_inserter(nodes)); - auto midpoint = Point(location.get_longitude(), location.get_latitude()); - std::vector> result; - for (const auto& radius : radii) { - radius_in_meter = 1000 * radius; - std::vector indices; - for (auto& node : nodes) { - if (bg::distance(midpoint, node.first) < radius_in_meter) { - indices.push_back(node.second); - } - } - result.push_back(indices); - } - return result; - } - - /** - * @brief Return the indices of the points within a given radius - * - * @param location Midpoint for the query, provides get_latitude() and get_longitude() - * @param radius The radius of the query - * @return Vector with indices of the points found - * - * Basically the same as \ref inrange_indices_approximate, but filters the result to make sure the points are within the radius. - */ - auto inrange_indices(const IsSphericalLocation auto& location, double radius) const - { - auto radius_in_meter = 1000 * radius; - - auto circle = create_circle_approximation(location, radius_in_meter); - std::vector nodes; - bgi::query(rtree, bgi::covered_by(circle), std::back_inserter(nodes)); - auto midpoint = Point(location.get_longitude(), location.get_latitude()); - std::vector indices; - for (auto& node : nodes) { - if (bg::distance(midpoint, node.first) < radius_in_meter) { - indices.push_back(node.second); - } - } - return indices; - } - -private: - /** - * @brief Create a circle approximation object - * - * @param location Midpoint, needs to provide get_latitude() and get_longitude() - * @param radius in meters - * @return auto - */ - auto create_circle_approximation(const IsSphericalLocation auto& location, double radius) const - { - bgsb::geographic_point_circle<> point_strategy(36); - bgsb::distance_symmetric distance_strategy(radius); - bgsb::join_round join_strategy; - bgsb::end_round end_strategy; - bgsb::side_straight side_strategy; - - Point midpoint(location.get_longitude(), location.get_latitude()); - - bg::model::multi_polygon> circle; - bg::buffer(midpoint, circle, distance_strategy, side_strategy, join_strategy, end_strategy, point_strategy); - return circle; - } - - /** - * @brief Back inserter that ignores the first element of pairs given to it - */ - template - struct back_inserter_second_element { - Container& container; - back_inserter_second_element& operator*() - { - return *this; - } - back_inserter_second_element& operator++() - { - return *this; - } - back_inserter_second_element operator++(int) - { - return *this; - } - back_inserter_second_element operator=(const Node& node) - { - container.push_back(node.second); - return *this; - } - }; - bgi::rtree> rtree; -}; - -} // namespace geo -} // namespace mio - -#endif // TREE_H diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 75aa62147f..47fb0851e8 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -22,7 +22,7 @@ #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/random_number_generator.h" -#include "memilio/geography/locations.h" +#include "memilio/geography/geolocation.h" #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/graph.h" #include "memilio/mobility/metapopulation_mobility_instant.h" From 955b205191e86c29b2242426c8b590cbc2874c85 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:14:41 +0100 Subject: [PATCH 110/169] FIX: Add rtree constructor from range again --- cpp/examples/asymmetric_graph.cpp | 4 ++-- cpp/memilio/geography/rtree.h | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 30a49ef4ad..733cbe589c 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -119,12 +119,12 @@ int main(int /*argc*/, char** /*argv*/) auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); - auto tree = mio::geo::RTree({nodes.begin(), nodes.end()}); + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { node.property.set_regional_neighbors( - tree.inrange_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); + tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); } mio::log_info("Neighbors set"); diff --git a/cpp/memilio/geography/rtree.h b/cpp/memilio/geography/rtree.h index f8c022c438..730600d4b0 100644 --- a/cpp/memilio/geography/rtree.h +++ b/cpp/memilio/geography/rtree.h @@ -46,6 +46,12 @@ concept SphericalLocationType = requires(const Location& loc) { std::is_floating_point_v && std::is_floating_point_v; }; +template +concept SphericalLocationIteratorType = std::indirectly_readable && requires(const Iter& loc) { + std::is_floating_point_vget_latitude())> && + std::is_floating_point_vget_longitude())>; +}; + /** * @brief R-tree for spatial queries of geographical locations on the sphere. * @@ -79,6 +85,26 @@ class RTree } } + /** + * @brief Construct a new RTree object with data given in a range + * + * @param first The beginning of the range + * @param last The end of the range + * The provided location data needs to provide get_latitude() and get_longitude(). + */ + template + RTree(Iter first, Iter last) + : rtree{} + { + size_t index = 0; + while (first != last) { + Point point((*first)->get_longitude(), (*first)->get_latitude()); + rtree.insert(Node(point, index)); + ++first; + ++index; + } + } + /** * @brief Return the number of data points stored in the RTree. * From e85da315b748c15177485ffed1d21b15ff51ac11 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:55:08 +0100 Subject: [PATCH 111/169] CHG: Use global rng --- .../metapopulation_mobility_asymmetric.h | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h index 47fb0851e8..9a98d723f0 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h +++ b/cpp/memilio/mobility/metapopulation_mobility_asymmetric.h @@ -157,7 +157,7 @@ class MobilityEdgeDirected */ template void apply_mobility(const FP t, const FP num_moving, LocationNode& node_from, - LocationNode& node_to); + LocationNode& node_to, mio::RandomNumberGenerator& rng); private: // MobilityParametersTimed m_parameters; @@ -192,12 +192,11 @@ class MobilityEdgeDirected template template void MobilityEdgeDirected::apply_mobility(const FP t, const FP num_moving, LocationNode& node_from, - LocationNode& node_to) + LocationNode& node_to, mio::RandomNumberGenerator& rng) { // auto next_event = m_parameters.process_next_event(); // auto num_moving = next_event.number; // auto num_available = boost::numeric::ublas::sum(node_from.get_result().get_last_value()); - auto rng = mio::RandomNumberGenerator(); auto distribution = DiscreteDistributionInPlace(); std::vector travellers(node_from.get_result().get_last_value().size(), 0); if (num_moving > std::accumulate(node_from.get_result().get_last_value().begin(), @@ -217,9 +216,10 @@ void MobilityEdgeDirected::apply_mobility(const FP t, const FP num_moving, L template void apply_timed_mobility(const FP t, const FP num_moving, MobilityEdgeDirected& edge, - LocationNode& node_from, LocationNode& node_to) + LocationNode& node_from, LocationNode& node_to, + mio::RandomNumberGenerator& rng) { - edge.apply_mobility(t, num_moving, node_from, node_to); + edge.apply_mobility(t, num_moving, node_from, node_to, rng); } /** @@ -236,10 +236,11 @@ template AsymmetricGraphSimulation, MobilityEdgeDirected>> make_mobility_sim(FP t0, FP dt, const Graph, MobilityEdgeDirected>& graph) { - using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, - void (*)(FP, FP, mio::MobilityEdgeDirected&, - mio::LocationNode&, mio::LocationNode&), - void (*)(FP, FP, mio::LocationNode&)>; + using GraphSim = + AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, + void (*)(FP, FP, mio::MobilityEdgeDirected&, mio::LocationNode&, + mio::LocationNode&, mio::RandomNumberGenerator&), + void (*)(FP, FP, mio::LocationNode&)>; return GraphSim(t0, dt, graph, &advance_model, &apply_timed_mobility); } @@ -247,10 +248,11 @@ template AsymmetricGraphSimulation, MobilityEdgeDirected>> make_mobility_sim(FP t0, FP dt, Graph, MobilityEdgeDirected>&& graph) { - using GraphSim = AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, - void (*)(FP, FP, mio::MobilityEdgeDirected&, - mio::LocationNode&, mio::LocationNode&), - void (*)(FP, FP, mio::LocationNode&)>; + using GraphSim = + AsymmetricGraphSimulation, MobilityEdgeDirected>, FP, FP, + void (*)(FP, FP, mio::MobilityEdgeDirected&, mio::LocationNode&, + mio::LocationNode&, mio::RandomNumberGenerator&), + void (*)(FP, FP, mio::LocationNode&)>; return GraphSim(t0, dt, std::move(graph), &advance_model, &apply_timed_mobility); } From 032491e081da5e3fb884dd95f73388a91e9cfbb7 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:55:45 +0100 Subject: [PATCH 112/169] CHG: use global rng in edge function --- cpp/memilio/mobility/graph_simulation.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index d66549afe2..ecb2e74ae8 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -390,7 +390,7 @@ class GraphSimulationStochastic template class AsymmetricGraphSimulation : public GraphSimulationBase { @@ -427,7 +427,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase Date: Thu, 30 Oct 2025 16:56:31 +0100 Subject: [PATCH 113/169] CHG: use global rng --- cpp/memilio/mobility/graph_simulation.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index ecb2e74ae8..d4cd118960 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -594,10 +594,15 @@ class AsymmetricGraphSimulation : public GraphSimulationBase // } // I don't know any smart way to access and modify the adoption rates here, I would need to know the template parameters. // } + + RandomNumberGenerator& get_rng() + { + return m_rng; } private: mio::MobilityParametersTimed m_parameters; + RandomNumberGenerator m_rng; }; template From d33e36f26d69f8c8838867f7a647ad326f1351b7 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:57:48 +0100 Subject: [PATCH 114/169] NEW: file for real data experiments --- cpp/examples/CMakeLists.txt | 4 + cpp/examples/asym_ex_data.cpp | 191 ++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 cpp/examples/asym_ex_data.cpp diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 8bc387ef82..7b618d1f81 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -108,6 +108,10 @@ add_executable(asymmetric_graph_example asymmetric_graph.cpp) target_link_libraries(asymmetric_graph_example PRIVATE memilio smm) target_compile_options(asymmetric_graph_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +add_executable(asymmetric_graph_data asym_ex_data.cpp) +target_link_libraries(asymmetric_graph_data PRIVATE memilio smm) +target_compile_options(asymmetric_graph_data PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) + add_executable(abm_minimal_example abm_minimal.cpp) target_link_libraries(abm_minimal_example PRIVATE memilio abm) target_compile_options(abm_minimal_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) diff --git a/cpp/examples/asym_ex_data.cpp b/cpp/examples/asym_ex_data.cpp new file mode 100644 index 0000000000..6325113da9 --- /dev/null +++ b/cpp/examples/asym_ex_data.cpp @@ -0,0 +1,191 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Daniel Abele +* +* 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 "memilio/geography/geolocation.h" +#include "memilio/geography/rtree.h" +#include "memilio/geography/distance.h" +#include "memilio/mobility/graph_simulation.h" +#include "memilio/mobility/metapopulation_mobility_asymmetric.h" +#include "memilio/mobility/graph.h" +#include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/logging.h" +#include "memilio/timer/auto_timer.h" +#include "memilio/utils/parameter_distributions.h" +#include "memilio/utils/random_number_generator.h" +#include "smm/simulation.h" +#include "smm/parameters.h" +#include "thirdparty/csv.h" +#include +#include + +enum class InfectionState +{ + S, + E, + I, + INS, + ICS, + R, + D, + Count +}; + +int main(int /*argc*/, char** /*argv*/) +{ + const auto t0 = 0.; + const auto tmax = 100.; + const auto dt = 1.; //initial time step + + //total compartment sizes + + using Model = mio::smm::Model; + auto home = mio::regions::Region(0); + auto S = InfectionState::S; + auto E = InfectionState::E; + auto I = InfectionState::I; + auto INS = InfectionState::INS; + auto ICS = InfectionState::ICS; + auto R = InfectionState::R; + auto D = InfectionState::D; + + std::vector> adoption_rates; + // Adoption rates corresponding to our model, paramters are arbitrary + adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); + adoption_rates.push_back({E, I, home, 0.2, {}}); + adoption_rates.push_back({I, INS, home, 0.1, {}}); + adoption_rates.push_back({I, ICS, home, 0.1, {}}); + adoption_rates.push_back({ICS, D, home, 0.6, {}}); + adoption_rates.push_back({ICS, R, home, 0.4, {}}); + adoption_rates.push_back({INS, R, home, 0.5, {}}); + + mio::Graph>, + mio::MobilityEdgeDirected> + graph; + // mio::log_info("Starting Graph generation"); + { + mio::timing::AutoTimer<"Graph Nodes Generation"> timer; + io::CSVReader<7> farms("../../../../data/trade_network/example_data_NRW/Data/Daten_RH_DE_coords_anon_NW.csv"); + farms.read_header(io::ignore_extra_column, "num_id", "id", "farm_type", "farm_size", "BL", "x", "y"); + int farm_id, num_cows, farm_type, bl, num_id; + double latitude, longitude; + while (farms.read_row(num_id, farm_id, farm_type, num_cows, bl, latitude, longitude)) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph.add_node(farm_id, longitude, latitude, curr_model, t0); + } + } + // mio::log_info("Nodes added to Graph"); + auto rng = mio::RandomNumberGenerator(); + + std::vector> interesting_indices; + interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + // graph.reserve_edges(262144); + { + mio::timing::AutoTimer<"Graph Edges Generation"> timer; + io::CSVReader<2> edges("../../../../data/trade_network/example_data_NRW/Datenedges.csv"); + edges.read_header(io::ignore_extra_column, "from", "to"); + size_t from, to; + while (edges.read_row(from, to)) { + // graph.add_edge(from, to, interesting_indices); + graph.lazy_add_edge(from, to, interesting_indices); + } + graph.sort_edges(); + } + // mio::log_info("Graph generated"); + + auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + // mio::log_info("RTree generated"); + + for (auto& node : graph.nodes()) { + node.property.set_regional_neighbors( + tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); + } + + // mio::log_info("Neighbors set"); + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); + + io::CSVReader<4> exchanges("../../../../data/trade_network/example_data_NRW/Data/exchanges.csv"); + exchanges.read_header(io::ignore_extra_column, "from", "to", "day", "weight"); + + int date, num_animals; + size_t from, to; + while (exchanges.read_row(from, to, date, num_animals)) { + sim.add_exchange(date, num_animals, from, to); + } + // mio::log_info("Exchanges added"); + + // #ifdef MEMILIO_ENABLE_OPENMP + // #pragma omp parallel for + // for (size_t i = 0; i < 100; ++i) { + // #endif + + auto sim2(sim); + // mio::log_info("new Simulation created"); + + sim2.advance(tmax); + // mio::log_info("Simulation finished"); + + // #ifdef MEMILIO_ENABLE_OPENMP + // } + // #endif + + // sim2.get_graph().nodes()[28].property.get_result().print_table({"S", "E", "I", "R", "D"}); + // std::cout << "Second Table" << std::endl; + // sim2.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R", "D"}); + + // auto& edge_1_0 = sim2.get_graph().edges()[1]; + // auto& results = edge_1_0.property.get_mobility_results(); + // results.print_table({"Commuter Sick", "Commuter Total"}); + + // auto exchange_results = sim2.sum_exchanges(); + // // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); + // // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); + + auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); + + // for (auto node : sim2.get_graph().nodes()) { + // if (node.property.get_result().get_num_time_points() < num_time_points) { + // mio::log_error("Node with inconsistent number of time points in results."); + // } + // } + + // exchange_results = sim2.sum_nodes(); + // // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); + + sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); + // // auto combined_results = sim2.combine_node_results(); + // // combined_results.print_table({"S", "E", "I", "R", "D"}); + // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); + + // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); + // mio::log_info("Finished postprocessing"); + + return 0; +} From 21f06c65d1cbb4177d436ff42084b7d7046c6b9a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:58:17 +0100 Subject: [PATCH 115/169] CHG: Access parameters of the models --- cpp/memilio/mobility/graph_simulation.h | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index d4cd118960..79693da1ec 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -28,6 +28,7 @@ #include #include "memilio/compartments/feedback_simulation.h" #include "memilio/geography/regions.h" +#include "models/smm/parameters.h" namespace mio { @@ -589,11 +590,17 @@ class AsymmetricGraphSimulation : public GraphSimulationBase 40) { - // n.property.get_model().get_parameters().get < mio::smm::AdoptionRates - // } // I don't know any smart way to access and modify the adoption rates here, I would need to know the template parameters. - // } + for (auto& n : Base::m_graph.nodes()) { + // using Model = mio::smm::Model; + using Model = std::decay_t; + using AdoptionRate = mio::smm::AdoptionRates; + // n.property.get_simulation().get_model().parameters.template get()[0].factor = 0.5; + mio::unused(n.property.get_simulation().get_model().parameters.template get()); + // if (n.property.get_result().get_last_value()[2] > 40) { + // n.property.get_model().get_parameters().get < mio::smm::AdoptionRates + // } // I don't know any smart way to access and modify the adoption rates here, I would need to know the template parameters. + } + } RandomNumberGenerator& get_rng() { From e69f0802d3fa291743673d8b8146c67e0e91cfb5 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:51:51 +0100 Subject: [PATCH 116/169] CHG: Add MPI parameter study --- cpp/examples/CMakeLists.txt | 4 + cpp/examples/asym_ex_data.cpp | 2 +- cpp/examples/asymmetric_graph.cpp | 29 ++-- cpp/examples/asymmetric_params.cpp | 256 +++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+), 15 deletions(-) create mode 100644 cpp/examples/asymmetric_params.cpp diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 7ccc4aa6a6..7067358726 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -112,6 +112,10 @@ add_executable(asymmetric_graph_data asym_ex_data.cpp) target_link_libraries(asymmetric_graph_data PRIVATE memilio smm) target_compile_options(asymmetric_graph_data PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +add_executable(asymmetric_params asymmetric_params.cpp) +target_link_libraries(asymmetric_params PRIVATE memilio smm) +target_compile_options(asymmetric_params PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) + add_executable(abm_minimal_example abm_minimal.cpp) target_link_libraries(abm_minimal_example PRIVATE memilio abm) target_compile_options(abm_minimal_example PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) diff --git a/cpp/examples/asym_ex_data.cpp b/cpp/examples/asym_ex_data.cpp index 6325113da9..0f83d9202d 100644 --- a/cpp/examples/asym_ex_data.cpp +++ b/cpp/examples/asym_ex_data.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2025 MEmilio * -* Authors: Daniel Abele +* Authors: Kilian Volmer * * Contact: Martin J. Kuehn * diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 733cbe589c..abfa4a381b 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2025 MEmilio * -* Authors: Daniel Abele +* Authors: Kilian Volmer * * Contact: Martin J. Kuehn * @@ -81,7 +81,7 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; - io::CSVReader<4> farms("../../farms.csv"); + io::CSVReader<4> farms("../../farms10000.csv"); farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); int farm_id, num_cows; double latitude, longitude; @@ -103,9 +103,10 @@ int main(int /*argc*/, char** /*argv*/) std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + // graph.reserve_edges(262144); { mio::timing::AutoTimer<"Graph Edges Generation"> timer; - io::CSVReader<2> edges("../../edges.csv"); + io::CSVReader<2> edges("../../edges10000.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { @@ -114,23 +115,23 @@ int main(int /*argc*/, char** /*argv*/) } // graph.sort_edges(); } - mio::log_info("Graph generated"); + // mio::log_info("Graph generated"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); - mio::log_info("RTree generated"); + // mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { node.property.set_regional_neighbors( tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); } - mio::log_info("Neighbors set"); + // mio::log_info("Neighbors set"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - io::CSVReader<5> exchanges("../../trade.csv"); + io::CSVReader<5> exchanges("../../trade10000.csv"); exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); int date, num_animals, edge; @@ -138,7 +139,7 @@ int main(int /*argc*/, char** /*argv*/) while (exchanges.read_row(date, num_animals, from, to, edge)) { sim.add_exchange(date, num_animals, from, to); } - mio::log_info("Exchanges added"); + // mio::log_info("Exchanges added"); // #ifdef MEMILIO_ENABLE_OPENMP // #pragma omp parallel for @@ -146,10 +147,10 @@ int main(int /*argc*/, char** /*argv*/) // #endif auto sim2(sim); - mio::log_info("new Simulation created"); + // mio::log_info("new Simulation created"); sim2.advance(tmax); - mio::log_info("Simulation finished"); + // mio::log_info("Simulation finished"); // #ifdef MEMILIO_ENABLE_OPENMP // } @@ -164,8 +165,8 @@ int main(int /*argc*/, char** /*argv*/) // results.print_table({"Commuter Sick", "Commuter Total"}); // auto exchange_results = sim2.sum_exchanges(); - // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); - // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); + // // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); + // // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); @@ -176,7 +177,7 @@ int main(int /*argc*/, char** /*argv*/) // } // exchange_results = sim2.sum_nodes(); - // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); + // // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); // // auto combined_results = sim2.combine_node_results(); @@ -184,7 +185,7 @@ int main(int /*argc*/, char** /*argv*/) // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); - mio::log_info("Finished postprocessing"); + // mio::log_info("Finished postprocessing"); return 0; } diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp new file mode 100644 index 0000000000..7172fb9e01 --- /dev/null +++ b/cpp/examples/asymmetric_params.cpp @@ -0,0 +1,256 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 "memilio/geography/geolocation.h" +#include "memilio/compartments/parameter_studies.h" +#include "memilio/geography/rtree.h" +#include "memilio/geography/distance.h" +#include "memilio/mobility/graph_simulation.h" +#include "memilio/mobility/metapopulation_mobility_asymmetric.h" +#include "memilio/mobility/graph.h" +#include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/custom_index_array.h" +#include "memilio/utils/logging.h" +#include "memilio/timer/auto_timer.h" +#include "memilio/utils/parameter_distributions.h" +#include "memilio/utils/random_number_generator.h" +#include "memilio/utils/miompi.h" +#include "memilio/utils/base_dir.h" +#include "smm/simulation.h" +#include "smm/parameters.h" +#include "abm/time.h" +#include "thirdparty/csv.h" +#include +#include + +enum class InfectionState +{ + S, + E, + I, + INS, + ICS, + R, + D, + Count +}; + +template +MPI_Datatype mpi_type(); + +template <> +inline MPI_Datatype mpi_type() +{ + return MPI_INT; +} +template <> +inline MPI_Datatype mpi_type() +{ + return MPI_DOUBLE; +} +template <> +inline MPI_Datatype mpi_type() +{ + return MPI_FLOAT; +} +template <> +inline MPI_Datatype mpi_type() +{ + return MPI_LONG_LONG; +} +// If your MPI supports it (MPI-3+), for fixed-width: +#include +template <> +inline MPI_Datatype mpi_type() +{ + return MPI_INT64_T; +} +template <> +inline MPI_Datatype mpi_type() +{ + return MPI_UINT64_T; +} + +template +void bcast_vector(std::vector& v, int root, MPI_Comm comm) +{ + int n = static_cast(v.size()); + MPI_Bcast(&n, 1, MPI_INT, root, comm); + if (n < 0) + std::abort(); + if (n == 0) { + v.clear(); + return; + } + if (v.size() != static_cast(n)) + v.resize(n); + MPI_Bcast(v.data(), n, mpi_type(), root, comm); +} + +int main(int /*argc*/, char** /*argv*/) +{ + mio::mpi::init(); + int size, rank; + MPI_Comm_size(MPI_COMM_WORLD, &size); + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + const std::string result_dir = mio::path_join(mio::base_dir(), "example_results"); + + const auto t0 = 0.; + const auto tmax = 100.; + const auto dt = 1.; //initial time step + + //total compartment sizes + + using Model = mio::smm::Model; + auto home = mio::regions::Region(0); + auto S = InfectionState::S; + auto E = InfectionState::E; + auto I = InfectionState::I; + auto INS = InfectionState::INS; + auto ICS = InfectionState::ICS; + auto R = InfectionState::R; + auto D = InfectionState::D; + + std::vector> adoption_rates; + // Adoption rates corresponding to our model, paramters are arbitrary + adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); + adoption_rates.push_back({E, I, home, 0.2, {}}); + adoption_rates.push_back({I, INS, home, 0.1, {}}); + adoption_rates.push_back({I, ICS, home, 0.1, {}}); + adoption_rates.push_back({ICS, D, home, 0.6, {}}); + adoption_rates.push_back({ICS, R, home, 0.4, {}}); + adoption_rates.push_back({INS, R, home, 0.5, {}}); + + mio::Graph>, + mio::MobilityEdgeDirected> + graph; + + // usage: + // if (rank==0) v = {1,2,3,4}; + // bcast_vector(v, 0, MPI_COMM_WORLD); + // now every rank has v + std::vector latitudes, longitudes; + std::vector farm_ids, num_cows_vec, dates, num_animals_exchanges; + std::vector froms, tos, from_exchanges, to_exchanges; + if (rank == 0) { + io::CSVReader<4> farms("../../farms10000.csv"); + farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); + int farm_id, num_cows; + double latitude, longitude; + while (farms.read_row(farm_id, num_cows, latitude, longitude)) { + farm_ids.push_back(farm_id); + num_cows_vec.push_back(num_cows); + latitudes.push_back(latitude); + longitudes.push_back(longitude); + } + io::CSVReader<2> edges("../../edges10000.csv"); + edges.read_header(io::ignore_extra_column, "from", "to"); + size_t from, to; + while (edges.read_row(from, to)) { + froms.push_back(from); + tos.push_back(to); + } + io::CSVReader<4> exchanges("../../trade10000.csv"); + exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to"); + + int date, num_animals; + while (exchanges.read_row(date, num_animals, from, to)) { + dates.push_back(date); + num_animals_exchanges.push_back(num_animals); + from_exchanges.push_back(from); + to_exchanges.push_back(to); + } + if (!mio::create_directory(result_dir)) { + mio::log_error("Could not create result directory \"{}\".", result_dir); + return 1; + } + } + bcast_vector(farm_ids, 0, MPI_COMM_WORLD); + bcast_vector(num_cows_vec, 0, MPI_COMM_WORLD); + bcast_vector(latitudes, 0, MPI_COMM_WORLD); + bcast_vector(longitudes, 0, MPI_COMM_WORLD); + bcast_vector(froms, 0, MPI_COMM_WORLD); + bcast_vector(tos, 0, MPI_COMM_WORLD); + bcast_vector(dates, 0, MPI_COMM_WORLD); + bcast_vector(num_animals_exchanges, 0, MPI_COMM_WORLD); + bcast_vector(from_exchanges, 0, MPI_COMM_WORLD); + bcast_vector(to_exchanges, 0, MPI_COMM_WORLD); + + for (size_t i = 0; i < farm_ids.size(); ++i) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + } + auto rng = mio::RandomNumberGenerator(); + + std::vector> interesting_indices; + interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + + for (size_t i = 0; i < froms.size(); ++i) { + graph.add_edge(froms[i], tos[i], interesting_indices); + } + + auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + + for (auto& node : graph.nodes()) { + node.property.set_regional_neighbors( + tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); + } + + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); + + for (size_t i = 0; i < dates.size(); ++i) { + sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); + } + + const size_t num_runs = 400; + + mio::ParameterStudy study(sim, t0, tmax, dt, num_runs); + + auto get_simulation = [](const auto& sim, ScalarType, ScalarType, size_t run_id) { + auto sim2 = sim; + sim2.get_rng() = mio::thread_local_rng(); + if (run_id % 2 == 0) { + auto index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( + {mio::regions::Region(0), InfectionState::E}); + sim2.get_graph().nodes()[0].property.get_result().get_last_value()[index] = 100; + } + return sim2; + }; + auto handle_result = [](auto&& sim, auto&& run) { + auto abs_path = mio::path_join(mio::base_dir(), "example_results"); + auto result = sim.statistics_per_timestep().export_csv( + mio::path_join(abs_path, "AsymmetricParams_run" + std::to_string(run) + ".csv")); + return 0; + }; + auto result = study.run(get_simulation, handle_result); + mio::mpi::finalize(); + return 0; +} From 487282926d1a51b0b8255dc9534cfc11bbcdcb11 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:20:42 +0100 Subject: [PATCH 117/169] CHG: Add local infectivity --- cpp/examples/asymmetric_graph.cpp | 9 ++++--- cpp/examples/asymmetric_params.cpp | 10 +++++--- cpp/memilio/mobility/graph_simulation.h | 33 ++++++++++++++++--------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index abfa4a381b..7fb34e15ca 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -68,6 +68,7 @@ int main(int /*argc*/, char** /*argv*/) std::vector> adoption_rates; // Adoption rates corresponding to our model, paramters are arbitrary adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); + adoption_rates.push_back({S, E, home, 0.0, {}}); adoption_rates.push_back({E, I, home, 0.2, {}}); adoption_rates.push_back({I, INS, home, 0.1, {}}); adoption_rates.push_back({I, ICS, home, 0.1, {}}); @@ -131,12 +132,12 @@ int main(int /*argc*/, char** /*argv*/) // mio::log_info("Neighbors set"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - io::CSVReader<5> exchanges("../../trade10000.csv"); - exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to", "edge"); + io::CSVReader<4> exchanges("../../trade10000.csv"); + exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to"); - int date, num_animals, edge; + int date, num_animals; size_t from, to; - while (exchanges.read_row(date, num_animals, from, to, edge)) { + while (exchanges.read_row(date, num_animals, from, to)) { sim.add_exchange(date, num_animals, from, to); } // mio::log_info("Exchanges added"); diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 7172fb9e01..389c5e3428 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -131,6 +131,7 @@ int main(int /*argc*/, char** /*argv*/) std::vector> adoption_rates; // Adoption rates corresponding to our model, paramters are arbitrary adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); + adoption_rates.push_back({S, E, home, 0.0, {}}); adoption_rates.push_back({E, I, home, 0.2, {}}); adoption_rates.push_back({I, INS, home, 0.1, {}}); adoption_rates.push_back({I, ICS, home, 0.1, {}}); @@ -230,7 +231,7 @@ int main(int /*argc*/, char** /*argv*/) sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); } - const size_t num_runs = 400; + const size_t num_runs = 4000; mio::ParameterStudy study(sim, t0, tmax, dt, num_runs); @@ -238,10 +239,11 @@ int main(int /*argc*/, char** /*argv*/) auto sim2 = sim; sim2.get_rng() = mio::thread_local_rng(); if (run_id % 2 == 0) { - auto index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( - {mio::regions::Region(0), InfectionState::E}); - sim2.get_graph().nodes()[0].property.get_result().get_last_value()[index] = 100; + sim2.infectionrisk = 0.1; } + auto index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( + {mio::regions::Region(0), InfectionState::E}); + sim2.get_graph().nodes()[0].property.get_result().get_last_value()[index] = 10; return sim2; }; auto handle_result = [](auto&& sim, auto&& run) { diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index af42a35b64..1bbc066a81 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -414,8 +414,6 @@ class AsymmetricGraphSimulation : public GraphSimulationBase; - using Model = std::decay_t; - using AdoptionRate = mio::smm::AdoptionRates; - // n.property.get_simulation().get_model().parameters.template get()[0].factor = 0.5; - mio::unused(n.property.get_simulation().get_model().parameters.template get()); - // if (n.property.get_result().get_last_value()[2] > 40) { - // n.property.get_model().get_parameters().get < mio::smm::AdoptionRates - // } // I don't know any smart way to access and modify the adoption rates here, I would need to know the template parameters. + auto total_infections = 0; + for (auto comp : infectious_compartments) { + total_infections += n.property.get_result().get_last_value()[comp]; + } + if (total_infections > 1) { + mio::log_debug("Node {} spreads higher risk category at time {} because there are {} total infections.", + n.id, Base::m_t, total_infections); + mio::log_debug("This will affect {} other farms.", n.property.get_regional_neighbors()[0].size()); + for (size_t index = 0; index < n.property.get_regional_neighbors()[0].size(); ++index) { + const auto node_id = n.property.get_regional_neighbors()[0][index]; + mio::log_debug("Neighbor node id: {}", node_id); + auto neighbour_property = Base::m_graph.nodes()[node_id].property; + using Model = std::decay_t; + using AdoptionRate = mio::smm::AdoptionRates; + neighbour_property.get_simulation().get_model().parameters.template get()[1].factor += + infectionrisk; + } + } } } @@ -609,6 +618,8 @@ class AsymmetricGraphSimulation : public GraphSimulationBase Date: Mon, 3 Nov 2025 13:25:10 +0100 Subject: [PATCH 118/169] Update data reader --- cpp/examples/asym_ex_data.cpp | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/cpp/examples/asym_ex_data.cpp b/cpp/examples/asym_ex_data.cpp index 0f83d9202d..8c53e35845 100644 --- a/cpp/examples/asym_ex_data.cpp +++ b/cpp/examples/asym_ex_data.cpp @@ -50,7 +50,7 @@ enum class InfectionState int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; - const auto tmax = 100.; + const auto tmax = 400.; const auto dt = 1.; //initial time step //total compartment sizes @@ -78,14 +78,14 @@ int main(int /*argc*/, char** /*argv*/) mio::Graph>, mio::MobilityEdgeDirected> graph; - // mio::log_info("Starting Graph generation"); + mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; - io::CSVReader<7> farms("../../../../data/trade_network/example_data_NRW/Data/Daten_RH_DE_coords_anon_NW.csv"); - farms.read_header(io::ignore_extra_column, "num_id", "id", "farm_type", "farm_size", "BL", "x", "y"); - int farm_id, num_cows, farm_type, bl, num_id; + io::CSVReader<4> farms("../../../../data/trade_network/example_data_NRW/Data/farms.csv"); + farms.read_header(io::ignore_extra_column, "id_dec", "x", "y", "farm_size"); + size_t farm_id, num_cows; double latitude, longitude; - while (farms.read_row(num_id, farm_id, farm_type, num_cows, bl, latitude, longitude)) { + while (farms.read_row(farm_id, latitude, longitude, num_cows)) { Model curr_model; curr_model.populations[{home, InfectionState::S}] = num_cows; curr_model.populations[{home, InfectionState::E}] = 0; @@ -98,7 +98,7 @@ int main(int /*argc*/, char** /*argv*/) graph.add_node(farm_id, longitude, latitude, curr_model, t0); } } - // mio::log_info("Nodes added to Graph"); + mio::log_info("Nodes added to Graph"); auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; @@ -106,7 +106,7 @@ int main(int /*argc*/, char** /*argv*/) // graph.reserve_edges(262144); { mio::timing::AutoTimer<"Graph Edges Generation"> timer; - io::CSVReader<2> edges("../../../../data/trade_network/example_data_NRW/Datenedges.csv"); + io::CSVReader<2> edges("../../../../data/trade_network/example_data_NRW/Data/edges.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { @@ -115,31 +115,31 @@ int main(int /*argc*/, char** /*argv*/) } graph.sort_edges(); } - // mio::log_info("Graph generated"); + mio::log_info("Graph generated"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); - // mio::log_info("RTree generated"); + mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { node.property.set_regional_neighbors( tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); } - // mio::log_info("Neighbors set"); + mio::log_info("Neighbors set"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); io::CSVReader<4> exchanges("../../../../data/trade_network/example_data_NRW/Data/exchanges.csv"); - exchanges.read_header(io::ignore_extra_column, "from", "to", "day", "weight"); + exchanges.read_header(io::ignore_extra_column, "from_dec", "to_dec", "day", "weight"); - int date, num_animals; + size_t date, num_animals; size_t from, to; while (exchanges.read_row(from, to, date, num_animals)) { sim.add_exchange(date, num_animals, from, to); } - // mio::log_info("Exchanges added"); + mio::log_info("Exchanges added"); // #ifdef MEMILIO_ENABLE_OPENMP // #pragma omp parallel for @@ -147,10 +147,10 @@ int main(int /*argc*/, char** /*argv*/) // #endif auto sim2(sim); - // mio::log_info("new Simulation created"); + mio::log_info("new Simulation created"); sim2.advance(tmax); - // mio::log_info("Simulation finished"); + mio::log_info("Simulation finished"); // #ifdef MEMILIO_ENABLE_OPENMP // } @@ -165,8 +165,8 @@ int main(int /*argc*/, char** /*argv*/) // results.print_table({"Commuter Sick", "Commuter Total"}); // auto exchange_results = sim2.sum_exchanges(); - // // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); - // // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); + // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); + // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); @@ -177,7 +177,7 @@ int main(int /*argc*/, char** /*argv*/) // } // exchange_results = sim2.sum_nodes(); - // // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); + // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); // // auto combined_results = sim2.combine_node_results(); @@ -185,7 +185,7 @@ int main(int /*argc*/, char** /*argv*/) // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); - // mio::log_info("Finished postprocessing"); + mio::log_info("Finished postprocessing"); return 0; } From 06a67b9230cc2d23d71784d47c8fbe7fe7451d81 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:14:44 +0100 Subject: [PATCH 119/169] CHG: Add timers --- cpp/memilio/utils/stl_util.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/memilio/utils/stl_util.h b/cpp/memilio/utils/stl_util.h index 88764bc662..1777c9a434 100644 --- a/cpp/memilio/utils/stl_util.h +++ b/cpp/memilio/utils/stl_util.h @@ -68,7 +68,7 @@ inline std::ostream& set_ostream_format(std::ostream& out, size_t width, size_t template typename std::vector::iterator insert_sorted_replace(std::vector& vec, T const& item, Pred pred) { - // mio::timing::AutoTimer<"insert_sorted_replace"> timer; + mio::timing::AutoTimer<"insert_sorted_replace"> timer; auto bounds = std::equal_range(begin(vec), end(vec), item, pred); auto lb = bounds.first; auto ub = bounds.second; @@ -78,7 +78,7 @@ typename std::vector::iterator insert_sorted_replace(std::vector& vec, T c return lb; } else { - // mio::timing::AutoTimer<"insert_sorted_replace_vec_insert"> timer; + mio::timing::AutoTimer<"insert_sorted_replace_vec_insert"> timer; return vec.insert(lb, item); } } From a8f94dd155d35c24b861e6de2b7d6b244d3e55ca Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:29:22 +0100 Subject: [PATCH 120/169] NEW: Benchmarking file --- cpp/examples/CMakeLists.txt | 4 + cpp/examples/asym_graph_ben.cpp | 177 ++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 cpp/examples/asym_graph_ben.cpp diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 7067358726..f1a678ef6b 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -112,6 +112,10 @@ add_executable(asymmetric_graph_data asym_ex_data.cpp) target_link_libraries(asymmetric_graph_data PRIVATE memilio smm) target_compile_options(asymmetric_graph_data PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) +add_executable(asym_ben asym_graph_ben.cpp) +target_link_libraries(asym_ben PRIVATE memilio smm) +target_compile_options(asym_ben PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) + add_executable(asymmetric_params asymmetric_params.cpp) target_link_libraries(asymmetric_params PRIVATE memilio smm) target_compile_options(asymmetric_params PRIVATE ${MEMILIO_CXX_FLAGS_ENABLE_WARNING_ERRORS}) diff --git a/cpp/examples/asym_graph_ben.cpp b/cpp/examples/asym_graph_ben.cpp new file mode 100644 index 0000000000..1559cf440a --- /dev/null +++ b/cpp/examples/asym_graph_ben.cpp @@ -0,0 +1,177 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 "memilio/geography/geolocation.h" +#include "memilio/geography/rtree.h" +#include "memilio/geography/distance.h" +#include "memilio/mobility/graph_simulation.h" +#include "memilio/mobility/metapopulation_mobility_asymmetric.h" +#include "memilio/mobility/graph.h" +#include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/logging.h" +#include "memilio/timer/auto_timer.h" +#include "memilio/utils/parameter_distributions.h" +#include "memilio/utils/random_number_generator.h" +#include "smm/simulation.h" +#include "smm/parameters.h" +#include "thirdparty/csv.h" +#include +#include + +enum class InfectionState +{ + S, + E, + I, + INS, + ICS, + R, + D, + Count +}; + +int main(int /*argc*/, char** /*argv*/) +{ + const auto t0 = 0.; + // const auto tmax = 100.; + const auto dt = 1.; //initial time step + + //total compartment sizes + + using Model = mio::smm::Model; + auto home = mio::regions::Region(0); + auto S = InfectionState::S; + auto E = InfectionState::E; + auto I = InfectionState::I; + auto INS = InfectionState::INS; + auto ICS = InfectionState::ICS; + auto R = InfectionState::R; + auto D = InfectionState::D; + + std::vector> adoption_rates; + // Adoption rates corresponding to our model, paramters are arbitrary + adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); + adoption_rates.push_back({S, E, home, 0.0, {}}); + adoption_rates.push_back({E, I, home, 0.2, {}}); + adoption_rates.push_back({I, INS, home, 0.1, {}}); + adoption_rates.push_back({I, ICS, home, 0.1, {}}); + adoption_rates.push_back({ICS, D, home, 0.6, {}}); + adoption_rates.push_back({ICS, R, home, 0.4, {}}); + adoption_rates.push_back({INS, R, home, 0.5, {}}); + + mio::Graph>, + mio::MobilityEdgeDirected> + graph; + mio::log_info("Reading CSVs"); + std::vector latitudes, longitudes; + std::vector farm_ids, num_cows_vec, dates, num_animals_exchanges; + std::vector froms, tos, from_exchanges, to_exchanges; + io::CSVReader<4> farms("../../farms200000.csv"); + farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); + int farm_id, num_cows; + double latitude, longitude; + while (farms.read_row(farm_id, num_cows, latitude, longitude)) { + farm_ids.push_back(farm_id); + num_cows_vec.push_back(num_cows); + latitudes.push_back(latitude); + longitudes.push_back(longitude); + } + io::CSVReader<2> edges("../../edges200000.csv"); + edges.read_header(io::ignore_extra_column, "from", "to"); + size_t from, to; + while (edges.read_row(from, to)) { + froms.push_back(from); + tos.push_back(to); + } + io::CSVReader<4> exchanges("../../trade200000.csv"); + exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to"); + + int date, num_animals; + while (exchanges.read_row(date, num_animals, from, to)) { + dates.push_back(date); + num_animals_exchanges.push_back(num_animals); + from_exchanges.push_back(from); + to_exchanges.push_back(to); + } + for (size_t i = 0; i < farm_ids.size(); ++i) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + } + auto rng = mio::RandomNumberGenerator(); + + std::vector> interesting_indices; + interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + + for (size_t i = 0; i < froms.size(); ++i) { + mio::timing::AutoTimer<"Graph Edges Generation"> timer; + graph.add_edge(froms[i], tos[i], interesting_indices); + } + + mio::log_info("First graph construction finished"); + + mio::Graph>, + mio::MobilityEdgeDirected> + graph2; + for (size_t i = 0; i < farm_ids.size(); ++i) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph2.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + } + for (size_t i = 0; i < froms.size(); ++i) { + mio::timing::AutoTimer<"Graph Edges lazy Generation"> timer; + graph2.lazy_add_edge(froms[i], tos[i], interesting_indices); + } + graph2.sort_edges(); + mio::log_info("Second graph construction finished"); + + auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + + for (auto& node : graph.nodes()) { + node.property.set_regional_neighbors( + tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); + } + + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); + + for (size_t i = 0; i < dates.size(); ++i) { + sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); + } + + mio::log_info("Starting and immediately finishing simulation"); + return 0; +} From 3715084c8270eecbb39e10a81647e953315fda13 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:18:01 +0100 Subject: [PATCH 121/169] CHG: Add make_edges_unique --- cpp/examples/asym_graph_ben.cpp | 1 + cpp/memilio/mobility/graph.h | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/cpp/examples/asym_graph_ben.cpp b/cpp/examples/asym_graph_ben.cpp index 1559cf440a..cf425bdb9f 100644 --- a/cpp/examples/asym_graph_ben.cpp +++ b/cpp/examples/asym_graph_ben.cpp @@ -154,6 +154,7 @@ int main(int /*argc*/, char** /*argv*/) graph2.lazy_add_edge(froms[i], tos[i], interesting_indices); } graph2.sort_edges(); + graph2.make_edges_unique(); mio::log_info("Second graph construction finished"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index b0c5ec96a0..1f5abd2e49 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -206,6 +206,24 @@ class Graph return m_edges.back(); } + Edge& make_edges_unique() + { + mio::timing::AutoTimer<"Graph.make_edges_unique()"> timer; + std::vector> unique_edges; + unique_edges.reserve(m_edges.size()); + auto start_node = m_edges.front().start_node_idx; + auto end_node = m_edges.front().end_node_idx; + for (const auto& edge : m_edges) { + if (edge.start_node_idx != start_node || edge.end_node_idx != end_node) { + unique_edges.push_back(edge); + start_node = edge.start_node_idx; + end_node = edge.end_node_idx; + } + } + m_edges = std::move(unique_edges); + return m_edges.back(); + } + /** * @brief range of nodes */ From b8566ee71de038a9efedda3db58f3829eedbf88d Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:20:10 +0100 Subject: [PATCH 122/169] CHG: Don't check edges uniqueness in lazy_add_edge --- cpp/memilio/mobility/graph.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 1f5abd2e49..6a510ce9a7 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -187,11 +187,6 @@ class Graph { mio::timing::AutoTimer<"Graph.lazy_add_edge()"> timer; assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); - for (auto& edge : m_edges) { - if (edge.start_node_idx == start_node_idx && edge.end_node_idx == end_node_idx) { - return m_edges.back(); - } - } m_edges.emplace_back(start_node_idx, end_node_idx, std::forward(args)...); return m_edges.back(); } From 317ecf275eafd1901b00e5afc54959b46b7fb1e7 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:16:36 +0100 Subject: [PATCH 123/169] CHG: Try add MPI distribution for rtree queries --- cpp/examples/asymmetric_params.cpp | 134 +++++++++++++++++++++++++++-- cpp/memilio/mobility/graph.h | 17 ++-- 2 files changed, 135 insertions(+), 16 deletions(-) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 389c5e3428..62efc6e2a7 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -210,19 +210,89 @@ int main(int /*argc*/, char** /*argv*/) std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); - + graph.reserve_edges(froms.size()); for (size_t i = 0; i < froms.size(); ++i) { - graph.add_edge(froms[i], tos[i], interesting_indices); + graph.lazy_add_edge(froms[i], tos[i], interesting_indices); } - + graph.sort_edges(); + graph.make_edges_unique(); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); - for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors( - tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); + std::vector>> locally_calculated_neighbors; + size_t num_calculations = farm_ids.size() / size; + mio::log_info("Number of calculations: {}", num_calculations); + + for (size_t i = num_calculations * rank; i < num_calculations * (rank + 1); ++i) { + locally_calculated_neighbors.push_back( + tree.in_range_indices_query(graph.nodes()[i].property.get_location(), {mio::geo::kilometers(2.0)})); + } + size_t num_neighbour_queries = 1; + + std::vector locally_saved_neighbour_list_sizes; + for (size_t outer_index = 0; outer_index < num_calculations; ++outer_index) { + for (size_t inner_index = 0; inner_index < num_neighbour_queries; ++inner_index) { + locally_saved_neighbour_list_sizes.push_back(locally_calculated_neighbors[outer_index][inner_index].size()); + } + } + int local_num_total_neighbours = + std::accumulate(locally_saved_neighbour_list_sizes.begin(), locally_saved_neighbour_list_sizes.end(), 0); + std::vector total_neighbours_per_rank(size, 0); + if (rank == 0) + mio::log_info("Now I will start to exchange the neighbor lists on all ranks."); + + MPI_Allgather(&local_num_total_neighbours, 1, MPI_INT, total_neighbours_per_rank.data(), 1, MPI_INT, + MPI_COMM_WORLD); + if (rank == 0) + mio::log_info("Gathering neighbor lists on all ranks."); + std::vector displacements(size, 0); + for (int i = 1; i < size; ++i) { + displacements[i] += displacements[i - 1]; + displacements[i] += total_neighbours_per_rank[i - 1]; + } + + std::vector all_neighbour_list_sizes(size * num_calculations * num_neighbour_queries); + MPI_Allgather(locally_saved_neighbour_list_sizes.data(), num_calculations * num_neighbour_queries, MPI_INT, + all_neighbour_list_sizes.data(), num_calculations * num_neighbour_queries, MPI_INT, MPI_COMM_WORLD); + if (rank == 0) + mio::log_info("Distributing neighbor lists to all ranks."); + std::vector flat_local; + for (const auto& nodes : locally_calculated_neighbors) { + for (const auto& neighbours : nodes) { + flat_local.insert(flat_local.end(), neighbours.begin(), neighbours.end()); + } + } + mio::log_info("The elements of displacements are: {} and the size is {}", displacements[0], displacements.size()); + int total_elements = std::accumulate(all_neighbour_list_sizes.begin(), all_neighbour_list_sizes.end(), 0); + std::vector flat_global(total_elements); + mio::log_info("The total number of elements that worker {} sends is {} and I also want to send {} elements", rank, + total_elements, flat_local.size()); + mio::log_info("All neighbour list sizes size: {}", all_neighbour_list_sizes.size()); + mio::log_info("Elements per rank: {}, {}", total_neighbours_per_rank[0], total_neighbours_per_rank[1]); + MPI_Allgatherv(flat_local.data(), flat_local.size(), MPI_INT, flat_global.data(), total_neighbours_per_rank.data(), + displacements.data(), MPI_INT, MPI_COMM_WORLD); + if (rank == 0) + mio::log_info("Allgather complete."); + + size_t big_index = 0; + for (size_t index = 0; index < size * num_calculations; ++index) { + std::vector> neighbors; + for (size_t i = 0; i < num_neighbour_queries; ++i) { + std::vector current_neighbors(all_neighbour_list_sizes[big_index]); + for (int j = 0; j < all_neighbour_list_sizes[big_index]; ++j) { + current_neighbors[j] = flat_global[big_index + j]; + } + neighbors.push_back(current_neighbors); + big_index++; + } + graph.nodes()[index].property.set_regional_neighbors(neighbors); + } + + for (auto index = num_calculations * size; index < farm_ids.size(); ++index) { + graph.nodes()[index].property.set_regional_neighbors( + tree.in_range_indices_query(graph.nodes()[index].property.get_location(), {mio::geo::kilometers(2.0)})); } auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); @@ -230,9 +300,59 @@ int main(int /*argc*/, char** /*argv*/) for (size_t i = 0; i < dates.size(); ++i) { sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); } + if (rank == 0) { + mio::log_info("I am worker 0 and I will check the correctness of the setup myself:"); + mio::Graph>, + mio::MobilityEdgeDirected> + graph2; + for (size_t i = 0; i < farm_ids.size(); ++i) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph2.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + } + auto nodes2 = graph2.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + auto tree2 = mio::geo::RTree(nodes2.begin(), nodes2.end()); - const size_t num_runs = 4000; + for (size_t index = 0; index < farm_ids.size(); ++index) { + graph2.nodes()[index].property.set_regional_neighbors(tree2.in_range_indices_query( + graph2.nodes()[index].property.get_location(), {mio::geo::kilometers(2.0)})); + } + for (size_t i = 0; i < farm_ids.size(); ++i) { + auto neighbors1 = sim.get_graph().nodes()[i].property.get_regional_neighbors(); + auto neighbors2 = graph2.nodes()[i].property.get_regional_neighbors(); + if (neighbors1.size() != neighbors2.size()) { + mio::log_error("Neighbor list sizes do not match for node {}!", i); + } + else { + for (size_t j = 0; j < neighbors1.size(); ++j) { + if (neighbors1[j].size() != neighbors2[j].size()) { + mio::log_error("Neighbor list sizes do not match for node {}!", i); + } + auto sort1 = neighbors1[j]; + auto sort2 = neighbors2[j]; + std::sort(sort1.begin(), sort1.end()); + std::sort(sort2.begin(), sort2.end()); + if (sort1 != sort2) { + mio::log_error("Neighbor lists do not match for node {}!", i); + } + } + } + } + mio::log_info("Setup correctness check complete."); + } + if (rank == 0) { + mio::log_info("Starting the study:"); + } mio::ParameterStudy study(sim, t0, tmax, dt, num_runs); auto get_simulation = [](const auto& sim, ScalarType, ScalarType, size_t run_id) { diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 6a510ce9a7..75d4ba2fc2 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -206,19 +206,18 @@ class Graph mio::timing::AutoTimer<"Graph.make_edges_unique()"> timer; std::vector> unique_edges; unique_edges.reserve(m_edges.size()); - auto start_node = m_edges.front().start_node_idx; - auto end_node = m_edges.front().end_node_idx; - for (const auto& edge : m_edges) { - if (edge.start_node_idx != start_node || edge.end_node_idx != end_node) { - unique_edges.push_back(edge); - start_node = edge.start_node_idx; - end_node = edge.end_node_idx; - } - } + std::ranges::unique_copy(m_edges, std::back_inserter(unique_edges), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; + }); m_edges = std::move(unique_edges); return m_edges.back(); } + void reserve_edges(size_t n) + { + m_edges.reserve(n); + } + /** * @brief range of nodes */ From a4ec96c4218cd810da96fbb8575b25c9c051cdac Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:41:13 +0100 Subject: [PATCH 124/169] FIX: MPI distributed rtree queries work --- cpp/examples/asymmetric_params.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 62efc6e2a7..0beca2c26c 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -194,6 +194,10 @@ int main(int /*argc*/, char** /*argv*/) bcast_vector(from_exchanges, 0, MPI_COMM_WORLD); bcast_vector(to_exchanges, 0, MPI_COMM_WORLD); + if (rank == 0) { + mio::log_info("Data broadcasted to all {} ranks.", size); + } + for (size_t i = 0; i < farm_ids.size(); ++i) { Model curr_model; curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; @@ -277,12 +281,14 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("Allgather complete."); size_t big_index = 0; + size_t pos = 0; for (size_t index = 0; index < size * num_calculations; ++index) { std::vector> neighbors; for (size_t i = 0; i < num_neighbour_queries; ++i) { std::vector current_neighbors(all_neighbour_list_sizes[big_index]); for (int j = 0; j < all_neighbour_list_sizes[big_index]; ++j) { - current_neighbors[j] = flat_global[big_index + j]; + current_neighbors[j] = flat_global[pos]; + pos++; } neighbors.push_back(current_neighbors); big_index++; @@ -327,6 +333,9 @@ int main(int /*argc*/, char** /*argv*/) graph2.nodes()[index].property.get_location(), {mio::geo::kilometers(2.0)})); } for (size_t i = 0; i < farm_ids.size(); ++i) { + if (sim.get_graph().nodes()[i].id != graph2.nodes()[i].id) { + mio::log_error("Node ids do not match for node {}!", i); + } auto neighbors1 = sim.get_graph().nodes()[i].property.get_regional_neighbors(); auto neighbors2 = graph2.nodes()[i].property.get_regional_neighbors(); if (neighbors1.size() != neighbors2.size()) { @@ -343,6 +352,9 @@ int main(int /*argc*/, char** /*argv*/) std::sort(sort2.begin(), sort2.end()); if (sort1 != sort2) { mio::log_error("Neighbor lists do not match for node {}!", i); + for (size_t index = 0; index < sort1.size(); index++) { + mio::log_error(" Neighbor 1: {}, Neighbor 2: {}", sort1[index], sort2[index]); + } } } } From f736260ec707091d317d17f93317f88b77fe984c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:06:34 +0100 Subject: [PATCH 125/169] CHG: Clean up parameter study --- cpp/examples/asymmetric_params.cpp | 96 +++++------------------------- 1 file changed, 15 insertions(+), 81 deletions(-) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 0beca2c26c..b0a82e7b22 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -116,7 +116,7 @@ int main(int /*argc*/, char** /*argv*/) const auto tmax = 100.; const auto dt = 1.; //initial time step - //total compartment sizes + const size_t num_runs = 100; using Model = mio::smm::Model; auto home = mio::regions::Region(0); @@ -223,18 +223,20 @@ int main(int /*argc*/, char** /*argv*/) auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); - auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + + std::vector query_distances = {mio::geo::kilometers(2.0)}; std::vector>> locally_calculated_neighbors; size_t num_calculations = farm_ids.size() / size; - mio::log_info("Number of calculations: {}", num_calculations); - + // Perform your fair share of rtree queries. for (size_t i = num_calculations * rank; i < num_calculations * (rank + 1); ++i) { locally_calculated_neighbors.push_back( - tree.in_range_indices_query(graph.nodes()[i].property.get_location(), {mio::geo::kilometers(2.0)})); + tree.in_range_indices_query(graph.nodes()[i].property.get_location(), query_distances)); } - size_t num_neighbour_queries = 1; - + size_t num_neighbour_queries = query_distances.size(); + // Look up how many results each query returned. std::vector locally_saved_neighbour_list_sizes; for (size_t outer_index = 0; outer_index < num_calculations; ++outer_index) { for (size_t inner_index = 0; inner_index < num_neighbour_queries; ++inner_index) { @@ -244,42 +246,30 @@ int main(int /*argc*/, char** /*argv*/) int local_num_total_neighbours = std::accumulate(locally_saved_neighbour_list_sizes.begin(), locally_saved_neighbour_list_sizes.end(), 0); std::vector total_neighbours_per_rank(size, 0); - if (rank == 0) - mio::log_info("Now I will start to exchange the neighbor lists on all ranks."); - + // Share with everybody the total number of all your query results. MPI_Allgather(&local_num_total_neighbours, 1, MPI_INT, total_neighbours_per_rank.data(), 1, MPI_INT, MPI_COMM_WORLD); - if (rank == 0) - mio::log_info("Gathering neighbor lists on all ranks."); std::vector displacements(size, 0); for (int i = 1; i < size; ++i) { displacements[i] += displacements[i - 1]; displacements[i] += total_neighbours_per_rank[i - 1]; } - + //Share with everybody the actual lists with query result counts. std::vector all_neighbour_list_sizes(size * num_calculations * num_neighbour_queries); MPI_Allgather(locally_saved_neighbour_list_sizes.data(), num_calculations * num_neighbour_queries, MPI_INT, all_neighbour_list_sizes.data(), num_calculations * num_neighbour_queries, MPI_INT, MPI_COMM_WORLD); - if (rank == 0) - mio::log_info("Distributing neighbor lists to all ranks."); std::vector flat_local; for (const auto& nodes : locally_calculated_neighbors) { for (const auto& neighbours : nodes) { flat_local.insert(flat_local.end(), neighbours.begin(), neighbours.end()); } } - mio::log_info("The elements of displacements are: {} and the size is {}", displacements[0], displacements.size()); int total_elements = std::accumulate(all_neighbour_list_sizes.begin(), all_neighbour_list_sizes.end(), 0); std::vector flat_global(total_elements); - mio::log_info("The total number of elements that worker {} sends is {} and I also want to send {} elements", rank, - total_elements, flat_local.size()); - mio::log_info("All neighbour list sizes size: {}", all_neighbour_list_sizes.size()); - mio::log_info("Elements per rank: {}, {}", total_neighbours_per_rank[0], total_neighbours_per_rank[1]); + //Share with everybody the actual lists with query results. MPI_Allgatherv(flat_local.data(), flat_local.size(), MPI_INT, flat_global.data(), total_neighbours_per_rank.data(), displacements.data(), MPI_INT, MPI_COMM_WORLD); - if (rank == 0) - mio::log_info("Allgather complete."); - + // Reconstruct the neighbor lists for all nodes. size_t big_index = 0; size_t pos = 0; for (size_t index = 0; index < size * num_calculations; ++index) { @@ -295,10 +285,10 @@ int main(int /*argc*/, char** /*argv*/) } graph.nodes()[index].property.set_regional_neighbors(neighbors); } - + // Calculate the nodes that were left out if the number of nodes is not divisible by the number of ranks. for (auto index = num_calculations * size; index < farm_ids.size(); ++index) { graph.nodes()[index].property.set_regional_neighbors( - tree.in_range_indices_query(graph.nodes()[index].property.get_location(), {mio::geo::kilometers(2.0)})); + tree.in_range_indices_query(graph.nodes()[index].property.get_location(), query_distances)); } auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); @@ -307,62 +297,6 @@ int main(int /*argc*/, char** /*argv*/) sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); } if (rank == 0) { - mio::log_info("I am worker 0 and I will check the correctness of the setup myself:"); - mio::Graph>, - mio::MobilityEdgeDirected> - graph2; - for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - graph2.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); - } - auto nodes2 = graph2.nodes() | std::views::transform([](const auto& node) { - return &node.property; - }); - auto tree2 = mio::geo::RTree(nodes2.begin(), nodes2.end()); - - for (size_t index = 0; index < farm_ids.size(); ++index) { - graph2.nodes()[index].property.set_regional_neighbors(tree2.in_range_indices_query( - graph2.nodes()[index].property.get_location(), {mio::geo::kilometers(2.0)})); - } - for (size_t i = 0; i < farm_ids.size(); ++i) { - if (sim.get_graph().nodes()[i].id != graph2.nodes()[i].id) { - mio::log_error("Node ids do not match for node {}!", i); - } - auto neighbors1 = sim.get_graph().nodes()[i].property.get_regional_neighbors(); - auto neighbors2 = graph2.nodes()[i].property.get_regional_neighbors(); - if (neighbors1.size() != neighbors2.size()) { - mio::log_error("Neighbor list sizes do not match for node {}!", i); - } - else { - for (size_t j = 0; j < neighbors1.size(); ++j) { - if (neighbors1[j].size() != neighbors2[j].size()) { - mio::log_error("Neighbor list sizes do not match for node {}!", i); - } - auto sort1 = neighbors1[j]; - auto sort2 = neighbors2[j]; - std::sort(sort1.begin(), sort1.end()); - std::sort(sort2.begin(), sort2.end()); - if (sort1 != sort2) { - mio::log_error("Neighbor lists do not match for node {}!", i); - for (size_t index = 0; index < sort1.size(); index++) { - mio::log_error(" Neighbor 1: {}, Neighbor 2: {}", sort1[index], sort2[index]); - } - } - } - } - } - mio::log_info("Setup correctness check complete."); - } - if (rank == 0) { - mio::log_info("Starting the study:"); } mio::ParameterStudy study(sim, t0, tmax, dt, num_runs); From e40c9c2ecb5212879bfea4ac5e3b94f4f870eed5 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:23:23 +0100 Subject: [PATCH 126/169] CHG: Add correctness check for current code version --- cpp/examples/asymmetric_params.cpp | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index b0a82e7b22..89a2f70cb3 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -296,6 +296,61 @@ int main(int /*argc*/, char** /*argv*/) for (size_t i = 0; i < dates.size(); ++i) { sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); } + if (rank == 0) { + mio::log_info("I am worker 0 and I will check the correctness of the setup myself:"); + mio::Graph>, + mio::MobilityEdgeDirected> + graph2; + for (size_t i = 0; i < farm_ids.size(); ++i) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph2.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + } + auto nodes2 = graph2.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + auto tree2 = mio::geo::RTree(nodes2.begin(), nodes2.end()); + + for (size_t index = 0; index < farm_ids.size(); ++index) { + graph2.nodes()[index].property.set_regional_neighbors( + tree2.in_range_indices_query(graph2.nodes()[index].property.get_location(), query_distances)); + } + for (size_t i = 0; i < farm_ids.size(); ++i) { + if (sim.get_graph().nodes()[i].id != graph2.nodes()[i].id) { + mio::log_error("Node ids do not match for node {}!", i); + } + auto neighbors1 = sim.get_graph().nodes()[i].property.get_regional_neighbors(); + auto neighbors2 = graph2.nodes()[i].property.get_regional_neighbors(); + if (neighbors1.size() != neighbors2.size()) { + mio::log_error("Neighbor list sizes do not match for node {}!", i); + } + else { + for (size_t j = 0; j < neighbors1.size(); ++j) { + if (neighbors1[j].size() != neighbors2[j].size()) { + mio::log_error("Neighbor list sizes do not match for node {}!", i); + } + auto sort1 = neighbors1[j]; + auto sort2 = neighbors2[j]; + std::sort(sort1.begin(), sort1.end()); + std::sort(sort2.begin(), sort2.end()); + if (sort1 != sort2) { + mio::log_error("Neighbor lists do not match for node {}!", i); + for (size_t index = 0; index < sort1.size(); index++) { + mio::log_error(" Neighbor 1: {}, Neighbor 2: {}", sort1[index], sort2[index]); + } + } + } + } + } + mio::log_info("Setup correctness check complete."); + } if (rank == 0) { mio::log_info("Starting the study:"); } From 6e6c60ac5c578751d6a7fb37b717f0b2c43924d6 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:23:51 +0100 Subject: [PATCH 127/169] CHG: remove correctness check --- cpp/examples/asymmetric_params.cpp | 55 ------------------------------ 1 file changed, 55 deletions(-) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 89a2f70cb3..b0a82e7b22 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -296,61 +296,6 @@ int main(int /*argc*/, char** /*argv*/) for (size_t i = 0; i < dates.size(); ++i) { sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); } - if (rank == 0) { - mio::log_info("I am worker 0 and I will check the correctness of the setup myself:"); - mio::Graph>, - mio::MobilityEdgeDirected> - graph2; - for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - graph2.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); - } - auto nodes2 = graph2.nodes() | std::views::transform([](const auto& node) { - return &node.property; - }); - auto tree2 = mio::geo::RTree(nodes2.begin(), nodes2.end()); - - for (size_t index = 0; index < farm_ids.size(); ++index) { - graph2.nodes()[index].property.set_regional_neighbors( - tree2.in_range_indices_query(graph2.nodes()[index].property.get_location(), query_distances)); - } - for (size_t i = 0; i < farm_ids.size(); ++i) { - if (sim.get_graph().nodes()[i].id != graph2.nodes()[i].id) { - mio::log_error("Node ids do not match for node {}!", i); - } - auto neighbors1 = sim.get_graph().nodes()[i].property.get_regional_neighbors(); - auto neighbors2 = graph2.nodes()[i].property.get_regional_neighbors(); - if (neighbors1.size() != neighbors2.size()) { - mio::log_error("Neighbor list sizes do not match for node {}!", i); - } - else { - for (size_t j = 0; j < neighbors1.size(); ++j) { - if (neighbors1[j].size() != neighbors2[j].size()) { - mio::log_error("Neighbor list sizes do not match for node {}!", i); - } - auto sort1 = neighbors1[j]; - auto sort2 = neighbors2[j]; - std::sort(sort1.begin(), sort1.end()); - std::sort(sort2.begin(), sort2.end()); - if (sort1 != sort2) { - mio::log_error("Neighbor lists do not match for node {}!", i); - for (size_t index = 0; index < sort1.size(); index++) { - mio::log_error(" Neighbor 1: {}, Neighbor 2: {}", sort1[index], sort2[index]); - } - } - } - } - } - mio::log_info("Setup correctness check complete."); - } if (rank == 0) { mio::log_info("Starting the study:"); } From 14c753cd0dacdb59d3d399eaaf9869b8468763d6 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:53:06 +0100 Subject: [PATCH 128/169] CHG: Update file --- cpp/examples/asymmetric_graph.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 7fb34e15ca..a69eb6f337 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -104,17 +104,17 @@ int main(int /*argc*/, char** /*argv*/) std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); - // graph.reserve_edges(262144); + graph.reserve_edges(200000); { mio::timing::AutoTimer<"Graph Edges Generation"> timer; io::CSVReader<2> edges("../../edges10000.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { - graph.add_edge(from, to, interesting_indices); - // graph.lazy_add_edge(from, to, interesting_indices); + graph.lazy_add_edge(from, to, interesting_indices); } - // graph.sort_edges(); + graph.sort_edges(); + graph.make_edges_unique(); } // mio::log_info("Graph generated"); @@ -125,11 +125,12 @@ int main(int /*argc*/, char** /*argv*/) // mio::log_info("RTree generated"); for (auto& node : graph.nodes()) { + mio::timing::AutoTimer<"neighbourhood search"> timer; node.property.set_regional_neighbors( tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); } - // mio::log_info("Neighbors set"); + mio::log_info("Neighbors set"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); io::CSVReader<4> exchanges("../../trade10000.csv"); From e46e15ed7ef971a3baa7fa8bc215572a113fc82e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:23:41 +0100 Subject: [PATCH 129/169] CHG: Add Culling, vaccination --- cpp/memilio/mobility/graph_simulation.h | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 1bbc066a81..c06bd6c159 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -25,6 +25,7 @@ #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" #include "memilio/utils/random_number_generator.h" +#include #include #include "memilio/compartments/feedback_simulation.h" #include "memilio/geography/regions.h" @@ -410,6 +411,8 @@ class AsymmetricGraphSimulation : public GraphSimulationBase 0) { + auto [node_id, day] = culling_queue.front(); + auto animals = Base::m_graph.nodes()[node_id].property.get_result().get_last_value(); + auto num_animals = std::accumulate(animals.begin(), animals.end() - 1, 0); + if (num_animals <= capacity) { + mio::log_info("Culling {} animals at node {} starting on day {} and going on for {} days.", num_animals, + node_id, Base::m_t, dt); + for (auto index = 0; index < (animals.size() - 1); index++) { + animals[index] = 0; + } + animals[animals.size() - 1] += num_animals; + culling_queue.pop(); + capacity -= num_animals; + } + else { + while (capacity > 0) { + for (auto index = 0; index < (animals.size() - 1); index++) { + if (animals[index] < capacity) { + capacity -= animals[index]; + animals[animals.size() - 1] += animals[index]; + animals[index] = 0; + } + else { + animals[animals.size() - 1] += capacity; + animals[index] -= capacity; + capacity = 0; + break; + } + } + } + } + } + } + + /** + * @brief Go through the vaccination queue and vaccinate as many animals as possible within dt. + * + * @param dt Time span for vaccination. + */ + void vaccinate(ScalarType dt) + { + auto capacity = vaccination_capacity_per_day * dt; + while (!vaccination_queue.empty() && capacity > 0) { + auto [node_id, day] = vaccination_queue.front(); + auto animals = Base::m_graph.nodes()[node_id].property.get_result().get_last_value(); + auto num_animals = std::accumulate(animals.begin(), animals.end() - 1, 0); + if (num_animals <= capacity) { + mio::log_info("Vaccinating {} animals at node {} starting on day {} and going on for {} days.", + num_animals, node_id, Base::m_t, dt); + for (auto index = 0; index < (animals.size() - 2); index++) { + animals[index] = 0; + } + animals[animals.size() - 2] += num_animals; + vaccination_queue.pop(); + capacity -= num_animals; + } + else { + while (capacity > 0) { + for (auto index = 0; index < (animals.size() - 1); index++) { + if (animals[index] < capacity) { + capacity -= animals[index]; + animals[animals.size() - 2] += animals[index]; + animals[index] = 0; + } + else { + animals[animals.size() - 2] += capacity; + animals[index] -= capacity; + capacity = 0; + break; + } + } + } + } + } + } + void add_exchange(double time, double number, size_t from, size_t to) { m_parameters.add_exchange(time, number, from, to); @@ -544,6 +631,15 @@ class AsymmetricGraphSimulation : public GraphSimulationBase> all_time_series; + for (auto& n : Base::m_graph.nodes()) { + all_time_series.push_back(n.property.get_result()); + } + return all_time_series; + } + auto statistics_per_timestep(std::vector node_indices) { assert(node_indices.size() > 0); @@ -610,9 +706,25 @@ class AsymmetricGraphSimulation : public GraphSimulationBase 4) { + mio::log_debug("Node {} is quarantined at time {} because there are {} total infections.", n.id, + Base::m_t, total_infections); + cull_node(n.id); + } } } + void cull_node(size_t node_id) + { + culling_queue.push(std::make_pair(node_id, Base::m_t)); + Base::m_graph.nodes()[node_id].property.set_quarantined(true); + } + + void vaccinate_node(size_t node_id) + { + vaccination_queue.push(std::make_pair(node_id, Base::m_t)); + } + RandomNumberGenerator& get_rng() { return m_rng; @@ -623,6 +735,10 @@ class AsymmetricGraphSimulation : public GraphSimulationBase> culling_queue; + ScalarType culling_capacity_per_day = 200; + std::queue> vaccination_queue; + ScalarType vaccination_capacity_per_day = 500; }; template From b0c5e31c6bf9c7cc381ea29514524141043cb6ec Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:24:02 +0100 Subject: [PATCH 130/169] CHG: Add vaccination to advance function --- cpp/memilio/mobility/graph_simulation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index c06bd6c159..2d3b4ea3c7 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -25,7 +25,6 @@ #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" #include "memilio/utils/random_number_generator.h" -#include #include #include "memilio/compartments/feedback_simulation.h" #include "memilio/geography/regions.h" @@ -412,6 +411,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase Date: Thu, 6 Nov 2025 15:20:57 +0100 Subject: [PATCH 131/169] CHG: Add lazy edge addition function --- cpp/memilio/mobility/graph.h | 56 +++++++++++++++++++++++++++++++ cpp/tests/test_graph.cpp | 35 +++++++++++++++++++ docs/source/cpp/graph_metapop.rst | 15 +++++++++ 3 files changed, 106 insertions(+) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 26a8bb5a6a..7b49954993 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -178,6 +178,62 @@ class Graph }); } + /** + * @brief Add edges to a graph without checking for duplicates and without sorting. + * + * @param start_node_idx Id of start node + * @param end_node_idx Id of end node + * @param args Additional arguments for edge construction + * @return Edge& End of edge vector + */ + template + Edge& lazy_add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) + { + assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); + m_edges.emplace_back(start_node_idx, end_node_idx, std::forward(args)...); + return m_edges.back(); + } + + /** + * @brief Sort the edge vector of a graph. + * + * @return Edge& End of edge vector + */ + Edge& sort_edges() + { + std::sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx + : e1.start_node_idx < e2.start_node_idx; + }); + return m_edges.back(); + } + + /** + * @brief Make the edges of a graph unique. + * + * Copies all the unique edges to a new vector and replaces the edge vector of the graph with it. Unique means that + * the start and end node indices are unique. Other edge properties are not checked. + * @return Edge& End of edge vector + */ + Edge& make_edges_unique() + { + std::vector> unique_edges; + unique_edges.reserve(m_edges.size()); + std::ranges::unique_copy(m_edges, std::back_inserter(unique_edges), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; + }); + m_edges = std::move(unique_edges); + return m_edges.back(); + } + + /** + * @brief reserve space for edges. + */ + void reserve_edges(size_t n) + { + m_edges.reserve(n); + } + /** * @brief range of nodes */ diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index 65bdb71c42..319921bec2 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -344,6 +344,41 @@ TEST(TestGraph, ot_edges) EXPECT_THAT(g.out_edges(1), testing::ElementsAreArray(v1)); } +TEST(TestGraph, compare_add_edge_functions) +{ + mio::Graph g; + mio::Graph g_lazy; + int num_nodes = 10; + for (int index = 0; index < num_nodes; ++index) { + g.add_node(index); + g_lazy.add_node(index); + } + for (int first_node = num_nodes; first_node >= 0; --first_node) { + for (int second_node = 0; second_node < num_nodes; ++second_node) { + if (first_node != second_node) { + g.add_edge(first_node, second_node, int(first_node + second_node)); + g_lazy.lazy_add_edge(first_node, second_node, int(first_node + second_node)); + } + } + } + for (int first_node = num_nodes; first_node >= 0; --first_node) { + for (int second_node = 0; second_node < num_nodes; ++second_node) { + if (first_node != second_node) { + g.add_edge(first_node, second_node, int(first_node + second_node)); + g_lazy.lazy_add_edge(first_node, second_node, int(first_node + second_node)); + } + } + } + g_lazy.sort_edges(); + g_lazy.make_edges_unique(); + EXPECT_EQ(g.edges().size(), g_lazy.edges().size()); + + for (size_t index = 0; index < g.edges().size(); index++) { + EXPECT_EQ(g.edges()[index].start_node_idx, g_lazy.edges()[index].start_node_idx); + EXPECT_EQ(g.edges()[index].end_node_idx, g_lazy.edges()[index].end_node_idx); + } +} + namespace { diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index 1662d91246..2c07bd2c3e 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -177,6 +177,21 @@ The following steps detail how to configure and execute a graph simulation: graph.add_edge(0, 1, std::move(transition_rates)); graph.add_edge(1, 0, std::move(transition_rates)); +.. dropdown:: :fa:`gears` Working with large graphs + + When working with very large graphs, i.e. starting from a few thousand edges, it will be faster to not use the standard ``add_edge`` function. This function always + keeps the list of edges inside the graph sorted and checks for duplicates. For large graphs, it is faster to first add all the edges to the graph + and then sort them and remove duplicates: + + .. code-block:: cpp + + graph.reserve_edges(2); + graph.lazy_add_edge(0, 1, std::move(transition_rates)); + graph.lazy_add_edge(1, 0, std::move(transition_rates)); + graph.sort_edges(); + graph.make_edges_unique(); + + 5. **Initialize and Advance the Mobility Simulation:** With the graph constructed, initialize the simulation with the starting time and time step. Then, advance the simulation until the final time :math:`t_{max}`. From d14011baae6252649539a120cd35d5c9aea63b22 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:55:10 +0100 Subject: [PATCH 132/169] FIX: start edge generation with correct node index --- cpp/tests/test_graph.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index 319921bec2..dba5dacdd9 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -353,7 +353,7 @@ TEST(TestGraph, compare_add_edge_functions) g.add_node(index); g_lazy.add_node(index); } - for (int first_node = num_nodes; first_node >= 0; --first_node) { + for (int first_node = num_nodes - 1; first_node >= 0; --first_node) { for (int second_node = 0; second_node < num_nodes; ++second_node) { if (first_node != second_node) { g.add_edge(first_node, second_node, int(first_node + second_node)); @@ -361,7 +361,7 @@ TEST(TestGraph, compare_add_edge_functions) } } } - for (int first_node = num_nodes; first_node >= 0; --first_node) { + for (int first_node = num_nodes - 1; first_node >= 0; --first_node) { for (int second_node = 0; second_node < num_nodes; ++second_node) { if (first_node != second_node) { g.add_edge(first_node, second_node, int(first_node + second_node)); From 6bb9762224da5e1f37a618a9c26318a7b29fb1c0 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:04:04 +0100 Subject: [PATCH 133/169] CHG: Add vaccinations --- cpp/examples/asym_graph_ben.cpp | 1 + cpp/examples/asymmetric_graph.cpp | 1 + cpp/examples/asymmetric_params.cpp | 5 +++-- cpp/memilio/mobility/graph_simulation.h | 25 ++++++++++++++++--------- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/cpp/examples/asym_graph_ben.cpp b/cpp/examples/asym_graph_ben.cpp index cf425bdb9f..e02b6795fa 100644 --- a/cpp/examples/asym_graph_ben.cpp +++ b/cpp/examples/asym_graph_ben.cpp @@ -43,6 +43,7 @@ enum class InfectionState INS, ICS, R, + V, D, Count }; diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index a69eb6f337..1e41fca6c0 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -43,6 +43,7 @@ enum class InfectionState INS, ICS, R, + V, D, Count }; diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index b0a82e7b22..375cd639b0 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -48,6 +48,7 @@ enum class InfectionState INS, ICS, R, + V, D, Count }; @@ -226,7 +227,7 @@ int main(int /*argc*/, char** /*argv*/) auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); - std::vector query_distances = {mio::geo::kilometers(2.0)}; + std::vector query_distances = {mio::geo::kilometers(5.0)}; std::vector>> locally_calculated_neighbors; size_t num_calculations = farm_ids.size() / size; @@ -309,7 +310,7 @@ int main(int /*argc*/, char** /*argv*/) } auto index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( {mio::regions::Region(0), InfectionState::E}); - sim2.get_graph().nodes()[0].property.get_result().get_last_value()[index] = 10; + sim2.get_graph().nodes()[145236].property.get_result().get_last_value()[index] = 100; return sim2; }; auto handle_result = [](auto&& sim, auto&& run) { diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 2d3b4ea3c7..607ce3fd24 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -423,8 +423,8 @@ class AsymmetricGraphSimulation : public GraphSimulationBase 0) { + mio::log_debug("Culling {} animals of node {} on day {} for {} days.", capacity, node_id, Base::m_t, + dt); for (auto index = 0; index < (animals.size() - 1); index++) { if (animals[index] < capacity) { capacity -= animals[index]; @@ -493,8 +495,8 @@ class AsymmetricGraphSimulation : public GraphSimulationBase 4) { - mio::log_debug("Node {} is quarantined at time {} because there are {} total infections.", n.id, - Base::m_t, total_infections); + mio::log_debug("Node {} is culled at time {} because there are {} total infections.", n.id, Base::m_t, + total_infections); cull_node(n.id); + for (size_t index = 0; index < n.property.get_regional_neighbors()[0].size(); ++index) { + const auto node_id = n.property.get_regional_neighbors()[0][index]; + vaccinate(node_id); + } } } } @@ -718,6 +724,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase> culling_queue; - ScalarType culling_capacity_per_day = 200; + ScalarType culling_capacity_per_day = 2000; std::queue> vaccination_queue; ScalarType vaccination_capacity_per_day = 500; }; From 0f68abadba1bbd1bb353f2ccbc8de7359c6b6bfc Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:06:26 +0100 Subject: [PATCH 134/169] CHG: use more farms --- cpp/examples/asymmetric_params.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 375cd639b0..6427525d9f 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -152,7 +152,7 @@ int main(int /*argc*/, char** /*argv*/) std::vector farm_ids, num_cows_vec, dates, num_animals_exchanges; std::vector froms, tos, from_exchanges, to_exchanges; if (rank == 0) { - io::CSVReader<4> farms("../../farms10000.csv"); + io::CSVReader<4> farms("../../farms200000.csv"); farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); int farm_id, num_cows; double latitude, longitude; @@ -162,14 +162,14 @@ int main(int /*argc*/, char** /*argv*/) latitudes.push_back(latitude); longitudes.push_back(longitude); } - io::CSVReader<2> edges("../../edges10000.csv"); + io::CSVReader<2> edges("../../edges200000.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { froms.push_back(from); tos.push_back(to); } - io::CSVReader<4> exchanges("../../trade10000.csv"); + io::CSVReader<4> exchanges("../../trade200000.csv"); exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to"); int date, num_animals; From 7fa39ae468eb0195aec50ae40d98b09943dca930 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:20:02 +0100 Subject: [PATCH 135/169] CHG: Rename make_edges_unique --- cpp/memilio/mobility/graph.h | 10 +++++++--- cpp/tests/test_graph.cpp | 2 +- docs/source/cpp/graph_metapop.rst | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 7b49954993..382e11a6b7 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -185,6 +185,9 @@ class Graph * @param end_node_idx Id of end node * @param args Additional arguments for edge construction * @return Edge& End of edge vector + * + * This can be used in combination with :ref sort_edges and :ref remove_duplicate_edges instead of :ref add_edge + * for better performance when adding many edges at once. */ template Edge& lazy_add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) @@ -209,13 +212,14 @@ class Graph } /** - * @brief Make the edges of a graph unique. + * @brief Remove duplicate edges from a sorted edge vector. * * Copies all the unique edges to a new vector and replaces the edge vector of the graph with it. Unique means that - * the start and end node indices are unique. Other edge properties are not checked. + * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the first + * edge in the vector is kept. * @return Edge& End of edge vector */ - Edge& make_edges_unique() + Edge& remove_duplicate_edges() { std::vector> unique_edges; unique_edges.reserve(m_edges.size()); diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index dba5dacdd9..dfbffa3e59 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -370,7 +370,7 @@ TEST(TestGraph, compare_add_edge_functions) } } g_lazy.sort_edges(); - g_lazy.make_edges_unique(); + g_lazy.remove_duplicate_edges(); EXPECT_EQ(g.edges().size(), g_lazy.edges().size()); for (size_t index = 0; index < g.edges().size(); index++) { diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index 2c07bd2c3e..d540a9dd62 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -189,7 +189,7 @@ The following steps detail how to configure and execute a graph simulation: graph.lazy_add_edge(0, 1, std::move(transition_rates)); graph.lazy_add_edge(1, 0, std::move(transition_rates)); graph.sort_edges(); - graph.make_edges_unique(); + graph.remove_duplicate_edges(); 5. **Initialize and Advance the Mobility Simulation:** From 52bbf8f49a436ae3afaad4b7cc7e068f286c431a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:44:19 +0100 Subject: [PATCH 136/169] CHG: Add timer for advance function --- cpp/memilio/mobility/graph_simulation.h | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 607ce3fd24..3aa09a3552 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -403,6 +403,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase timer; auto dt = m_parameters.next_event_time() - Base::m_t; while (Base::m_t < t_max) { mio::log_debug("Time: {}", Base::m_t); From c237bb59aa4bf7a4ee485bdbbaa732081ccff8af Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:48:17 +0100 Subject: [PATCH 137/169] CHG: files for performance comparison --- cpp/examples/asymmetric_graph.cpp | 53 ++++++++++++++++-------------- cpp/examples/asymmetric_params.cpp | 2 +- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 1e41fca6c0..90e7c633c5 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -83,7 +83,7 @@ int main(int /*argc*/, char** /*argv*/) mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; - io::CSVReader<4> farms("../../farms10000.csv"); + io::CSVReader<4> farms("../../farms200000.csv"); farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); int farm_id, num_cows; double latitude, longitude; @@ -108,7 +108,7 @@ int main(int /*argc*/, char** /*argv*/) graph.reserve_edges(200000); { mio::timing::AutoTimer<"Graph Edges Generation"> timer; - io::CSVReader<2> edges("../../edges10000.csv"); + io::CSVReader<2> edges("../../edges200000.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { @@ -117,7 +117,7 @@ int main(int /*argc*/, char** /*argv*/) graph.sort_edges(); graph.make_edges_unique(); } - // mio::log_info("Graph generated"); + // // mio::log_info("Graph generated"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; @@ -128,13 +128,13 @@ int main(int /*argc*/, char** /*argv*/) for (auto& node : graph.nodes()) { mio::timing::AutoTimer<"neighbourhood search"> timer; node.property.set_regional_neighbors( - tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); + tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(5.0)})); } mio::log_info("Neighbors set"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - io::CSVReader<4> exchanges("../../trade10000.csv"); + io::CSVReader<4> exchanges("../../trade200000.csv"); exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to"); int date, num_animals; @@ -144,16 +144,17 @@ int main(int /*argc*/, char** /*argv*/) } // mio::log_info("Exchanges added"); - // #ifdef MEMILIO_ENABLE_OPENMP - // #pragma omp parallel for - // for (size_t i = 0; i < 100; ++i) { - // #endif + // // #ifdef MEMILIO_ENABLE_OPENMP + // // #pragma omp parallel for + // // for (size_t i = 0; i < 100; ++i) { + // // #endif + + // // mio::log_info("new Simulation created"); + // sim2.infectionrisk = 0.1; - auto sim2(sim); - // mio::log_info("new Simulation created"); + sim.advance(tmax); - sim2.advance(tmax); - // mio::log_info("Simulation finished"); + mio::log_info("Simulation finished"); // #ifdef MEMILIO_ENABLE_OPENMP // } @@ -171,24 +172,26 @@ int main(int /*argc*/, char** /*argv*/) // // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); // // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); - auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); + // auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); - // for (auto node : sim2.get_graph().nodes()) { - // if (node.property.get_result().get_num_time_points() < num_time_points) { - // mio::log_error("Node with inconsistent number of time points in results."); - // } - // } + // // for (auto node : sim2.get_graph().nodes()) { + // // if (node.property.get_result().get_num_time_points() < num_time_points) { + // // mio::log_error("Node with inconsistent number of time points in results."); + // // } + // // } - // exchange_results = sim2.sum_nodes(); - // // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); + // // exchange_results = sim2.sum_nodes(); + // // // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); - sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); - // // auto combined_results = sim2.combine_node_results(); - // // combined_results.print_table({"S", "E", "I", "R", "D"}); - // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); + // sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); + // // // auto combined_results = sim2.combine_node_results(); + // // // combined_results.print_table({"S", "E", "I", "R", "D"}); + // // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); // mio::log_info("Finished postprocessing"); + mio::unused(sim.statistics_per_timestep().export_csv(mio::path_join("AsymmetricParams_single_run.csv"))); + return 0; } diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 6427525d9f..a8a674ff22 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -117,7 +117,7 @@ int main(int /*argc*/, char** /*argv*/) const auto tmax = 100.; const auto dt = 1.; //initial time step - const size_t num_runs = 100; + const size_t num_runs = 100000; using Model = mio::smm::Model; auto home = mio::regions::Region(0); From 3e5ad8e7807ae02a6e51b7c4984b49879aa6b66c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:15:27 +0100 Subject: [PATCH 138/169] CHG: Store first detection time --- cpp/memilio/mobility/graph_simulation.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 3aa09a3552..13b5188709 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -710,6 +710,9 @@ class AsymmetricGraphSimulation : public GraphSimulationBase 4) { + if (first_detection > Base::m_t) { + first_detection = Base::m_t; + } mio::log_debug("Node {} is culled at time {} because there are {} total infections.", n.id, Base::m_t, total_infections); cull_node(n.id); @@ -747,6 +750,7 @@ class AsymmetricGraphSimulation : public GraphSimulationBase> vaccination_queue; ScalarType vaccination_capacity_per_day = 500; + ScalarType first_detection = std::numeric_limits::max(); }; template From 32b1bec7f95214eee091ef1894fe2ed10879e910 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:12:47 +0100 Subject: [PATCH 139/169] CHG: Make add_edge void --- cpp/memilio/mobility/graph.h | 14 ++++++-------- cpp/memilio/utils/stl_util.h | 9 +++++---- cpp/tests/test_stl_util.cpp | 16 ---------------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 382e11a6b7..80fe278313 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -166,16 +166,14 @@ class Graph * @brief add an edge to the graph. property of the edge is constructed from arguments. */ template - Edge& add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) + void add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) { assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); - return *insert_sorted_replace(m_edges, - Edge(start_node_idx, end_node_idx, std::forward(args)...), - [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx - ? e1.end_node_idx < e2.end_node_idx - : e1.start_node_idx < e2.start_node_idx; - }); + insert_sorted_replace(m_edges, Edge(start_node_idx, end_node_idx, std::forward(args)...), + [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx + : e1.start_node_idx < e2.start_node_idx; + }); } /** diff --git a/cpp/memilio/utils/stl_util.h b/cpp/memilio/utils/stl_util.h index f1deb46c04..72a668eac0 100644 --- a/cpp/memilio/utils/stl_util.h +++ b/cpp/memilio/utils/stl_util.h @@ -65,7 +65,7 @@ inline std::ostream& set_ostream_format(std::ostream& out, size_t width, size_t * @return iterator to inserted or replaced item in vec */ template -typename std::vector::iterator insert_sorted_replace(std::vector& vec, T const& item, Pred pred) +void insert_sorted_replace(std::vector& vec, T const& item, Pred pred) { auto bounds = std::equal_range(begin(vec), end(vec), item, pred); auto lb = bounds.first; @@ -73,15 +73,16 @@ typename std::vector::iterator insert_sorted_replace(std::vector& vec, T c assert(ub - lb <= 1); //input vector contains at most one item that is equal to the new item if (ub - lb == 1) { *lb = item; - return lb; + return; } else { - return vec.insert(lb, item); + vec.insert(lb, item); + return; } } template -typename std::vector::iterator insert_sorted_replace(std::vector& vec, T const& item) +void insert_sorted_replace(std::vector& vec, T const& item) { return insert_sorted_replace(vec, item, std::less()); } diff --git a/cpp/tests/test_stl_util.cpp b/cpp/tests/test_stl_util.cpp index 3de196389e..a09498c51f 100644 --- a/cpp/tests/test_stl_util.cpp +++ b/cpp/tests/test_stl_util.cpp @@ -109,22 +109,6 @@ TEST(TestInsertSortedReplace, normal) EXPECT_THAT(v, testing::ElementsAre(1, 2, 5, 6, 7)); } -TEST(TestInsertSortedReplace, returnsValidIterator) -{ - std::vector v; - int x; - - //There is no GTEST_NO_DEATH macro so we just let the test crash. - //If this test crashes, the function does not return a valid iterator. - //Dereferencing an invalid iterator is undefined behavior so the test - //may behave unexpectedly (pass, fail, or something else) if the iterator is invalid. - x = *mio::insert_sorted_replace(v, 5); - x = *mio::insert_sorted_replace(v, 1); - x = *mio::insert_sorted_replace(v, 4); - x = *mio::insert_sorted_replace(v, 7); - ASSERT_EQ(x, 7); -} - TEST(TestInsertSortedReplace, reverse) { std::vector v = {5}; From 9b37c779fab386202280052e9f865a047c9cc187 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:19:50 +0100 Subject: [PATCH 140/169] CHG: Make add_node void --- cpp/memilio/mobility/graph.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 80fe278313..e85f25cbe3 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -156,10 +156,9 @@ class Graph * @brief add a node to the graph. property of the node is constructed from arguments. */ template - Node& add_node(int id, Args&&... args) + void add_node(int id, Args&&... args) { m_nodes.emplace_back(id, std::forward(args)...); - return m_nodes.back(); } /** From d908d71e54ffa9fdd13bb8fd8a73d66aa303390f Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:34:29 +0100 Subject: [PATCH 141/169] CHG: Add GraphBuilder --- cpp/memilio/mobility/graph.h | 177 ++++++++++++++++++++---------- cpp/tests/test_graph.cpp | 63 +++++------ docs/source/cpp/graph_metapop.rst | 31 ++++-- 3 files changed, 171 insertions(+), 100 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index e85f25cbe3..7b8b24d521 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -20,7 +20,6 @@ #ifndef GRAPH_H #define GRAPH_H -#include #include "memilio/utils/stl_util.h" #include "memilio/epidemiology/age_group.h" #include "memilio/utils/date.h" @@ -28,7 +27,10 @@ #include "memilio/utils/parameter_distributions.h" #include "memilio/epidemiology/damping.h" #include "memilio/geography/regions.h" +#include +#include #include +#include #include "boost/filesystem.hpp" @@ -152,6 +154,13 @@ class Graph using NodeProperty = NodePropertyT; using EdgeProperty = EdgePropertyT; + Graph() = default; + Graph(std::vector>&& nodes, std::vector>&& edges) + : m_nodes(std::move(nodes)) + , m_edges(std::move(edges)) + { + } + /** * @brief add a node to the graph. property of the node is constructed from arguments. */ @@ -175,66 +184,6 @@ class Graph }); } - /** - * @brief Add edges to a graph without checking for duplicates and without sorting. - * - * @param start_node_idx Id of start node - * @param end_node_idx Id of end node - * @param args Additional arguments for edge construction - * @return Edge& End of edge vector - * - * This can be used in combination with :ref sort_edges and :ref remove_duplicate_edges instead of :ref add_edge - * for better performance when adding many edges at once. - */ - template - Edge& lazy_add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) - { - assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); - m_edges.emplace_back(start_node_idx, end_node_idx, std::forward(args)...); - return m_edges.back(); - } - - /** - * @brief Sort the edge vector of a graph. - * - * @return Edge& End of edge vector - */ - Edge& sort_edges() - { - std::sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx - : e1.start_node_idx < e2.start_node_idx; - }); - return m_edges.back(); - } - - /** - * @brief Remove duplicate edges from a sorted edge vector. - * - * Copies all the unique edges to a new vector and replaces the edge vector of the graph with it. Unique means that - * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the first - * edge in the vector is kept. - * @return Edge& End of edge vector - */ - Edge& remove_duplicate_edges() - { - std::vector> unique_edges; - unique_edges.reserve(m_edges.size()); - std::ranges::unique_copy(m_edges, std::back_inserter(unique_edges), [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; - }); - m_edges = std::move(unique_edges); - return m_edges.back(); - } - - /** - * @brief reserve space for edges. - */ - void reserve_edges(size_t n) - { - m_edges.reserve(n); - } - /** * @brief range of nodes */ @@ -493,6 +442,112 @@ void print_graph(std::ostream& os, const Graph& g) } } +/** + * @brief A builder class for constructing graphs. + * + * This class provides a interface for adding nodes and edges to a graph. It allows for efficient construction of large + * graphs by reserving space for nodes and edges in advance. The build method finalizes the graph by sorting edges and + * optionally removing duplicates. + * The advantage over the :ref add_edge function of the Graph class is that edges are only sorted once during the build + * process, improving performance when adding many edges. + * + * @tparam NodePropertyT Type of the node property. + * @tparam EdgePropertyT Type of the edge property. + */ +template +class GraphBuilder +{ +public: + using NodeProperty = NodePropertyT; + using EdgeProperty = EdgePropertyT; + + GraphBuilder() = default; + GraphBuilder(const size_t num_nodes, const size_t num_edges) + { + m_nodes.reserve(num_nodes); + m_edges.reserve(num_edges); + } + + /** + * @brief Add a node to the GraphBuilder. + * + * The property of the node is constructed from arguments. + * @param id Id for the node. + * @tparam args Additional arguments for node construction. + */ + template + void add_node(int id, Args&&... args) + { + m_nodes.emplace_back(id, std::forward(args)...); + } + + /** + * @brief Add an edge to the GraphBuilder. + * + * @param start_node_idx Id of start node + * @param end_node_idx Id of end node + * @tparam args Additional arguments for edge construction + */ + template + void add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) + { + assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); + m_edges.emplace_back(start_node_idx, end_node_idx, std::forward(args)...); + } + + /** + * @brief Build the graph from the added nodes and edges. + * + * Sorts the edges and optionally removes duplicate edges (same start and end node indices). + * @param make_unique If true, duplicate edges are removed. The first added edge is kept! + * @return Graph The constructed graph. + * @tparam NodeProperty The type of the node property. + * @tparam EdgeProperty The type of the edge property. + */ + Graph build(bool make_unique = false) + { + sort_edges(); + if (make_unique) { + remove_duplicate_edges(); + } + Graph graph(std::move(m_nodes), std::move(m_edges)); + return graph; + } + +private: + /** + * @brief Sort the edge vector of a graph. + */ + void sort_edges() + { + std::stable_sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx + : e1.start_node_idx < e2.start_node_idx; + }); + } + + /** + * @brief Remove duplicate edges from a sorted edge vector. + * + * Copies all the unique edges to a new vector and replaces the original edge vector with it. Unique means that + * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the first + * edge in the vector is kept. + */ + void remove_duplicate_edges() + { + std::vector> unique_edges; + unique_edges.reserve(m_edges.size()); + std::ranges::unique_copy(m_edges, std::back_inserter(unique_edges), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; + }); + m_edges = std::move(unique_edges); + } + +private: + std::vector> m_nodes; + std::vector> m_edges; +}; + } // namespace mio #endif //GRAPH_H diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index dfbffa3e59..b084537cb8 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -325,7 +325,7 @@ TEST(TestGraph, set_edges_saving_edges) EXPECT_EQ(indices_edge1, indices_save_edges); } -TEST(TestGraph, ot_edges) +TEST(TestGraph, out_edges) { mio::Graph g; g.add_node(0); @@ -344,38 +344,39 @@ TEST(TestGraph, ot_edges) EXPECT_THAT(g.out_edges(1), testing::ElementsAreArray(v1)); } -TEST(TestGraph, compare_add_edge_functions) +TEST(TestGraphBuilder, Build) { - mio::Graph g; - mio::Graph g_lazy; - int num_nodes = 10; - for (int index = 0; index < num_nodes; ++index) { - g.add_node(index); - g_lazy.add_node(index); - } - for (int first_node = num_nodes - 1; first_node >= 0; --first_node) { - for (int second_node = 0; second_node < num_nodes; ++second_node) { - if (first_node != second_node) { - g.add_edge(first_node, second_node, int(first_node + second_node)); - g_lazy.lazy_add_edge(first_node, second_node, int(first_node + second_node)); - } - } - } - for (int first_node = num_nodes - 1; first_node >= 0; --first_node) { - for (int second_node = 0; second_node < num_nodes; ++second_node) { - if (first_node != second_node) { - g.add_edge(first_node, second_node, int(first_node + second_node)); - g_lazy.lazy_add_edge(first_node, second_node, int(first_node + second_node)); - } - } - } - g_lazy.sort_edges(); - g_lazy.remove_duplicate_edges(); - EXPECT_EQ(g.edges().size(), g_lazy.edges().size()); + mio::GraphBuilder builder(3, 3); + builder.add_node(0, 100); + builder.add_node(1, 100); + builder.add_node(2, 100); + builder.add_edge(0, 1, 100); + builder.add_edge(2, 1, 100); + builder.add_edge(1, 2, 100); + + auto g = builder.build(); + + EXPECT_EQ(g.nodes().size(), 3); + EXPECT_EQ(g.edges().size(), 3); +} - for (size_t index = 0; index < g.edges().size(); index++) { - EXPECT_EQ(g.edges()[index].start_node_idx, g_lazy.edges()[index].start_node_idx); - EXPECT_EQ(g.edges()[index].end_node_idx, g_lazy.edges()[index].end_node_idx); +TEST(TestGraphBuilder, Build_unique) +{ + mio::GraphBuilder builder; + builder.add_node(0, 100); + builder.add_node(1, 100); + builder.add_node(2, 100); + builder.add_edge(1, 2, 100); + builder.add_edge(0, 1, 100); + builder.add_edge(2, 1, 100); + builder.add_edge(1, 2, 200); + + auto g = builder.build(true); + + EXPECT_EQ(g.nodes().size(), 3); + EXPECT_EQ(g.edges().size(), 3); + for (const auto& e : g.edges()) { + EXPECT_EQ(e.property, 100); } } diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index d540a9dd62..122e6bb1a6 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -179,17 +179,32 @@ The following steps detail how to configure and execute a graph simulation: .. dropdown:: :fa:`gears` Working with large graphs - When working with very large graphs, i.e. starting from a few thousand edges, it will be faster to not use the standard ``add_edge`` function. This function always - keeps the list of edges inside the graph sorted and checks for duplicates. For large graphs, it is faster to first add all the edges to the graph - and then sort them and remove duplicates: + When working with very large graphs, i.e. starting from a few thousand edges, it will be faster to not use the standard ``add_edge`` function. + For this case, we provide a ``GraphBuilder``. There you can add all edges without any checks and the edges will be sorted when the graph is generated: .. code-block:: cpp - graph.reserve_edges(2); - graph.lazy_add_edge(0, 1, std::move(transition_rates)); - graph.lazy_add_edge(1, 0, std::move(transition_rates)); - graph.sort_edges(); - graph.remove_duplicate_edges(); + mio::GraphBuilder>>, mio::MobilityEdgeStochastic> builder; + builder.add_node(1001, model_group1, t0); + builder.add_node(1002, model_group2, to); + builder.add_edge(0, 1, std::move(transition_rates)); + builder.add_edge(1, 0, std::move(transition_rates)); + auto graph = builder.build(); + + + Usually, there should be no duplicate edges. If this is not certain, the ``GraphBuilder`` can also remove duplicates, based on the start and end node. + The parameters in the edge will not be compared. In this case it will only keep the first edge that was inserted: + + .. code-block:: cpp + + mio::GraphBuilder builder; + builder.add_node(1001, 100); + builder.add_node(1002, 100); + builder.add_edge(0, 1, 100); + builder.add_edge(1, 0, 100); + builder.add_edge(0, 1, 200); + auto graph = builder.build(true); + // graph contains the edges (0, 1, 100) and (1, 0, 100) 5. **Initialize and Advance the Mobility Simulation:** From 447a7ca2c4bb3812e0c84407312441614519e120 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:41:50 +0100 Subject: [PATCH 142/169] CHG: try fix bindigs --- .../bindings/mobility/metapopulation_mobility_instant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h b/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h index 361bfea983..146db880fd 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h +++ b/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h @@ -38,7 +38,7 @@ void bind_MobilityGraph(pybind11::module_& m, std::string const& name) .def( "add_node", [](G& self, int id, const typename Simulation::Model& p, double t0, double dt) -> auto& { - return self.add_node(id, p, t0, dt); + self.add_node(id, p, t0, dt); }, pybind11::arg("id"), pybind11::arg("model"), pybind11::arg("t0") = 0.0, pybind11::arg("dt") = 0.1, pybind11::return_value_policy::reference_internal) From 5f35220c4743bac84d25d32aebfeb38bb5201ed5 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:59:05 +0100 Subject: [PATCH 143/169] CHG: try fix bindings --- .../bindings/mobility/metapopulation_mobility_instant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h b/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h index 146db880fd..c35b586248 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h +++ b/pycode/memilio-simulation/memilio/simulation/bindings/mobility/metapopulation_mobility_instant.h @@ -37,7 +37,7 @@ void bind_MobilityGraph(pybind11::module_& m, std::string const& name) .def(pybind11::init<>()) .def( "add_node", - [](G& self, int id, const typename Simulation::Model& p, double t0, double dt) -> auto& { + [](G& self, int id, const typename Simulation::Model& p, double t0, double dt) -> void { self.add_node(id, p, t0, dt); }, pybind11::arg("id"), pybind11::arg("model"), pybind11::arg("t0") = 0.0, pybind11::arg("dt") = 0.1, From b08003b887844bda67c93fd838374e37ee478070 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:31:24 +0000 Subject: [PATCH 144/169] Apply suggestions from code review Co-authored-by: Henrik Zunker <69154294+HenrZu@users.noreply.github.com> --- cpp/memilio/mobility/graph.h | 2 -- cpp/memilio/utils/stl_util.h | 1 - docs/source/cpp/graph_metapop.rst | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 7b8b24d521..e286428cec 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -501,8 +501,6 @@ class GraphBuilder * Sorts the edges and optionally removes duplicate edges (same start and end node indices). * @param make_unique If true, duplicate edges are removed. The first added edge is kept! * @return Graph The constructed graph. - * @tparam NodeProperty The type of the node property. - * @tparam EdgeProperty The type of the edge property. */ Graph build(bool make_unique = false) { diff --git a/cpp/memilio/utils/stl_util.h b/cpp/memilio/utils/stl_util.h index 72a668eac0..5abc2791f3 100644 --- a/cpp/memilio/utils/stl_util.h +++ b/cpp/memilio/utils/stl_util.h @@ -62,7 +62,6 @@ inline std::ostream& set_ostream_format(std::ostream& out, size_t width, size_t * @param item item to insert * @param pred binary comparator, pred(item, a) returns true if item should go before element a, * pred(a, item) returns true if element a should go before item - * @return iterator to inserted or replaced item in vec */ template void insert_sorted_replace(std::vector& vec, T const& item, Pred pred) diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index 122e6bb1a6..fe032fd344 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -186,7 +186,7 @@ The following steps detail how to configure and execute a graph simulation: mio::GraphBuilder>>, mio::MobilityEdgeStochastic> builder; builder.add_node(1001, model_group1, t0); - builder.add_node(1002, model_group2, to); + builder.add_node(1002, model_group2, t0); builder.add_edge(0, 1, std::move(transition_rates)); builder.add_edge(1, 0, std::move(transition_rates)); auto graph = builder.build(); From be9571c26481ef4186876de141e7320c1ce32b1f Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:11:02 +0000 Subject: [PATCH 145/169] CHG: Add suggestions from code review --- cpp/memilio/mobility/graph.h | 16 ++++++++++++++-- docs/source/cpp/graph_metapop.rst | 9 +++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 7b8b24d521..48b25b0fd7 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -162,7 +162,10 @@ class Graph } /** - * @brief add a node to the graph. property of the node is constructed from arguments. + * @brief add a node to the graph. The property of the node is constructed from arguments. + * + * @param id id for the node + * @tparam args additional arguments for node construction */ template void add_node(int id, Args&&... args) @@ -171,7 +174,12 @@ class Graph } /** - * @brief add an edge to the graph. property of the edge is constructed from arguments. + * @brief add an edge to the graph. The property of the edge is constructed from arguments. + * @param start_node_idx id of start node + * @param end_node_idx id of end node + * @tparam args additional arguments for edge construction + * + * If an edge with the same start and end node indices already exists, it is replaced by the newly constructed edge. */ template void add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) @@ -499,6 +507,7 @@ class GraphBuilder * @brief Build the graph from the added nodes and edges. * * Sorts the edges and optionally removes duplicate edges (same start and end node indices). + * Wihout dupplicate removal, multiple edges between the same nodes are allowed and the order of insertion is stable. * @param make_unique If true, duplicate edges are removed. The first added edge is kept! * @return Graph The constructed graph. * @tparam NodeProperty The type of the node property. @@ -517,6 +526,9 @@ class GraphBuilder private: /** * @brief Sort the edge vector of a graph. + * + * Sorts the edges first by start node index, then by end node index. We use stable_sort to keep the order of insertion + * for edges with the same start and end node indices. */ void sort_edges() { diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index 122e6bb1a6..3d9aff5276 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -180,7 +180,8 @@ The following steps detail how to configure and execute a graph simulation: .. dropdown:: :fa:`gears` Working with large graphs When working with very large graphs, i.e. starting from a few thousand edges, it will be faster to not use the standard ``add_edge`` function. - For this case, we provide a ``GraphBuilder``. There you can add all edges without any checks and the edges will be sorted when the graph is generated: + For this case, we provide a ``GraphBuilder``. There you can add all edges without checking for uniqueness and sorting, thus improving the speed. + The edges will be sorted when the graph is generated: .. code-block:: cpp @@ -191,9 +192,9 @@ The following steps detail how to configure and execute a graph simulation: builder.add_edge(1, 0, std::move(transition_rates)); auto graph = builder.build(); - - Usually, there should be no duplicate edges. If this is not certain, the ``GraphBuilder`` can also remove duplicates, based on the start and end node. - The parameters in the edge will not be compared. In this case it will only keep the first edge that was inserted: + Usually, there should be no duplicate edges in your input. If this is not certain, the ``GraphBuilder`` can also remove duplicates. + Here, duplicate means that the start and end node are the same. The parameters in the edge will not be compared. + When duplicates are found, only the **first** inserted edge is kept, all subsequent edges are discarded: .. code-block:: cpp From 5c6fed49d91a0ce5cc2e46eceb9d339048877d10 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:43:24 +0000 Subject: [PATCH 146/169] CHG: Add table --- docs/source/cpp/graph_metapop.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index 3d9aff5276..171550b60c 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -192,6 +192,7 @@ The following steps detail how to configure and execute a graph simulation: builder.add_edge(1, 0, std::move(transition_rates)); auto graph = builder.build(); + Usually, there should be no duplicate edges in your input. If this is not certain, the ``GraphBuilder`` can also remove duplicates. Here, duplicate means that the start and end node are the same. The parameters in the edge will not be compared. When duplicates are found, only the **first** inserted edge is kept, all subsequent edges are discarded: @@ -207,6 +208,24 @@ The following steps detail how to configure and execute a graph simulation: auto graph = builder.build(true); // graph contains the edges (0, 1, 100) and (1, 0, 100) + To sum this up: + + .. list-table:: + :header-rows: 1 + + * - + - ``graph.add_edge`` + - ``GraphBuilder.add_edge`` + * - Sorting + - Always + - Only when graph is built + * - Duplicate deletion + - Always + - Only when activated on insertion + * - Keeps + - last duplicate + - first duplicate + 5. **Initialize and Advance the Mobility Simulation:** From c547df816a25195ed6eed10d331d81322020da8e Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:26:00 +0000 Subject: [PATCH 147/169] CHG: add new function for benchmarking --- cpp/memilio/mobility/graph.h | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 499514b579..100f4be663 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -553,6 +553,34 @@ class GraphBuilder m_edges = std::move(unique_edges); } + /** + * @brief Remove duplicate edges from a sorted edge vector. + * + * Copies all the unique edges to a new vector and replaces the original edge vector with it. Unique means that + * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the first + * edge in the vector is kept. + */ + void remove_double_edges() + { + std::vector> unique_edges; + unique_edges.reserve(m_edges.size()); + auto curr_elem = m_edges.begin(); + auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), is_equal); + while (next_elem != m_edges.end()) { + std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); + curr_elem = next_elem; + while (is_equal(*curr_elem, *next_elem) && next_elem != m_edges.end()) { + next_elem = next(next_elem); + } + curr_elem = prev(next_elem); + std::copy(curr_elem, next(curr_elem), std::back_inserter(unique_edges)); + curr_elem = next(curr_elem); + next_elem = std::adjacent_find(curr_elem, m_edges.end(), is_equal); + } + std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); + m_edges = std::move(unique_edges); + } + private: std::vector> m_nodes; std::vector> m_edges; From d367215611613ac696cb6b5eea2a68dfb1c8fa01 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:56:02 +0100 Subject: [PATCH 148/169] generalize Region and State --- cpp/examples/smm.cpp | 85 +++++------ cpp/memilio/epidemiology/adoption_rate.h | 40 ++---- cpp/memilio/epidemiology/populations.h | 6 + cpp/memilio/utils/index.h | 159 +++++++++++++++------ cpp/models/hybrid/conversion_functions.cpp | 4 +- cpp/models/hybrid/conversion_functions.h | 7 +- cpp/models/smm/model.h | 80 ++++------- cpp/models/smm/parameters.h | 22 ++- cpp/models/smm/simulation.h | 64 +++------ cpp/tests/test_smm_model.cpp | 147 +++++++++---------- cpp/tests/test_temporal_hybrid_model.cpp | 17 +-- 11 files changed, 311 insertions(+), 320 deletions(-) diff --git a/cpp/examples/smm.cpp b/cpp/examples/smm.cpp index cf13a0c7be..89dd7185b3 100644 --- a/cpp/examples/smm.cpp +++ b/cpp/examples/smm.cpp @@ -18,12 +18,12 @@ * limitations under the License. */ +#include "memilio/config.h" #include "memilio/epidemiology/age_group.h" #include "smm/simulation.h" #include "smm/model.h" #include "smm/parameters.h" #include "memilio/data/analyze_result.h" -#include "memilio/epidemiology/adoption_rate.h" enum class InfectionState { @@ -36,78 +36,71 @@ enum class InfectionState Count }; -using Age = mio::AgeGroup; -using Species = mio::AgeGroup; +struct Species : public mio::Index { + Species(size_t val) + : Index(val) + { + } +}; int main() { + using Age = mio::AgeGroup; + using Status = mio::Index; + using mio::regions::Region; + using enum InfectionState; //Example how to run the stochastic metapopulation models with four regions const size_t num_regions = 4; const size_t num_age_groups = 1; - const size_t num_groups = 1; - using Model = mio::smm::Model; + const size_t num_species = 1; + using Model = mio::smm::Model; ScalarType numE = 12, numC = 4, numI = 12, numR = 0, numD = 0; - Model model; + Model model(Status{Count, Age(num_age_groups), Species(num_species)}, Region(num_regions)); //Population are distributed uniformly to the four regions for (size_t r = 0; r < num_regions; ++r) { - model.populations[{mio::regions::Region(r), InfectionState::S, Age(0), Species(0)}] = - (1000 - numE - numC - numI - numR - numD) / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::E, Age(0), Species(0)}] = numE / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::C, Age(0), Species(0)}] = numC / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::I, Age(0), Species(0)}] = numI / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::R, Age(0), Species(0)}] = numR / num_regions; - model.populations[{mio::regions::Region(r), InfectionState::D, Age(0), Species(0)}] = numD / num_regions; + model.populations[{Region(r), S, Age(0), Species(0)}] = (1000 - numE - numC - numI - numR - numD) / num_regions; + model.populations[{Region(r), E, Age(0), Species(0)}] = numE / num_regions; + model.populations[{Region(r), C, Age(0), Species(0)}] = numC / num_regions; + model.populations[{Region(r), I, Age(0), Species(0)}] = numI / num_regions; + model.populations[{Region(r), R, Age(0), Species(0)}] = numR / num_regions; + model.populations[{Region(r), D, Age(0), Species(0)}] = numD / num_regions; } + using AR = mio::smm::AdoptionRates; + using TR = mio::smm::TransitionRates; + //Set infection state adoption and spatial transition rates - std::vector> adoption_rates; - std::vector> transition_rates; + AR::Type adoption_rates; + TR::Type transition_rates; for (size_t r = 0; r < num_regions; ++r) { - adoption_rates.push_back({InfectionState::S, - InfectionState::E, - mio::regions::Region(r), + adoption_rates.push_back({{S, Age(0), Species(0)}, + {E, Age(0), Species(0)}, + Region(r), 0.1, - {{InfectionState::C, 1, mio::regions::Region(3), {Age(0), Species(0)}}, - {InfectionState::I, 0.5, mio::regions::Region(1), {Age(0), Species(0)}}}, - {Age(0), Species(0)}}); - adoption_rates.push_back( - {InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}, {Age(0), Species(0)}}); - adoption_rates.push_back( - {InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}, {Age(0), Species(0)}}); - adoption_rates.push_back( - {InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}, {Age(0), Species(0)}}); - adoption_rates.push_back( - {InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}, {Age(0), Species(0)}}); - adoption_rates.push_back( - {InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}, {Age(0), Species(0)}}); + {{{C, Age(0), Species(0)}, 1}, {{I, Age(0), Species(0)}, 0.5}}}); + adoption_rates.push_back({{C, Age(0), Species(0)}, {R, Age(0), Species(0)}, Region(r), 0.2 / 3., {}}); + adoption_rates.push_back({{E, Age(0), Species(0)}, {C, Age(0), Species(0)}, Region(r), 1.0 / 5., {}}); + adoption_rates.push_back({{C, Age(0), Species(0)}, {I, Age(0), Species(0)}, Region(r), 0.8 / 3., {}}); + adoption_rates.push_back({{I, Age(0), Species(0)}, {R, Age(0), Species(0)}, Region(r), 0.99 / 5., {}}); + adoption_rates.push_back({{I, Age(0), Species(0)}, {D, Age(0), Species(0)}, Region(r), 0.01 / 5., {}}); } //Agents in infection state D do not transition - for (size_t s = 0; s < static_cast(InfectionState::D); ++s) { + for (size_t s = 0; s < static_cast(D); ++s) { for (size_t i = 0; i < num_regions; ++i) { for (size_t j = 0; j < num_regions; ++j) if (i != j) { - transition_rates.push_back({InfectionState(s), - mio::regions::Region(i), - mio::regions::Region(j), - 0.01, - {Age(0), Species(0)}, - {Age(0), Species(0)}}); - transition_rates.push_back({InfectionState(s), - mio::regions::Region(j), - mio::regions::Region(i), - 0.01, - {Age(0), Species(0)}, - {Age(0), Species(0)}}); + transition_rates.push_back({{InfectionState(s), Age(0), Species(0)}, Region(i), Region(j), 0.01}); + transition_rates.push_back({{InfectionState(s), Age(0), Species(0)}, Region(j), Region(i), 0.01}); } } } - model.parameters.get>() = adoption_rates; - model.parameters.get>() = transition_rates; + model.parameters.get() = adoption_rates; + model.parameters.get() = transition_rates; ScalarType dt = 0.1; ScalarType tmax = 30.0; diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index f1a89a8d2c..5a2a953ca1 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -20,16 +20,23 @@ #ifndef MIO_EPI_ADOPTIONRATE_H #define MIO_EPI_ADOPTIONRATE_H -#include "memilio/utils/index.h" -#include "memilio/config.h" #include "memilio/geography/regions.h" -#include -#include -#include +#include namespace mio { +/** + * @brief Struct defining an influence for a second-order adoption. + * The population having "status" is multiplied with "factor." + * @tparam Status An infection state enum. + */ +template +struct Influence { + Status status; + FP factor; +}; + /** * @brief Struct defining a possible status adoption in a Model based on Poisson Processes. * The AdoptionRate is considered to be of second-order if there are any "influences". @@ -38,30 +45,13 @@ namespace mio * @tparam Status An infection state enum. * @tparam Groups Additional grouping indices. */ -template +template struct AdoptionRate { - - /** - * @brief Struct defining an influence for a second-order adoption. - * The population having "status" is multiplied with "factor." - * @tparam status An infection state enum. - * @tparam factor Scaling factor for the influence. - * @tparam - * @tparam Groups Additional grouping indices. - */ - struct Influence { - Status status; - FP factor; - std::optional region = std::nullopt; - std::tuple group_indices{}; - }; - Status from; // i Status to; // j - mio::regions::Region region; // k + Region region; // k FP factor; // gammahat_{ij}^k - std::vector influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} ) - std::tuple group_indices{}; + std::vector> influences; // influences[tau] = ( Psi_{i,j,tau} , gamma_{i,j,tau} )}; }; } // namespace mio diff --git a/cpp/memilio/epidemiology/populations.h b/cpp/memilio/epidemiology/populations.h index f2da6092ad..24246b4471 100644 --- a/cpp/memilio/epidemiology/populations.h +++ b/cpp/memilio/epidemiology/populations.h @@ -287,6 +287,12 @@ class Populations : public CustomIndexArray, Categories...> } }; +template +class Populations> : public Populations +{ + using Populations::Populations; +}; + } // namespace mio #endif // MIO_EPI_POPULATIONS_H diff --git a/cpp/memilio/utils/index.h b/cpp/memilio/utils/index.h index cba56b1d57..70c5f75d38 100644 --- a/cpp/memilio/utils/index.h +++ b/cpp/memilio/utils/index.h @@ -20,15 +20,69 @@ #ifndef INDEX_H #define INDEX_H +#include "memilio/io/io.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/type_safe.h" +#include +#include +#include +#include + namespace mio { template class Index; +namespace details +{ + +/// @brief Function definition that accepts a MultiIndex, used for the definition of IsMultiIndex. +template +void is_multi_index_impl(Index); + +} // namespace details + +/// @brief A MultiIndex is an Index any number of categories. Does accept empty or single category indices. +template +concept IsMultiIndex = requires(Index i) { details::is_multi_index_impl(i); }; + +namespace details +{ + +/// @brief Obtain a tuple of singular indices from a Index or MultiIndex. +template +std::tuple...> get_tuple(const Index& i) +{ + if constexpr (sizeof...(Tags) == 1) { + return std::tuple(i); + } + else { + return i.indices; + } +} + +/// @brief Obtain a tuple of one index from an enum value. +template +std::tuple> get_tuple(Enum i) + requires std::is_enum::value +{ + return std::tuple(Index(i)); +} + +template + requires((std::is_enum_v || IsMultiIndex) && ...) +decltype(auto) merge_indices_impl(IndexArgs&&... args) +{ + return std::tuple_cat(details::get_tuple(args)...); +} + +template +Index tuple_to_index(std::tuple...>); + +} // namespace details + /** * @brief An Index with a single template parameter is a typesafe wrapper for size_t * that is associated with a Tag. It is used to index into a CustomIndexArray @@ -72,8 +126,8 @@ class MEMILIO_ENABLE_EBO Index : public TypeSafe::value, void>* = nullptr> - Index(Dummy val) + Index(CategoryTag val) + requires std::is_enum_v : TypeSafe>((size_t)val) { } @@ -132,6 +186,13 @@ class Index { } + template + requires(sizeof...(IndexArgs) > 1) + Index(IndexArgs&&... subindices) + : indices(details::merge_indices_impl(std::forward(subindices)...)) + { + } + private: Index(const std::tuple...>& _indices) : indices(_indices) @@ -197,70 +258,69 @@ struct index_of_type, Index> { static constexpr std::size_t value = 0; }; -// retrieves the Index at the Ith position for a Index with more than one Tag -template 1), void>* = nullptr> +// retrieves the Index at the Ith position for a Index +template constexpr typename std::tuple_element...>>::type& get(Index& i) noexcept { - return std::get(i.indices); -} - -// retrieves the Index at the Ith position for a Index with one Tag, equals identity function -template * = nullptr> -constexpr typename std::tuple_element...>>::type& -get(Index& i) noexcept -{ - static_assert(I == 0, "I must be equal to zero for an Index with just one template parameter"); - return i; -} - -// retrieves the Index at the Ith position for a Index with more than one Tag const version -template 1), void>* = nullptr> -constexpr typename std::tuple_element...>>::type const& -get(Index const& i) noexcept -{ - return std::get(i.indices); + if constexpr (sizeof...(CategoryTags) == 1) { + static_assert(I == 0, "I must be equal to zero for an Index with just one template parameter"); + return i; + } + else { + return std::get(i.indices); + } } -// retrieves the Index at the Ith position for a Index with one Tag, equals identity function const version -template * = nullptr> +// retrieves the Index at the Ith position for a Index, const version +template constexpr typename std::tuple_element...>>::type const& get(Index const& i) noexcept { - static_assert(I == 0, "I must be equal to zero for an Index with just one template parameter"); - return i; + if constexpr (sizeof...(CategoryTags) == 1) { + static_assert(I == 0, "I must be equal to zero for an Index with just one template parameter"); + return i; + } + else { + return std::get(i.indices); + } } // retrieves the Index for the tag Tag of a Index with more than one Tag -template 1), void>* = nullptr> +template constexpr Index& get(Index& i) noexcept { - return std::get>(i.indices); -} - -// retrieves the Index for the tag Tag of a Index with one Tag, equals identity function -template * = nullptr> -constexpr Index& get(Index& i) noexcept -{ - using IndexTag = std::tuple_element_t<0, std::tuple>; - static_assert(std::is_same::value, "Tags must match for an Index with just one template parameter"); - return i; + if constexpr (sizeof...(CategoryTags) == 1) { + using IndexTag = std::tuple_element_t<0, std::tuple>; + static_assert(std::is_same::value, + "Tags must match for an Index with just one template parameter"); + return i; + } + else { + return std::get>(i.indices); + } } -// retrieves the Index for the tag Tag for a Index with more than one Tag const version -template 1), void>* = nullptr> +// retrieves the Index for the tag Tag of a Index with more than one Tag +template constexpr Index const& get(Index const& i) noexcept { - return std::get>(i.indices); + if constexpr (sizeof...(CategoryTags) == 1) { + using IndexTag = std::tuple_element_t<0, std::tuple>; + static_assert(std::is_same::value, + "Tags must match for an Index with just one template parameter"); + return i; + } + else { + return std::get>(i.indices); + } } -// retrieves the Index for the tag Tag for a Index with one Tag, equals identity function const version -template * = nullptr> -constexpr Index const& get(Index const& i) noexcept +template +decltype(auto) merge_indices(IndexArgs&&... args) { - using IndexTag = std::tuple_element_t<0, std::tuple>; - static_assert(std::is_same::value, "Tags must match for an Index with just one template parameter"); - return i; + using MergedIndex = decltype(details::tuple_to_index(details::merge_indices_impl(std::declval()...))); + return MergedIndex(std::forward(args)...); } namespace details @@ -317,6 +377,13 @@ SubIndex reduce_index(const SuperIndex& index) return details::reduce_index_impl(index, mio::Tag{}); } +template + requires std::is_enum_v +Index reduce_index(const SuperIndex& index) +{ + return details::reduce_index_impl(index, mio::Tag>{}); +} + /** * @brief Create a SuperIndex by copying values from SubIndex, filling new categories with fill_value. * If a type T is contained multiple times in SubIndex, only the first occurance of T is used. diff --git a/cpp/models/hybrid/conversion_functions.cpp b/cpp/models/hybrid/conversion_functions.cpp index 8fa4b047b4..7fa8d4831a 100644 --- a/cpp/models/hybrid/conversion_functions.cpp +++ b/cpp/models/hybrid/conversion_functions.cpp @@ -36,7 +36,7 @@ namespace hybrid { template <> void convert_model(const dabm::Simulation>& current_model, - smm::Simulation& target_model) + smm::Simulation& target_model) { auto& current_result = current_model.get_result(); auto& target_result = target_model.get_result(); @@ -58,7 +58,7 @@ void convert_model(const dabm::Simulation -void convert_model(const smm::Simulation& current_model, +void convert_model(const smm::Simulation& current_model, dabm::Simulation>& target_model) { auto& current_result = current_model.get_result(); diff --git a/cpp/models/hybrid/conversion_functions.h b/cpp/models/hybrid/conversion_functions.h index 9d0e316543..2d15f6d638 100644 --- a/cpp/models/hybrid/conversion_functions.h +++ b/cpp/models/hybrid/conversion_functions.h @@ -37,11 +37,12 @@ namespace hybrid template <> void convert_model(const dabm::Simulation>& current_model, - smm::Simulation& target_model); + smm::Simulation>& target_model); template <> -void convert_model(const smm::Simulation& current_model, - dabm::Simulation>& target_model); +void convert_model( + const smm::Simulation>& current_model, + dabm::Simulation>& target_model); template <> void convert_model(const dabm::Simulation>& current_model, diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index b5942bc251..cd6be1577d 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -21,12 +21,13 @@ #ifndef MIO_SMM_MODEL_H #define MIO_SMM_MODEL_H -#include "memilio/config.h" -#include "memilio/epidemiology/age_group.h" +#include "memilio/utils/index.h" +#include "memilio/utils/index_range.h" #include "smm/parameters.h" #include "memilio/compartments/compartmental_model.h" #include "memilio/epidemiology/populations.h" #include "memilio/geography/regions.h" +#include namespace mio { @@ -36,26 +37,27 @@ namespace smm template using age_group = T; +template +using PopulationIndex = decltype(merge_indices(std::declval(), std::declval())); + /** * @brief Stochastic Metapopulation Model. * @tparam regions Number of regions. * @tparam Status An infection state enum. */ -template -class Model : public mio::CompartmentalModel< - FP, Status, mio::Populations...>, - ParametersBase...>> +template +class Model : public mio::CompartmentalModel>, + ParametersBase> { - using Base = - mio::CompartmentalModel...>, - ParametersBase...>>; - using Index = mio::Index...>; + using Base = mio::CompartmentalModel>, + ParametersBase>; public: - Model() - : Base(typename Base::Populations( - {static_cast(regions), Status::Count, static_cast(Groups)...}), + using Status = StatusT; + using Region = RegionT; + + Model(Status status_dimensions, Region region_dimensions) + : Base(typename Base::Populations(merge_indices(region_dimensions, status_dimensions)), typename Base::ParameterSet()) { } @@ -66,46 +68,26 @@ class Model : public mio::CompartmentalModel< * @param[in] x The current state of the model. * @return Current value of the adoption rate. */ - FP evaluate(const AdoptionRate...>& rate, - const Eigen::VectorXd& x) const + FP evaluate(const AdoptionRate& rate, const Eigen::VectorXd& x) const { - const auto& pop = this->populations; - const auto index_from = std::apply( - [&](auto&&... args) { - return Index{rate.region, rate.from, std::forward(args)...}; - }, - rate.group_indices); - const auto source = pop.get_flat_index(index_from); // Why is here rate.from used? KV + const auto& pop = this->populations; + const auto source = pop.get_flat_index({rate.region, rate.from}); // determine order and calculate rate if (rate.influences.size() == 0) { // first order adoption return rate.factor * x[source]; } else { // second order adoption + FP N = 0; + for (auto status : make_index_range(reduce_index(this->populations.size()))) { + N += x[pop.get_flat_index({rate.region, status})]; + } // accumulate influences FP influences = 0.0; for (size_t i = 0; i < rate.influences.size(); i++) { - FP N = 0; // Welches N brauchen wir hier?? - - for (size_t s = 0; s < static_cast(Status::Count); ++s) { - const auto index = std::apply( - [&](auto&&... args) { - return Index{rate.influences[i].region.value_or(rate.region), Status(s), - std::forward(args)...}; - }, - rate.influences[i].group_indices); - N += x[pop.get_flat_index(index)]; - } - const auto index = std::apply( - [&](auto&&... args) { - return Index{rate.influences[i].region.value_or(rate.region), rate.influences[i].status, - std::forward(args)...}; - }, - rate.influences[i].group_indices); - if (N > 0) { - influences += rate.influences[i].factor * x[pop.get_flat_index(index)] / N; - } + influences += + rate.influences[i].factor * x[pop.get_flat_index({rate.region, rate.influences[i].status})]; } - return rate.factor * x[source] * influences; + return (N > 0) ? (rate.factor * x[source] * influences / N) : 0; } } @@ -115,15 +97,9 @@ class Model : public mio::CompartmentalModel< * @param[in] x The current state of the model. * @return Current value of the transition rate. */ - FP evaluate(const TransitionRate...>& rate, - const Eigen::VectorXd& x) const + FP evaluate(const TransitionRate& rate, const Eigen::VectorXd& x) const { - auto index = std::apply( - [&](auto&&... args) { - return Index{rate.from, rate.status, std::forward(args)...}; - }, - rate.group_indices_from); - const auto source = this->populations.get_flat_index(index); + const auto source = this->populations.get_flat_index({rate.from, rate.status}); return rate.factor * x[source]; } diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index fe27c35526..b44b63e827 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -25,7 +25,6 @@ #include "memilio/geography/regions.h" #include "memilio/utils/parameter_set.h" #include "memilio/epidemiology/adoption_rate.h" -#include namespace mio { @@ -36,9 +35,9 @@ namespace smm * @brief A vector of AdoptionRate%s, see mio::AdoptionRate * @tparam Status An infection state enum. */ -template +template struct AdoptionRates { - using Type = std::vector>; + using Type = std::vector>; const static std::string name() { return "AdoptionRates"; @@ -49,26 +48,25 @@ struct AdoptionRates { * @brief Struct defining a possible regional transition in a Model based on Poisson Processes. * @tparam Status An infection state enum. */ -template +template struct TransitionRate { Status status; // i - mio::regions::Region from; // k - mio::regions::Region to; // l + Region from; // k + Region to; // l FP factor; // lambda_i^{kl} - std::tuple group_indices_from{}; - std::tuple group_indices_to{}; }; -template + +template struct TransitionRates { - using Type = std::vector>; + using Type = std::vector>; const static std::string name() { return "TransitionRates"; } }; -template -using ParametersBase = mio::ParameterSet, TransitionRates>; +template +using ParametersBase = mio::ParameterSet, TransitionRates>; } // namespace smm diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 53096ef1e1..3f7f922b25 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -21,14 +21,9 @@ #ifndef MIO_SMM_SIMULATION_H #define MIO_SMM_SIMULATION_H -#include "memilio/config.h" -#include "memilio/epidemiology/age_group.h" -#include "memilio/utils/index.h" -#include "memilio/utils/logging.h" #include "smm/model.h" #include "smm/parameters.h" -#include "memilio/compartments/simulation.h" -#include +#include "memilio/utils/time_series.h" namespace mio { @@ -41,12 +36,12 @@ namespace smm * @tparam regions The number of regions. * @tparam Status An infection state enum. */ -template +template class Simulation { public: - using Model = smm::Model; - using Index = mio::Index...>; + using Model = smm::Model; + using Index = typename Model::Populations::Index; /** * @brief Set up the simulation for a Stochastic Metapopulation Model. @@ -104,38 +99,18 @@ class Simulation if (next_event < adoption_rates().size()) { // perform adoption event const auto& rate = adoption_rates()[next_event]; - auto index_from = std::apply( - [&](auto&&... args) { - return Index{rate.region, rate.from, std::forward(args)...}; - }, - rate.group_indices); - m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; - m_model->populations[index_from] -= 1; - auto index_to = std::apply( - [&](auto&&... args) { - return Index{rate.region, rate.to, std::forward(args)...}; - }, - rate.group_indices); - m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; - m_model->populations[index_to] += 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.from})] -= 1; + m_model->populations[{rate.region, rate.from}] -= 1.0; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.region, rate.to})] += 1; + m_model->populations[{rate.region, rate.to}] += 1.0; } else { // perform transition event const auto& rate = transition_rates()[next_event - adoption_rates().size()]; - auto index_from = std::apply( - [&](auto&&... args) { - return Index{rate.from, rate.status, std::forward(args)...}; - }, - rate.group_indices_from); - m_result.get_last_value()[m_model->populations.get_flat_index(index_from)] -= 1; - m_model->populations[index_from] -= 1; - auto index_to = std::apply( - [&](auto&&... args) { - return Index{rate.to, rate.status, std::forward(args)...}; - }, - rate.group_indices_to); - m_result.get_last_value()[m_model->populations.get_flat_index(index_to)] += 1; - m_model->populations[index_to] += 1; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.from, rate.status})] -= 1; + m_model->populations[{rate.from, rate.status}] -= 1.0; + m_result.get_last_value()[m_model->populations.get_flat_index({rate.to, rate.status})] += 1; + m_model->populations[{rate.to, rate.status}] += 1.0; } // update internal times for (size_t i = 0; i < m_internal_time.size(); i++) { @@ -157,7 +132,6 @@ class Simulation } } return m_result.get_last_value(); - // } } /** @@ -189,25 +163,21 @@ class Simulation /** * @brief Returns the model's transition rates. */ - inline constexpr const typename smm::TransitionRates...>::Type& - transition_rates() + inline constexpr const typename smm::TransitionRates::Type& transition_rates() { - return m_model->parameters - .template get...>>(); + return m_model->parameters.template get>(); } /** * @brief Returns the model's adoption rates. */ - inline constexpr const typename smm::AdoptionRates...>::Type& - adoption_rates() + inline constexpr const typename smm::AdoptionRates::Type& adoption_rates() { - return m_model->parameters.template get...>>(); + return m_model->parameters.template get>(); } /** - * @brief - * + * @brief Calculate current values for m_current_rates and m_waiting_times. */ inline void update_current_rates_and_waiting_times() { diff --git a/cpp/tests/test_smm_model.cpp b/cpp/tests/test_smm_model.cpp index 9e7be226f4..56386c73d0 100644 --- a/cpp/tests/test_smm_model.cpp +++ b/cpp/tests/test_smm_model.cpp @@ -18,20 +18,17 @@ * limitations under the License. */ -#include -#include -#include -#include -#include +#include "abm_helpers.h" +#include "memilio/epidemiology/adoption_rate.h" #include "memilio/epidemiology/age_group.h" -#include "memilio/utils/compiler_diagnostics.h" -#include "memilio/utils/logging.h" +#include "memilio/geography/regions.h" #include "smm/model.h" #include "smm/parameters.h" #include "smm/simulation.h" -#include "abm_helpers.h" -#include "memilio/epidemiology/adoption_rate.h" -#include + +#include + +#include #include #include @@ -63,9 +60,9 @@ TEST(TestSMM, evaluateAdoptionRate) // with N(from, region) the population in Region "region" having infection state "from" //Second-order adoption rates are given by: // rate.factor * N(rate.from, rate.region)/total_pop * sum (over all rate.influences) influence.factor * N(influence.status, rate.region) - using Model = mio::smm::Model; + using Model = mio::smm::Model; - Model model; + Model model(InfectionState::Count, mio::regions::Region(1)); //Set adoption rates std::vector> adoption_rates; @@ -92,37 +89,38 @@ TEST(TestSMM, evaluateAdoptionRate) TEST(TestSMM, evaluateinterregionalAdoptionRate) { - using Model = mio::smm::Model; - using Age = mio::AgeGroup; + using Age = mio::AgeGroup; + using mio::regions::Region; + using Status = mio::Index; + using Model = mio::smm::Model; - std::vector> adoption_rates; + std::vector> adoption_rates; //Second-order adoption - adoption_rates.push_back({Infections::S, - Infections::I, + adoption_rates.push_back({{Infections::S, Age(0), Region(0)}, + {Infections::I, Age(0), Region(0)}, mio::regions::Region(0), 0.1, - {{Infections::I, 0.1, mio::regions::Region(0), {Age(1)}}, - {Infections::I, 0.2, mio::regions::Region(1), {Age(1)}}}, - Age(0)}); + {{{Infections::I, Age(1), Region(0)}, 0.1}, {{Infections::I, Age(1), Region(1)}, 0.2}}}); //First-order adoption - adoption_rates.push_back({Infections::I, Infections::R, mio::regions::Region(0), 0.2, {}, {Age(1)}}); - Model model; + adoption_rates.push_back( + {{Infections::I, Age(1), Region(0)}, {Infections::R, Age(1), Region(0)}, Region(0), 0.2, {}}); + Model model({Infections::Count, Age(2), Region(2)}, Region(1)); - model.populations[{mio::regions::Region(0), Infections::S, Age(0)}] = 50; - model.populations[{mio::regions::Region(0), Infections::I, Age(0)}] = 10; - model.populations[{mio::regions::Region(0), Infections::R, Age(0)}] = 0; + model.populations[{Region(0), Infections::S, Age(0), Region(0)}] = 50; + model.populations[{Region(0), Infections::I, Age(0), Region(0)}] = 10; + model.populations[{Region(0), Infections::R, Age(0), Region(0)}] = 0; - model.populations[{mio::regions::Region(0), Infections::S, Age(1)}] = 100; - model.populations[{mio::regions::Region(0), Infections::I, Age(1)}] = 20; - model.populations[{mio::regions::Region(0), Infections::R, Age(1)}] = 0; + model.populations[{Region(0), Infections::S, Age(1), Region(0)}] = 100; + model.populations[{Region(0), Infections::I, Age(1), Region(0)}] = 20; + model.populations[{Region(0), Infections::R, Age(1), Region(0)}] = 0; - model.populations[{mio::regions::Region(1), Infections::S, Age(0)}] = 40; - model.populations[{mio::regions::Region(1), Infections::I, Age(0)}] = 80; - model.populations[{mio::regions::Region(1), Infections::R, Age(0)}] = 0; + model.populations[{Region(0), Infections::S, Age(0), Region(1)}] = 40; + model.populations[{Region(0), Infections::I, Age(0), Region(1)}] = 80; + model.populations[{Region(0), Infections::R, Age(0), Region(1)}] = 0; - model.populations[{mio::regions::Region(1), Infections::S, Age(1)}] = 80; - model.populations[{mio::regions::Region(1), Infections::I, Age(1)}] = 16; - model.populations[{mio::regions::Region(1), Infections::R, Age(1)}] = 0; + model.populations[{Region(0), Infections::S, Age(1), Region(1)}] = 80; + model.populations[{Region(0), Infections::I, Age(1), Region(1)}] = 16; + model.populations[{Region(0), Infections::R, Age(1), Region(1)}] = 0; EXPECT_DOUBLE_EQ(model.evaluate(adoption_rates[0], model.populations.get_compartments()), 0.1 * 50. * (0.1 * 20. * 1. / 120. + 0.2 * 16 * 1. / 96.)); @@ -133,37 +131,27 @@ TEST(TestSMM, evaluateTransitionRate) { //Same test as 'evaluateAdoptionRate' only for spatial transition rates. //Transition rates are given by: rate.factor * N(rate.status, rate.from) - using Model = mio::smm::Model; + using Model = mio::smm::Model; - Model model; + Model model(InfectionState::Count, mio::regions::Region(2)); //Initialize model populations - model.populations[{mio::regions::Region(0), InfectionState::S, mio::AgeGroup(0)}] = 50; - model.populations[{mio::regions::Region(0), InfectionState::E, mio::AgeGroup(0)}] = 10; - model.populations[{mio::regions::Region(0), InfectionState::C, mio::AgeGroup(0)}] = 5; - model.populations[{mio::regions::Region(0), InfectionState::I, mio::AgeGroup(0)}] = 0; - model.populations[{mio::regions::Region(0), InfectionState::R, mio::AgeGroup(0)}] = 0; - model.populations[{mio::regions::Region(0), InfectionState::D, mio::AgeGroup(0)}] = 0; - - model.populations[{mio::regions::Region(1), InfectionState::S, mio::AgeGroup(0)}] = 55; - model.populations[{mio::regions::Region(1), InfectionState::E, mio::AgeGroup(0)}] = 10; - model.populations[{mio::regions::Region(1), InfectionState::C, mio::AgeGroup(0)}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::I, mio::AgeGroup(0)}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::R, mio::AgeGroup(0)}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::D, mio::AgeGroup(0)}] = 0; + model.populations[{mio::regions::Region(0), InfectionState::S}] = 50; + model.populations[{mio::regions::Region(0), InfectionState::E}] = 10; + model.populations[{mio::regions::Region(0), InfectionState::C}] = 5; + model.populations[{mio::regions::Region(0), InfectionState::I}] = 0; + model.populations[{mio::regions::Region(0), InfectionState::R}] = 0; + model.populations[{mio::regions::Region(0), InfectionState::D}] = 0; + + model.populations[{mio::regions::Region(1), InfectionState::S}] = 55; + model.populations[{mio::regions::Region(1), InfectionState::E}] = 10; + model.populations[{mio::regions::Region(1), InfectionState::C}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::I}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::R}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::D}] = 0; //Set transition rates - std::vector> transition_rates; - transition_rates.push_back({InfectionState::S, - mio::regions::Region(0), - mio::regions::Region(1), - 0.01, - {mio::AgeGroup(0)}, - {mio::AgeGroup(0)}}); - transition_rates.push_back({InfectionState::E, - mio::regions::Region(1), - mio::regions::Region(0), - 0.1, - {mio::AgeGroup(0)}, - {mio::AgeGroup(0)}}); + std::vector> transition_rates; + transition_rates.push_back({InfectionState::S, mio::regions::Region(0), mio::regions::Region(1), 0.01}); + transition_rates.push_back({InfectionState::E, mio::regions::Region(1), mio::regions::Region(0), 0.1}); EXPECT_EQ(model.evaluate(transition_rates[0], model.populations.get_compartments()), 0.5); EXPECT_EQ(model.evaluate(transition_rates[1], model.populations.get_compartments()), 1.); @@ -173,9 +161,9 @@ TEST(TestSMMSimulation, advance) { //Test whether Gillespie algorithm calculates events in the correct order using testing::Return; - using Model = mio::smm::Model; + using Model = mio::smm::Model; - Model model; + Model model(InfectionState::Count, mio::regions::Region(2)); //Initialize model populations model.populations[{mio::regions::Region(0), InfectionState::S}] = 1; model.populations[{mio::regions::Region(0), InfectionState::E}] = 0; @@ -210,8 +198,8 @@ TEST(TestSMMSimulation, advance) //Spatial transition transition_rates.push_back({InfectionState::R, mio::regions::Region(1), mio::regions::Region(0), 0.01}); - model.parameters.get>() = adoption_rates; - model.parameters.get>() = transition_rates; + model.parameters.get>() = adoption_rates; + model.parameters.get>() = transition_rates; //Mock exponential distribution to control the normalized waiting times that are drawn ScopedMockDistribution>>> @@ -254,9 +242,9 @@ TEST(TestSMMSimulation, stopsAtTmax) { //Test whether simulation stops at tmax and whether system state at tmax is saved if there are no adoptions/transitions using testing::Return; - using Model = mio::smm::Model; + using Model = mio::smm::Model; - Model model; + Model model(InfectionState::Count, mio::regions::Region(2)); //Set adoption and spatial transition rates std::vector> adoption_rates; @@ -270,8 +258,8 @@ TEST(TestSMMSimulation, stopsAtTmax) transition_rates.push_back({InfectionState::R, mio::regions::Region(1), mio::regions::Region(0), 0.01}); - model.parameters.get>() = adoption_rates; - model.parameters.get>() = transition_rates; + model.parameters.get>() = adoption_rates; + model.parameters.get>() = transition_rates; //As populations are not set they have value 0 i.e. no events will happen //Simulate for 30 days @@ -287,7 +275,7 @@ TEST(TestSMMSimulation, convergence) //Test whether the mean number of transitions in one unit-time step corresponds to the expected number of transitions // given by rate * pop up to some tolerance using testing::Return; - using Model = mio::smm::Model; + using Model = mio::smm::Model; double pop = 1000; size_t num_runs = 100; @@ -302,7 +290,7 @@ TEST(TestSMMSimulation, convergence) //Do 100 unit-time step simulations with 1000 agents for (size_t n = 0; n < num_runs; ++n) { - Model model; + Model model(InfectionState::Count, mio::regions::Region(2)); model.populations[{mio::regions::Region(0), InfectionState::S}] = pop; model.populations[{mio::regions::Region(0), InfectionState::E}] = 0; @@ -311,13 +299,14 @@ TEST(TestSMMSimulation, convergence) model.populations[{mio::regions::Region(0), InfectionState::R}] = 0; model.populations[{mio::regions::Region(0), InfectionState::D}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::S}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::E}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::C}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::I}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::R}] = 0; - model.populations[{mio::regions::Region(1), InfectionState::D}] = 0; - model.parameters.get>() = transition_rates; + model.populations[{mio::regions::Region(1), InfectionState::S}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::E}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::C}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::I}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::R}] = 0; + model.populations[{mio::regions::Region(1), InfectionState::D}] = 0; + model.parameters.get>() = + transition_rates; auto sim = mio::smm::Simulation(model, 0.0, 1.0); sim.advance(1.); diff --git a/cpp/tests/test_temporal_hybrid_model.cpp b/cpp/tests/test_temporal_hybrid_model.cpp index 0ae31660ec..5f0ff64891 100644 --- a/cpp/tests/test_temporal_hybrid_model.cpp +++ b/cpp/tests/test_temporal_hybrid_model.cpp @@ -151,7 +151,7 @@ TEST(TestTemporalHybrid, test_advance) TEST(TestTemporalHybrid, test_conversion_dabm_smm) { using Model1 = mio::dabm::Model>; - using Model2 = mio::smm::Model; + using Model2 = mio::smm::Model; //Initialize agents for dabm SingleWell::Agent a1{Eigen::Vector2d{-0.5, 0}, @@ -162,13 +162,14 @@ TEST(TestTemporalHybrid, test_conversion_dabm_smm) mio::osecir::InfectionState::InfectedSymptoms}; Model1 model1({a1, a2, a3}, {}); - Model2 model2; - model2.parameters.get>().push_back( - {mio::osecir::InfectionState::Susceptible, - mio::osecir::InfectionState::Exposed, - mio::regions::Region(0), - 0.1, - {{mio::osecir::InfectionState::InfectedNoSymptoms, 1}, {mio::osecir::InfectionState::InfectedSymptoms, 0.5}}}); + Model2 model2(mio::osecir::InfectionState::Count, mio::regions::Region(1)); + model2.parameters.get>() + .push_back({mio::osecir::InfectionState::Susceptible, + mio::osecir::InfectionState::Exposed, + mio::regions::Region(0), + 0.1, + {{mio::osecir::InfectionState::InfectedNoSymptoms, 1}, + {mio::osecir::InfectionState::InfectedSymptoms, 0.5}}}); //Parameters for simulation double t0 = 0; From 3337b98c0fecb2aa8ae3ca9d10565d1e8986ac0c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:55:28 +0000 Subject: [PATCH 149/169] CHG: Keep last edge instad of first one --- cpp/memilio/mobility/graph.h | 30 +++++++++--------------------- cpp/tests/test_graph.cpp | 5 ++--- docs/source/cpp/graph_metapop.rst | 20 +------------------- 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 100f4be663..43fdae8dd6 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -508,7 +508,7 @@ class GraphBuilder * * Sorts the edges and optionally removes duplicate edges (same start and end node indices). * Wihout dupplicate removal, multiple edges between the same nodes are allowed and the order of insertion is stable. - * @param make_unique If true, duplicate edges are removed. The first added edge is kept! + * @param make_unique If true, duplicate edges are removed. The last added edge is kept! * @return Graph The constructed graph. */ Graph build(bool make_unique = false) @@ -540,42 +540,30 @@ class GraphBuilder * @brief Remove duplicate edges from a sorted edge vector. * * Copies all the unique edges to a new vector and replaces the original edge vector with it. Unique means that - * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the first + * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the last * edge in the vector is kept. */ void remove_duplicate_edges() { std::vector> unique_edges; unique_edges.reserve(m_edges.size()); - std::ranges::unique_copy(m_edges, std::back_inserter(unique_edges), [](auto&& e1, auto&& e2) { + auto curr_elem = m_edges.begin(); + auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; }); - m_edges = std::move(unique_edges); - } - - /** - * @brief Remove duplicate edges from a sorted edge vector. - * - * Copies all the unique edges to a new vector and replaces the original edge vector with it. Unique means that - * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the first - * edge in the vector is kept. - */ - void remove_double_edges() - { - std::vector> unique_edges; - unique_edges.reserve(m_edges.size()); - auto curr_elem = m_edges.begin(); - auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), is_equal); while (next_elem != m_edges.end()) { std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); curr_elem = next_elem; - while (is_equal(*curr_elem, *next_elem) && next_elem != m_edges.end()) { + while ((*curr_elem).start_node_idx == (*next_elem).start_node_idx && + (*curr_elem).end_node_idx == (*next_elem).end_node_idx && next_elem != m_edges.end()) { next_elem = next(next_elem); } curr_elem = prev(next_elem); std::copy(curr_elem, next(curr_elem), std::back_inserter(unique_edges)); curr_elem = next(curr_elem); - next_elem = std::adjacent_find(curr_elem, m_edges.end(), is_equal); + next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; + }); } std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); m_edges = std::move(unique_edges); diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index b084537cb8..1c6e8faac7 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -375,9 +375,8 @@ TEST(TestGraphBuilder, Build_unique) EXPECT_EQ(g.nodes().size(), 3); EXPECT_EQ(g.edges().size(), 3); - for (const auto& e : g.edges()) { - EXPECT_EQ(e.property, 100); - } + // The last added edge is kept: + EXPECT_EQ(g.edges()[1].property, 200); } namespace diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index 8139d3e577..2de1c3a4c7 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -195,7 +195,7 @@ The following steps detail how to configure and execute a graph simulation: Usually, there should be no duplicate edges in your input. If this is not certain, the ``GraphBuilder`` can also remove duplicates. Here, duplicate means that the start and end node are the same. The parameters in the edge will not be compared. - When duplicates are found, only the **first** inserted edge is kept, all subsequent edges are discarded: + When duplicates are found, only the **last** inserted edge is kept, all previous edges are discarded: .. code-block:: cpp @@ -208,24 +208,6 @@ The following steps detail how to configure and execute a graph simulation: auto graph = builder.build(true); // graph contains the edges (0, 1, 100) and (1, 0, 100) - To sum this up: - - .. list-table:: - :header-rows: 1 - - * - - - ``graph.add_edge`` - - ``GraphBuilder.add_edge`` - * - Sorting - - Always - - Only when graph is built - * - Duplicate deletion - - Always - - Only when activated on insertion - * - Keeps - - last duplicate - - first duplicate - 5. **Initialize and Advance the Mobility Simulation:** From 83553702bfd879d15d26e5245afe67ee59de7bc3 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:18:10 +0000 Subject: [PATCH 150/169] CHG: Print warning when duplicate edges are deleted --- cpp/memilio/mobility/graph.h | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index 43fdae8dd6..d394093691 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -560,6 +560,7 @@ class GraphBuilder } curr_elem = prev(next_elem); std::copy(curr_elem, next(curr_elem), std::back_inserter(unique_edges)); + mio::log_warning("Deleted duplicate edge(s)"); curr_elem = next(curr_elem); next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; From bdff456fc9289f5fed4a457bc5674701f27ccb39 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:14:32 +0000 Subject: [PATCH 151/169] CHG: Benchmarking script for new method comparison --- cpp/examples/asym_graph_ben.cpp | 145 +++++++++++++++++--------------- 1 file changed, 78 insertions(+), 67 deletions(-) diff --git a/cpp/examples/asym_graph_ben.cpp b/cpp/examples/asym_graph_ben.cpp index e02b6795fa..e388967430 100644 --- a/cpp/examples/asym_graph_ben.cpp +++ b/cpp/examples/asym_graph_ben.cpp @@ -34,6 +34,7 @@ #include "thirdparty/csv.h" #include #include +#include enum class InfectionState { @@ -77,14 +78,11 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({ICS, R, home, 0.4, {}}); adoption_rates.push_back({INS, R, home, 0.5, {}}); - mio::Graph>, - mio::MobilityEdgeDirected> - graph; mio::log_info("Reading CSVs"); std::vector latitudes, longitudes; std::vector farm_ids, num_cows_vec, dates, num_animals_exchanges; std::vector froms, tos, from_exchanges, to_exchanges; - io::CSVReader<4> farms("../../farms200000.csv"); + io::CSVReader<4> farms("../../../farms16mio.csv"); farms.read_header(io::ignore_extra_column, "farms", "num_cows", "latitude", "longitude"); int farm_id, num_cows; double latitude, longitude; @@ -94,16 +92,15 @@ int main(int /*argc*/, char** /*argv*/) latitudes.push_back(latitude); longitudes.push_back(longitude); } - io::CSVReader<2> edges("../../edges200000.csv"); + io::CSVReader<2> edges("../../../edges16mio.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { froms.push_back(from); tos.push_back(to); } - io::CSVReader<4> exchanges("../../trade200000.csv"); + io::CSVReader<4> exchanges("../../../trade16mio.csv"); exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to"); - int date, num_animals; while (exchanges.read_row(date, num_animals, from, to)) { dates.push_back(date); @@ -111,69 +108,83 @@ int main(int /*argc*/, char** /*argv*/) from_exchanges.push_back(from); to_exchanges.push_back(to); } - for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - graph.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); - } - auto rng = mio::RandomNumberGenerator(); - std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); - for (size_t i = 0; i < froms.size(); ++i) { - mio::timing::AutoTimer<"Graph Edges Generation"> timer; - graph.add_edge(froms[i], tos[i], interesting_indices); - } - - mio::log_info("First graph construction finished"); - - mio::Graph>, - mio::MobilityEdgeDirected> - graph2; - for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - graph2.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); - } - for (size_t i = 0; i < froms.size(); ++i) { - mio::timing::AutoTimer<"Graph Edges lazy Generation"> timer; - graph2.lazy_add_edge(froms[i], tos[i], interesting_indices); + for (size_t num_edges_benchmark = 1024; num_edges_benchmark < 16000000; num_edges_benchmark *= 2) { + mio::log_info("Running with {} edges", num_edges_benchmark); + + mio::Graph>, + mio::MobilityEdgeDirected> + graph; + mio::GraphBuilder>, + mio::MobilityEdgeDirected> + builder; + + auto start = std::chrono::high_resolution_clock::now(); + + for (size_t i = 0; i < farm_ids.size(); ++i) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + graph.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + } + auto rng = mio::RandomNumberGenerator(); + + for (size_t i = 0; i < num_edges_benchmark; ++i) { + graph.add_edge(froms[i], tos[i], interesting_indices); + } + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + mio::log_info("Classic graph generation finished for {} edges in {} milliseconds", num_edges_benchmark, + duration.count()); + start = std::chrono::high_resolution_clock::now(); + for (size_t i = 0; i < farm_ids.size(); ++i) { + Model curr_model; + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + builder.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + } + for (size_t i = 0; i < num_edges_benchmark; ++i) { + builder.add_edge(from_exchanges[i], to_exchanges[i], interesting_indices); + } + auto graph2 = builder.build(true); + end = std::chrono::high_resolution_clock::now(); + duration = std::chrono::duration_cast(end - start); + + mio::log_info("New graph construction finished using {} edges in {} milliseconds", num_edges_benchmark, + duration.count()); + + // auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + // return &node.property; + // }); + // auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + + // for (auto& node : graph.nodes()) { + // node.property.set_regional_neighbors( + // tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); + // } + + auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); + + for (size_t i = 0; i < dates.size(); ++i) { + sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); + } + + mio::log_info("Starting and immediately finishing simulation"); } - graph2.sort_edges(); - graph2.make_edges_unique(); - mio::log_info("Second graph construction finished"); - - auto nodes = graph.nodes() | std::views::transform([](const auto& node) { - return &node.property; - }); - auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); - - for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors( - tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); - } - - auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - - for (size_t i = 0; i < dates.size(); ++i) { - sim.add_exchange(dates[i], num_animals_exchanges[i], from_exchanges[i], to_exchanges[i]); - } - - mio::log_info("Starting and immediately finishing simulation"); return 0; } From 5a0f431edbc051c6e65288a55fcb426006a97f9f Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:30:47 +0000 Subject: [PATCH 152/169] [ci skip] Add benchmark results to docs --- docs/source/cpp/graph_metapop.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/cpp/graph_metapop.rst b/docs/source/cpp/graph_metapop.rst index 2de1c3a4c7..9e3a5d8aba 100644 --- a/docs/source/cpp/graph_metapop.rst +++ b/docs/source/cpp/graph_metapop.rst @@ -179,7 +179,7 @@ The following steps detail how to configure and execute a graph simulation: .. dropdown:: :fa:`gears` Working with large graphs - When working with very large graphs, i.e. starting from a few thousand edges, it will be faster to not use the standard ``add_edge`` function. + When working with very large graphs, i.e. starting from ten thousand edges, it will be faster to not use the standard ``add_edge`` function. For this case, we provide a ``GraphBuilder``. There you can add all edges without checking for uniqueness and sorting, thus improving the speed. The edges will be sorted when the graph is generated: From 72a33049421be6e083d2953faab09bb5d265c29c Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:59:59 +0100 Subject: [PATCH 153/169] fix asserts for debug builds --- cpp/memilio/utils/index.h | 39 ++++++++++++++++++++++++++++++++----- cpp/models/smm/model.h | 1 + cpp/models/smm/simulation.h | 15 +++++++------- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/cpp/memilio/utils/index.h b/cpp/memilio/utils/index.h index 70c5f75d38..8388aaba8b 100644 --- a/cpp/memilio/utils/index.h +++ b/cpp/memilio/utils/index.h @@ -22,6 +22,7 @@ #include "memilio/io/io.h" #include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/metaprogramming.h" #include "memilio/utils/type_safe.h" #include @@ -116,9 +117,10 @@ class MEMILIO_ENABLE_EBO Index : public TypeSafe>::TypeSafe; - static size_t constexpr size = 1; + static constexpr size_t size = 1; + static constexpr bool has_duplicates = false; - static Index constexpr Zero() + static constexpr Index Zero() { return Index((size_t)0); } @@ -173,7 +175,8 @@ template class Index { public: - static size_t constexpr size = sizeof...(CategoryTag); + static constexpr size_t size = sizeof...(CategoryTag); + static constexpr bool has_duplicates = has_duplicates_v; static Index constexpr Zero() { @@ -211,6 +214,25 @@ class Index return !(this == other); } + bool operator<(Index const& other) const + { + // use apply to unfold both tuples, then use a fold expression to evaluate a pairwise less + return std::apply( + [&other](auto&&... indices_) { + return std::apply( + [&](auto&&... other_indices_) { + return ((indices_ < other_indices_) && ...); + }, + other.indices); + }, + indices); + } + + bool operator<=(Index const& other) const + { + return (*this == other) || (*this < other); + } + /** * serialize this. * @see mio::serialize @@ -372,9 +394,16 @@ inline Index extend_index_impl(const Index& i, const * @return A (sub)index with the given categories and values from index. */ template -SubIndex reduce_index(const SuperIndex& index) +decltype(auto) reduce_index(const SuperIndex& index) { - return details::reduce_index_impl(index, mio::Tag{}); + if constexpr (SubIndex::size == 1 && std::is_base_of_v, SubIndex>) { + // this case handles reducing from e.g. Index directly to AgeGroup + // the default case would instead reduce to Index, which may cause conversion errors + return details::reduce_index_impl(index, mio::Tag>{}); + } + else { + return details::reduce_index_impl(index, mio::Tag{}); + } } template diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index cd6be1577d..0fc0eca2f6 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -51,6 +51,7 @@ class Model : public mio::CompartmentalModel>, ParametersBase>; + static_assert(!Base::Populations::Index::has_duplicates, "Do not use the same Index tag multiple times!"); public: using Status = StatusT; diff --git a/cpp/models/smm/simulation.h b/cpp/models/smm/simulation.h index 3f7f922b25..0ed56ee13c 100644 --- a/cpp/models/smm/simulation.h +++ b/cpp/models/smm/simulation.h @@ -41,7 +41,6 @@ class Simulation { public: using Model = smm::Model; - using Index = typename Model::Populations::Index; /** * @brief Set up the simulation for a Stochastic Metapopulation Model. @@ -60,12 +59,14 @@ class Simulation { assert(dt > 0); assert(m_waiting_times.size() > 0); - assert(std::all_of(adoption_rates().begin(), adoption_rates().end(), [](auto&& r) { - return static_cast(r.region) < regions; - })); - assert(std::all_of(transition_rates().begin(), transition_rates().end(), [](auto&& r) { - return static_cast(r.from) < regions && static_cast(r.to) < regions; - })); + assert(std::all_of(adoption_rates().begin(), adoption_rates().end(), + [regions = reduce_index(model.populations.size())](auto&& r) { + return r.region < regions; + })); + assert(std::all_of(transition_rates().begin(), transition_rates().end(), + [regions = reduce_index(model.populations.size())](auto&& r) { + return r.from < regions && r.to < regions; + })); // initialize (internal) next event times by random values for (size_t i = 0; i < m_tp_next_event.size(); i++) { m_tp_next_event[i] += mio::ExponentialDistribution::get_instance()(m_model->get_rng(), 1.0); From d47625cd468c888825a40d06d5557d8dda0f93bb Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:00:27 +0100 Subject: [PATCH 154/169] change test to use region as part of status --- cpp/tests/test_smm_model.cpp | 37 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/cpp/tests/test_smm_model.cpp b/cpp/tests/test_smm_model.cpp index 56386c73d0..0e906dc13f 100644 --- a/cpp/tests/test_smm_model.cpp +++ b/cpp/tests/test_smm_model.cpp @@ -92,38 +92,37 @@ TEST(TestSMM, evaluateinterregionalAdoptionRate) using Age = mio::AgeGroup; using mio::regions::Region; using Status = mio::Index; - using Model = mio::smm::Model; + using Model = mio::smm::Model>; - std::vector> adoption_rates; + std::vector>> adoption_rates; //Second-order adoption adoption_rates.push_back({{Infections::S, Age(0), Region(0)}, {Infections::I, Age(0), Region(0)}, - mio::regions::Region(0), + {}, 0.1, {{{Infections::I, Age(1), Region(0)}, 0.1}, {{Infections::I, Age(1), Region(1)}, 0.2}}}); //First-order adoption - adoption_rates.push_back( - {{Infections::I, Age(1), Region(0)}, {Infections::R, Age(1), Region(0)}, Region(0), 0.2, {}}); - Model model({Infections::Count, Age(2), Region(2)}, Region(1)); + adoption_rates.push_back({{Infections::I, Age(1), Region(0)}, {Infections::R, Age(1), Region(0)}, {}, 0.2, {}}); + Model model({Infections::Count, Age(2), Region(2)}, {}); - model.populations[{Region(0), Infections::S, Age(0), Region(0)}] = 50; - model.populations[{Region(0), Infections::I, Age(0), Region(0)}] = 10; - model.populations[{Region(0), Infections::R, Age(0), Region(0)}] = 0; + model.populations[{Infections::S, Age(0), Region(0)}] = 50; + model.populations[{Infections::I, Age(0), Region(0)}] = 10; + model.populations[{Infections::R, Age(0), Region(0)}] = 0; - model.populations[{Region(0), Infections::S, Age(1), Region(0)}] = 100; - model.populations[{Region(0), Infections::I, Age(1), Region(0)}] = 20; - model.populations[{Region(0), Infections::R, Age(1), Region(0)}] = 0; + model.populations[{Infections::S, Age(1), Region(0)}] = 100; + model.populations[{Infections::I, Age(1), Region(0)}] = 20; + model.populations[{Infections::R, Age(1), Region(0)}] = 0; - model.populations[{Region(0), Infections::S, Age(0), Region(1)}] = 40; - model.populations[{Region(0), Infections::I, Age(0), Region(1)}] = 80; - model.populations[{Region(0), Infections::R, Age(0), Region(1)}] = 0; + model.populations[{Infections::S, Age(0), Region(1)}] = 40; + model.populations[{Infections::I, Age(0), Region(1)}] = 80; + model.populations[{Infections::R, Age(0), Region(1)}] = 0; - model.populations[{Region(0), Infections::S, Age(1), Region(1)}] = 80; - model.populations[{Region(0), Infections::I, Age(1), Region(1)}] = 16; - model.populations[{Region(0), Infections::R, Age(1), Region(1)}] = 0; + model.populations[{Infections::S, Age(1), Region(1)}] = 80; + model.populations[{Infections::I, Age(1), Region(1)}] = 16; + model.populations[{Infections::R, Age(1), Region(1)}] = 0; EXPECT_DOUBLE_EQ(model.evaluate(adoption_rates[0], model.populations.get_compartments()), - 0.1 * 50. * (0.1 * 20. * 1. / 120. + 0.2 * 16 * 1. / 96.)); + 0.1 * 50. * (0.1 * 20. * 1. + 0.2 * 16 * 1.) / (60 + 120 + 120 + 96)); EXPECT_DOUBLE_EQ(model.evaluate(adoption_rates[1], model.populations.get_compartments()), 4.); } From fa4a0f7d3b87b3d604f3d3ab4fa8c12f2e506c4c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:09:05 +0000 Subject: [PATCH 155/169] CHG: Apply suggestions from code review --- cpp/memilio/mobility/graph.h | 20 ++++++++++++-------- cpp/tests/test_graph.cpp | 6 ++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index d394093691..d433120814 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -511,7 +511,7 @@ class GraphBuilder * @param make_unique If true, duplicate edges are removed. The last added edge is kept! * @return Graph The constructed graph. */ - Graph build(bool make_unique = false) + Graph build(bool make_unique = false) && { sort_edges(); if (make_unique) { @@ -547,27 +547,31 @@ class GraphBuilder { std::vector> unique_edges; unique_edges.reserve(m_edges.size()); - auto curr_elem = m_edges.begin(); - auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { + bool duplicate_edges = false; + auto curr_elem = m_edges.begin(); + auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; }); while (next_elem != m_edges.end()) { std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); curr_elem = next_elem; - while ((*curr_elem).start_node_idx == (*next_elem).start_node_idx && - (*curr_elem).end_node_idx == (*next_elem).end_node_idx && next_elem != m_edges.end()) { + while (next_elem != m_edges.end() && (*curr_elem).start_node_idx == (*next_elem).start_node_idx && + (*curr_elem).end_node_idx == (*next_elem).end_node_idx) { next_elem = next(next_elem); } curr_elem = prev(next_elem); std::copy(curr_elem, next(curr_elem), std::back_inserter(unique_edges)); - mio::log_warning("Deleted duplicate edge(s)"); - curr_elem = next(curr_elem); - next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { + duplicate_edges = true; + curr_elem = next(curr_elem); + next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; }); } std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); m_edges = std::move(unique_edges); + if (duplicate_edges) { + mio::log_warning("Removed duplicate edge(s)"); + } } private: diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index 1c6e8faac7..6eaf51c06f 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -370,13 +370,15 @@ TEST(TestGraphBuilder, Build_unique) builder.add_edge(0, 1, 100); builder.add_edge(2, 1, 100); builder.add_edge(1, 2, 200); + builder.add_edge(2, 1, 300); - auto g = builder.build(true); + auto g = std::move(builder).build(true); EXPECT_EQ(g.nodes().size(), 3); EXPECT_EQ(g.edges().size(), 3); - // The last added edge is kept: + // The last added edges are kept: EXPECT_EQ(g.edges()[1].property, 200); + EXPECT_EQ(g.edges()[2].property, 300); } namespace From 3f3d42104eb70a096a65bd6980c5a5f84768df02 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:10:22 +0000 Subject: [PATCH 156/169] FIX: use std::move on builder --- cpp/tests/test_graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index 6eaf51c06f..cfa5a143e3 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -354,7 +354,7 @@ TEST(TestGraphBuilder, Build) builder.add_edge(2, 1, 100); builder.add_edge(1, 2, 100); - auto g = builder.build(); + auto g = std::move(builder).build(); EXPECT_EQ(g.nodes().size(), 3); EXPECT_EQ(g.edges().size(), 3); From 785275e46a16953943b4b06d202e86031c769980 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:21:55 +0000 Subject: [PATCH 157/169] CHG: Move GraphBuilder to own file --- cpp/memilio/CMakeLists.txt | 2 + cpp/memilio/mobility/graph.h | 129 ------------------- cpp/memilio/mobility/graph_builder.cpp | 25 ++++ cpp/memilio/mobility/graph_builder.h | 165 +++++++++++++++++++++++++ cpp/tests/test_graph.cpp | 1 + 5 files changed, 193 insertions(+), 129 deletions(-) create mode 100644 cpp/memilio/mobility/graph_builder.cpp create mode 100644 cpp/memilio/mobility/graph_builder.h diff --git a/cpp/memilio/CMakeLists.txt b/cpp/memilio/CMakeLists.txt index 5a51eafbc6..658733a7f8 100644 --- a/cpp/memilio/CMakeLists.txt +++ b/cpp/memilio/CMakeLists.txt @@ -81,6 +81,8 @@ add_library(memilio mobility/graph_simulation.cpp mobility/graph.h mobility/graph.cpp + mobility/graph_builder.h + mobility/graph_builder.cpp timer/auto_timer.h timer/basic_timer.cpp timer/basic_timer.h diff --git a/cpp/memilio/mobility/graph.h b/cpp/memilio/mobility/graph.h index d433120814..1c30749a24 100644 --- a/cpp/memilio/mobility/graph.h +++ b/cpp/memilio/mobility/graph.h @@ -450,135 +450,6 @@ void print_graph(std::ostream& os, const Graph& g) } } -/** - * @brief A builder class for constructing graphs. - * - * This class provides a interface for adding nodes and edges to a graph. It allows for efficient construction of large - * graphs by reserving space for nodes and edges in advance. The build method finalizes the graph by sorting edges and - * optionally removing duplicates. - * The advantage over the :ref add_edge function of the Graph class is that edges are only sorted once during the build - * process, improving performance when adding many edges. - * - * @tparam NodePropertyT Type of the node property. - * @tparam EdgePropertyT Type of the edge property. - */ -template -class GraphBuilder -{ -public: - using NodeProperty = NodePropertyT; - using EdgeProperty = EdgePropertyT; - - GraphBuilder() = default; - GraphBuilder(const size_t num_nodes, const size_t num_edges) - { - m_nodes.reserve(num_nodes); - m_edges.reserve(num_edges); - } - - /** - * @brief Add a node to the GraphBuilder. - * - * The property of the node is constructed from arguments. - * @param id Id for the node. - * @tparam args Additional arguments for node construction. - */ - template - void add_node(int id, Args&&... args) - { - m_nodes.emplace_back(id, std::forward(args)...); - } - - /** - * @brief Add an edge to the GraphBuilder. - * - * @param start_node_idx Id of start node - * @param end_node_idx Id of end node - * @tparam args Additional arguments for edge construction - */ - template - void add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) - { - assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); - m_edges.emplace_back(start_node_idx, end_node_idx, std::forward(args)...); - } - - /** - * @brief Build the graph from the added nodes and edges. - * - * Sorts the edges and optionally removes duplicate edges (same start and end node indices). - * Wihout dupplicate removal, multiple edges between the same nodes are allowed and the order of insertion is stable. - * @param make_unique If true, duplicate edges are removed. The last added edge is kept! - * @return Graph The constructed graph. - */ - Graph build(bool make_unique = false) && - { - sort_edges(); - if (make_unique) { - remove_duplicate_edges(); - } - Graph graph(std::move(m_nodes), std::move(m_edges)); - return graph; - } - -private: - /** - * @brief Sort the edge vector of a graph. - * - * Sorts the edges first by start node index, then by end node index. We use stable_sort to keep the order of insertion - * for edges with the same start and end node indices. - */ - void sort_edges() - { - std::stable_sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx - : e1.start_node_idx < e2.start_node_idx; - }); - } - - /** - * @brief Remove duplicate edges from a sorted edge vector. - * - * Copies all the unique edges to a new vector and replaces the original edge vector with it. Unique means that - * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the last - * edge in the vector is kept. - */ - void remove_duplicate_edges() - { - std::vector> unique_edges; - unique_edges.reserve(m_edges.size()); - bool duplicate_edges = false; - auto curr_elem = m_edges.begin(); - auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; - }); - while (next_elem != m_edges.end()) { - std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); - curr_elem = next_elem; - while (next_elem != m_edges.end() && (*curr_elem).start_node_idx == (*next_elem).start_node_idx && - (*curr_elem).end_node_idx == (*next_elem).end_node_idx) { - next_elem = next(next_elem); - } - curr_elem = prev(next_elem); - std::copy(curr_elem, next(curr_elem), std::back_inserter(unique_edges)); - duplicate_edges = true; - curr_elem = next(curr_elem); - next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; - }); - } - std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); - m_edges = std::move(unique_edges); - if (duplicate_edges) { - mio::log_warning("Removed duplicate edge(s)"); - } - } - -private: - std::vector> m_nodes; - std::vector> m_edges; -}; - } // namespace mio #endif //GRAPH_H diff --git a/cpp/memilio/mobility/graph_builder.cpp b/cpp/memilio/mobility/graph_builder.cpp new file mode 100644 index 0000000000..660a7b62ea --- /dev/null +++ b/cpp/memilio/mobility/graph_builder.cpp @@ -0,0 +1,25 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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/mobility/graph_builder.h" + +namespace mio +{ + +} diff --git a/cpp/memilio/mobility/graph_builder.h b/cpp/memilio/mobility/graph_builder.h new file mode 100644 index 0000000000..1ee68055c0 --- /dev/null +++ b/cpp/memilio/mobility/graph_builder.h @@ -0,0 +1,165 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 GRAPH_BUILDER_H +#define GRAPH_BUILDER_H + +#include "memilio/mobility/graph.h" +#include "memilio/utils/logging.h" +#include "memilio/utils/stl_util.h" +#include +#include +#include +#include + +namespace mio +{ + +/** + * @brief A builder class for constructing graphs. + * + * This class provides a interface for adding nodes and edges to a graph. It allows for efficient construction of large + * graphs by reserving space for nodes and edges in advance. The build method finalizes the graph by sorting edges and + * optionally removing duplicates. + * The advantage over the :ref add_edge function of the Graph class is that edges are only sorted once during the build + * process, improving performance when adding many edges. + * + * @tparam NodePropertyT Type of the node property. + * @tparam EdgePropertyT Type of the edge property. + */ +template +class GraphBuilder +{ +public: + using NodeProperty = NodePropertyT; + using EdgeProperty = EdgePropertyT; + + GraphBuilder() = default; + GraphBuilder(const size_t num_nodes, const size_t num_edges) + { + m_nodes.reserve(num_nodes); + m_edges.reserve(num_edges); + } + + /** + * @brief Add a node to the GraphBuilder. + * + * The property of the node is constructed from arguments. + * @param id Id for the node. + * @tparam args Additional arguments for node construction. + */ + template + void add_node(int id, Args&&... args) + { + m_nodes.emplace_back(id, std::forward(args)...); + } + + /** + * @brief Add an edge to the GraphBuilder. + * + * @param start_node_idx Id of start node + * @param end_node_idx Id of end node + * @tparam args Additional arguments for edge construction + */ + template + void add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args) + { + assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx); + m_edges.emplace_back(start_node_idx, end_node_idx, std::forward(args)...); + } + + /** + * @brief Build the graph from the added nodes and edges. + * + * Sorts the edges and optionally removes duplicate edges (same start and end node indices). + * Wihout dupplicate removal, multiple edges between the same nodes are allowed and the order of insertion is stable. + * @param make_unique If true, duplicate edges are removed. The last added edge is kept! + * @return Graph The constructed graph. + */ + Graph build(bool make_unique = false) && + { + sort_edges(); + if (make_unique) { + remove_duplicate_edges(); + } + Graph graph(std::move(m_nodes), std::move(m_edges)); + return graph; + } + +private: + /** + * @brief Sort the edge vector of a graph. + * + * Sorts the edges first by start node index, then by end node index. We use stable_sort to keep the order of insertion + * for edges with the same start and end node indices. + */ + void sort_edges() + { + std::stable_sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx + : e1.start_node_idx < e2.start_node_idx; + }); + } + + /** + * @brief Remove duplicate edges from a sorted edge vector. + * + * Copies all the unique edges to a new vector and replaces the original edge vector with it. Unique means that + * the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the last + * edge in the vector is kept. + */ + void remove_duplicate_edges() + { + std::vector> unique_edges; + unique_edges.reserve(m_edges.size()); + bool duplicate_edges = false; + auto curr_elem = m_edges.begin(); + auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; + }); + while (next_elem != m_edges.end()) { + std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); + curr_elem = next_elem; + while (next_elem != m_edges.end() && (*curr_elem).start_node_idx == (*next_elem).start_node_idx && + (*curr_elem).end_node_idx == (*next_elem).end_node_idx) { + next_elem = next(next_elem); + } + curr_elem = prev(next_elem); + std::copy(curr_elem, next(curr_elem), std::back_inserter(unique_edges)); + duplicate_edges = true; + curr_elem = next(curr_elem); + next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { + return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; + }); + } + std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); + m_edges = std::move(unique_edges); + if (duplicate_edges) { + mio::log_warning("Removed duplicate edge(s)"); + } + } + +private: + std::vector> m_nodes; + std::vector> m_edges; +}; + +} // namespace mio + +#endif // GRAPH_BUILDER_H \ No newline at end of file diff --git a/cpp/tests/test_graph.cpp b/cpp/tests/test_graph.cpp index cfa5a143e3..1f14ce65b9 100644 --- a/cpp/tests/test_graph.cpp +++ b/cpp/tests/test_graph.cpp @@ -18,6 +18,7 @@ * limitations under the License. */ #include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" #include "memilio/epidemiology/age_group.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/date.h" From 3cb845623aabc5e6fe64e93cd204885fe6ed6a9c Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:48:43 +0000 Subject: [PATCH 158/169] CHG: Simplify function --- cpp/memilio/mobility/graph_builder.h | 30 +++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/cpp/memilio/mobility/graph_builder.h b/cpp/memilio/mobility/graph_builder.h index 1ee68055c0..ef27fcf537 100644 --- a/cpp/memilio/mobility/graph_builder.h +++ b/cpp/memilio/mobility/graph_builder.h @@ -88,7 +88,7 @@ class GraphBuilder * @brief Build the graph from the added nodes and edges. * * Sorts the edges and optionally removes duplicate edges (same start and end node indices). - * Wihout dupplicate removal, multiple edges between the same nodes are allowed and the order of insertion is stable. + * Without duplicate removal, multiple edges between the same nodes are allowed and the order of insertion is stable. * @param make_unique If true, duplicate edges are removed. The last added edge is kept! * @return Graph The constructed graph. */ @@ -130,25 +130,19 @@ class GraphBuilder unique_edges.reserve(m_edges.size()); bool duplicate_edges = false; auto curr_elem = m_edges.begin(); - auto next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; - }); - while (next_elem != m_edges.end()) { - std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); - curr_elem = next_elem; - while (next_elem != m_edges.end() && (*curr_elem).start_node_idx == (*next_elem).start_node_idx && - (*curr_elem).end_node_idx == (*next_elem).end_node_idx) { - next_elem = next(next_elem); + + while (curr_elem != m_edges.end()) { + auto next_elem = std::next(curr_elem); + if (next_elem != m_edges.end() && curr_elem->start_node_idx == next_elem->start_node_idx && + curr_elem->end_node_idx == next_elem->end_node_idx) { + duplicate_edges = true; } - curr_elem = prev(next_elem); - std::copy(curr_elem, next(curr_elem), std::back_inserter(unique_edges)); - duplicate_edges = true; - curr_elem = next(curr_elem); - next_elem = std::adjacent_find(curr_elem, m_edges.end(), [](auto&& e1, auto&& e2) { - return e1.start_node_idx == e2.start_node_idx && e1.end_node_idx == e2.end_node_idx; - }); + else if (next_elem != m_edges.end()) { + std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); + } + curr_elem = next_elem; } - std::copy(curr_elem, next_elem, std::back_inserter(unique_edges)); + std::copy(std::prev(m_edges.end()), m_edges.end(), std::back_inserter(unique_edges)); m_edges = std::move(unique_edges); if (duplicate_edges) { mio::log_warning("Removed duplicate edge(s)"); From 66a7347cd81b6172966b8767b244f0eb981d0cc8 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 27 Nov 2025 11:30:53 +0000 Subject: [PATCH 159/169] CHG: ADd builder --- cpp/examples/asym_graph_ben.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/examples/asym_graph_ben.cpp b/cpp/examples/asym_graph_ben.cpp index e388967430..823aa0a4f3 100644 --- a/cpp/examples/asym_graph_ben.cpp +++ b/cpp/examples/asym_graph_ben.cpp @@ -24,6 +24,7 @@ #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" #include "memilio/timer/auto_timer.h" @@ -161,7 +162,7 @@ int main(int /*argc*/, char** /*argv*/) for (size_t i = 0; i < num_edges_benchmark; ++i) { builder.add_edge(from_exchanges[i], to_exchanges[i], interesting_indices); } - auto graph2 = builder.build(true); + auto graph2 = std::move(builder).build(true); end = std::chrono::high_resolution_clock::now(); duration = std::chrono::duration_cast(end - start); From 6c1121085a3f8ba0b3005244370277f1f34077a9 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 27 Nov 2025 11:58:10 +0000 Subject: [PATCH 160/169] CHG: Use GraphBuilder --- cpp/examples/asymmetric_graph.cpp | 27 +++++++++++---------------- cpp/examples/asymmetric_params.cpp | 14 +++++++------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index 90e7c633c5..f9ed4193a8 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -24,6 +24,7 @@ #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" #include "memilio/timer/auto_timer.h" @@ -77,9 +78,9 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({ICS, R, home, 0.4, {}}); adoption_rates.push_back({INS, R, home, 0.5, {}}); - mio::Graph>, - mio::MobilityEdgeDirected> - graph; + mio::GraphBuilder>, + mio::MobilityEdgeDirected> + builder; mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; @@ -97,7 +98,7 @@ int main(int /*argc*/, char** /*argv*/) curr_model.populations[{home, InfectionState::R}] = 0; curr_model.populations[{home, InfectionState::D}] = 0; curr_model.parameters.get>() = adoption_rates; - graph.add_node(farm_id, longitude, latitude, curr_model, t0); + builder.add_node(farm_id, longitude, latitude, curr_model, t0); } } mio::log_info("Nodes added to Graph"); @@ -105,18 +106,13 @@ int main(int /*argc*/, char** /*argv*/) std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); - graph.reserve_edges(200000); - { - mio::timing::AutoTimer<"Graph Edges Generation"> timer; - io::CSVReader<2> edges("../../edges200000.csv"); - edges.read_header(io::ignore_extra_column, "from", "to"); - size_t from, to; - while (edges.read_row(from, to)) { - graph.lazy_add_edge(from, to, interesting_indices); - } - graph.sort_edges(); - graph.make_edges_unique(); + io::CSVReader<2> edges("../../edges200000.csv"); + edges.read_header(io::ignore_extra_column, "from", "to"); + size_t from, to; + while (edges.read_row(from, to)) { + builder.add_edge(from, to, interesting_indices); } + auto graph = std::move(builder).build(); // // mio::log_info("Graph generated"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { @@ -138,7 +134,6 @@ int main(int /*argc*/, char** /*argv*/) exchanges.read_header(io::ignore_extra_column, "date", "num_animals", "from", "to"); int date, num_animals; - size_t from, to; while (exchanges.read_row(date, num_animals, from, to)) { sim.add_exchange(date, num_animals, from, to); } diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index a8a674ff22..f77e27ce4a 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -25,6 +25,7 @@ #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/custom_index_array.h" #include "memilio/utils/logging.h" @@ -140,9 +141,9 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({ICS, R, home, 0.4, {}}); adoption_rates.push_back({INS, R, home, 0.5, {}}); - mio::Graph>, - mio::MobilityEdgeDirected> - graph; + mio::GraphBuilder>, + mio::MobilityEdgeDirected> + builder; // usage: // if (rank==0) v = {1,2,3,4}; @@ -209,7 +210,7 @@ int main(int /*argc*/, char** /*argv*/) curr_model.populations[{home, InfectionState::R}] = 0; curr_model.populations[{home, InfectionState::D}] = 0; curr_model.parameters.get>() = adoption_rates; - graph.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + builder.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); } auto rng = mio::RandomNumberGenerator(); @@ -217,10 +218,9 @@ int main(int /*argc*/, char** /*argv*/) interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); graph.reserve_edges(froms.size()); for (size_t i = 0; i < froms.size(); ++i) { - graph.lazy_add_edge(froms[i], tos[i], interesting_indices); + builder.add_edge(froms[i], tos[i], interesting_indices); } - graph.sort_edges(); - graph.make_edges_unique(); + auto graph = std::move(builder).build(); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { return &node.property; }); From cfad58247811301904ade722330e9c61b4291029 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 27 Nov 2025 11:59:58 +0000 Subject: [PATCH 161/169] FIX --- cpp/examples/asymmetric_params.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index f77e27ce4a..aa61b45cbc 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -216,7 +216,6 @@ int main(int /*argc*/, char** /*argv*/) std::vector> interesting_indices; interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); - graph.reserve_edges(froms.size()); for (size_t i = 0; i < froms.size(); ++i) { builder.add_edge(froms[i], tos[i], interesting_indices); } From e0b4548966e2212af79f02a2f61c4e132efd1653 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:07:00 +0000 Subject: [PATCH 162/169] CHG: Use Builder --- cpp/examples/asym_ex_data.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cpp/examples/asym_ex_data.cpp b/cpp/examples/asym_ex_data.cpp index 8c53e35845..c196120d1e 100644 --- a/cpp/examples/asym_ex_data.cpp +++ b/cpp/examples/asym_ex_data.cpp @@ -24,6 +24,7 @@ #include "memilio/mobility/graph_simulation.h" #include "memilio/mobility/metapopulation_mobility_asymmetric.h" #include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/logging.h" #include "memilio/timer/auto_timer.h" @@ -75,9 +76,9 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({ICS, R, home, 0.4, {}}); adoption_rates.push_back({INS, R, home, 0.5, {}}); - mio::Graph>, - mio::MobilityEdgeDirected> - graph; + mio::GraphBuilder>, + mio::MobilityEdgeDirected> + builder; mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; @@ -95,7 +96,7 @@ int main(int /*argc*/, char** /*argv*/) curr_model.populations[{home, InfectionState::R}] = 0; curr_model.populations[{home, InfectionState::D}] = 0; curr_model.parameters.get>() = adoption_rates; - graph.add_node(farm_id, longitude, latitude, curr_model, t0); + builder.add_node(farm_id, longitude, latitude, curr_model, t0); } } mio::log_info("Nodes added to Graph"); @@ -110,11 +111,10 @@ int main(int /*argc*/, char** /*argv*/) edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { - // graph.add_edge(from, to, interesting_indices); - graph.lazy_add_edge(from, to, interesting_indices); + builder.add_edge(from, to, interesting_indices); } - graph.sort_edges(); } + auto graph = std::move(builder).build(); mio::log_info("Graph generated"); auto nodes = graph.nodes() | std::views::transform([](const auto& node) { From d2deb8fc145b7799c67e6235d7589c32fd337f43 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:56:03 +0100 Subject: [PATCH 163/169] CHG: improve docstrings --- cpp/memilio/epidemiology/adoption_rate.h | 2 +- cpp/memilio/epidemiology/populations.h | 7 +++++++ cpp/models/smm/model.h | 1 + cpp/models/smm/parameters.h | 13 +++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index 5a2a953ca1..9ad4340d9f 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -43,7 +43,7 @@ struct Influence { * In the d_abm and smm simulations, "from" is implicitly an influence, scaled by "factor". This is multiplied by * the sum over all "influences", which scale their "status" with the respective "factor". * @tparam Status An infection state enum. - * @tparam Groups Additional grouping indices. + * @tparam Region An (multi)-index. */ template struct AdoptionRate { diff --git a/cpp/memilio/epidemiology/populations.h b/cpp/memilio/epidemiology/populations.h index 24246b4471..6bdefaa841 100644 --- a/cpp/memilio/epidemiology/populations.h +++ b/cpp/memilio/epidemiology/populations.h @@ -287,6 +287,13 @@ class Populations : public CustomIndexArray, Categories...> } }; +/** + * @brief Population template specialization for Index types. + * + * @tparam FP Floating point type + * @tparam Categories Index categories + */ + template class Populations> : public Populations { diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index 0fc0eca2f6..ac4cd4272e 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -44,6 +44,7 @@ using PopulationIndex = decltype(merge_indices(std::declval(), std::decl * @brief Stochastic Metapopulation Model. * @tparam regions Number of regions. * @tparam Status An infection state enum. + * @tparam Region An (multi)-index, default is @ref mio::regions::Region. */ template class Model : public mio::CompartmentalModel>, diff --git a/cpp/models/smm/parameters.h b/cpp/models/smm/parameters.h index b44b63e827..b27e06cb39 100644 --- a/cpp/models/smm/parameters.h +++ b/cpp/models/smm/parameters.h @@ -33,7 +33,10 @@ namespace smm /** * @brief A vector of AdoptionRate%s, see mio::AdoptionRate + * + * @tparam FP Floating point type * @tparam Status An infection state enum. + * @tparam Region An (multi)-index. */ template struct AdoptionRates { @@ -46,7 +49,10 @@ struct AdoptionRates { /** * @brief Struct defining a possible regional transition in a Model based on Poisson Processes. + * + * @tparam FP Floating point type * @tparam Status An infection state enum. + * @tparam Region An (multi)-index. */ template struct TransitionRate { @@ -56,6 +62,13 @@ struct TransitionRate { FP factor; // lambda_i^{kl} }; +/** + * @brief A vector of TransitionRate%s, see mio::TransitionRate + * + * @tparam FP Floating point type + * @tparam Status An infection state enum. + * @tparam Region An (multi)-index. + */ template struct TransitionRates { using Type = std::vector>; From 168116b6927e16667886ffc97d081bd4517e4310 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:45:18 +0100 Subject: [PATCH 164/169] CHG: Update smm.rst --- docs/source/cpp/smm.rst | 128 ++++++++++++++++++++++++++++++---------- 1 file changed, 98 insertions(+), 30 deletions(-) diff --git a/docs/source/cpp/smm.rst b/docs/source/cpp/smm.rst index e2fb413f75..b1a1901f3c 100644 --- a/docs/source/cpp/smm.rst +++ b/docs/source/cpp/smm.rst @@ -1,11 +1,20 @@ Stochastic metapopulation model =============================== -The stochastic metapopulation model uses a Markov process to simulate disease dynamics. Similar to the `Diffusive Agent-based Model` the Markov process is given by location and infection state changes. However, in contrast to the diffusive ABM, location changes are not given by agent-dependent diffusion processes, but by stochastic jumps between regions with the requirement that the domain is split into disjoint regions. Hence, there is no further spatial resolution within one region and locations or positions are only given by the region index. The evolution of the system state is determined by the following master equation +The stochastic metapopulation model uses a Markov process to simulate disease dynamics. Similar to the :doc:`diffusive_abm` the +Markov process is given by location and infection state changes. However, in contrast to the diffusive ABM, location changes are +not given by agent-dependent diffusion processes, but by stochastic jumps between regions with the requirement that the domain +is split into disjoint regions. Hence, there is no further spatial resolution within one region and locations or positions are +only given by the region index. The evolution of the system state is determined by the following master equation :math:`\partial_t p(X,Z;t) = G p(X,Z;t) + L p(X,Z;t)`. -The operator :math:`G` defines the infection state adoptions and only acts on :math:`Z`, the vector containing all subpopulations stratified by infection state. :math:`L` defines location changes, only acting on :math:`X`, the vector containing all subpopulations stratified by region. Infection state adoptions are modeled as stochastic jumps with independent Poisson processes given by adoption rate functions. Similar to the infection state dynamics, spatial transitions between regions are also modeled as stochastic jumps with independent Poisson processes given by transition rate functions. Gillespie's direct method (stochastic simulation algorithm) is used for simulation. +The operator :math:`G` defines the infection state adoptions and only acts on :math:`Z`, the vector containing all subpopulations +stratified by infection state. :math:`L` defines location changes, only acting on :math:`X`, the vector containing all subpopulations +stratified by region. Infection state adoptions are modeled as stochastic jumps with independent Poisson processes given by adoption +rate functions. Similar to the infection state dynamics, spatial transitions between regions are also modeled as stochastic jumps +with independent Poisson processes given by transition rate functions. Gillespie's direct method (stochastic simulation algorithm) +is used for the simulation. In the following, we present more details of the stochastic metapopulation model, including code examples. An overview of nonstandard but often used data types can be found under :doc:`data_types`. @@ -13,7 +22,8 @@ An overview of nonstandard but often used data types can be found under :doc:`da Infection states ---------------- -The model does not have fixed infection states, but gets an enum class of infection states as template argument. Thus it can be used with any set of infection states. +The model does not have fixed infection states, but gets an enum class of infection states as template argument. Thus it +can be used with any set of infection states. Using the infection states Susceptible (S), Exposed (E), Carrier (C), Infected (I), Recovered (R) and Dead (D), this can be done as follows: .. code-block:: cpp @@ -32,41 +42,44 @@ Using the infection states Susceptible (S), Exposed (E), Carrier (C), Infected ( const size_t num_regions = 2; - using Model = mio::smm::Model; + using Status = mio::Index; + using Model = mio::smm::Model; Model model; Infection state transitions --------------------------- -The infection state transitions are explicitly given by the adoption rates and are therefore subject to user input. Adoption rates always depend on their source infection state. If an adoption event requires interaction of agents (e.g. disease transmission), the corresponding rate depends not only on the source infection state, but also on other infection states, the **Influence**\s. An adoption rate that only depends on the source infection state, e.g. recovery or worsening of disease symptoms, is called `first-order` adoption rate and an adoption rate that has influences is called `second-order` adoption rate. Adoption rates are region-dependent; therefore it is possible to have different rates in two regions for the same infection state transition which can be useful when having e.g. region-dependent interventions or contact behavior. +The infection state transitions are explicitly given by the adoption rates and are therefore subject to user input. +Adoption rates always depend on their source infection state. If an adoption event requires interaction of agents (e.g. +disease transmission), the corresponding rate depends not only on the source infection state, but also on other infection +states, the **Influence**\s. An adoption rate that only depends on the source infection state, e.g. recovery or worsening +of disease symptoms, is called `first-order` adoption rate and an adoption rate that has influences is called `second-order` +adoption rate. Adoption rates are region-dependent; therefore it is possible to have different rates in two regions for +the same infection state transition which can be useful when having e.g. region-dependent interventions or contact behavior. -Using the infection states from above and two regions, there are five first-order adoption rates per region and one second-order adoption rate per region. In the example below, the second-order adoption rate (transition from S to E) differs between the regions: +Using the infection states from above and two regions, there are five first-order adoption rates per region and one second-order +adoption rate per region. In the example below, the second-order adoption rate (transition from S to E) differs between the regions: .. code-block:: cpp - std::vector> adoption_rates; + std::vector> adoption_rates; //Set first-order adoption rates for both regions for (size_t r = 0; r < num_regions; ++r) { - adoption_rates.push_back({InfectionState::E, InfectionState::C, mio::regions::Region(r), 1.0 / 5., {}}); - adoption_rates.push_back({InfectionState::C, InfectionState::R, mio::regions::Region(r), 0.2 / 3., {}}); - adoption_rates.push_back({InfectionState::C, InfectionState::I, mio::regions::Region(r), 0.8 / 3., {}}); - adoption_rates.push_back({InfectionState::I, InfectionState::R, mio::regions::Region(r), 0.99 / 5., {}}); - adoption_rates.push_back({InfectionState::I, InfectionState::D, mio::regions::Region(r), 0.01 / 5., {}}); + adoption_rates.push_back({{InfectionState::E}, {InfectionState::C}, mio::regions::Region(r), 1.0 / 5., {}}); + adoption_rates.push_back({{InfectionState::C}, {InfectionState::R}, mio::regions::Region(r), 0.2 / 3., {}}); + adoption_rates.push_back({{InfectionState::C}, {InfectionState::I}, mio::regions::Region(r), 0.8 / 3., {}}); + adoption_rates.push_back({{InfectionState::I}, {InfectionState::R}, mio::regions::Region(r), 0.99 / 5., {}}); + adoption_rates.push_back({{InfectionState::I}, {InfectionState::D}, mio::regions::Region(r), 0.01 / 5., {}}); } //Set second-order adoption rate different for the two regions // adoption rate has the form {i, j, k, c_{i,j}, {{tau1.state, tau1.factor}, {tau2.state, tau2.factor}}}, see the equation below - adoption_rates.push_back({InfectionState::S, InfectionState::E, mio::regions::Region(0), 0.1, {{InfectionState::C, 1}, {InfectionState::I, 0.5}}}); - adoption_rates.push_back({InfectionState::S, InfectionState::E, mio::regions::Region(1), 0.2, {{InfectionState::C, 1}, {InfectionState::I, 0.5}}}); + adoption_rates.push_back({{InfectionState::S}, {InfectionState::E}, mio::regions::Region(0), 0.1, {{{InfectionState::C}, 1}, {{InfectionState::I}, 0.5}}}); + adoption_rates.push_back({{InfectionState::S}, {InfectionState::E}, mio::regions::Region(1), 0.2, {{{InfectionState::C}, 1}, {{InfectionState::I}, 0.5}}}); //Initialize model parameter - model.parameters.get>() = adoption_rates; - -Sociodemographic stratification -------------------------------- - -Sociodemographic stratification e.g. by age, gender or immunity can be incorporated by stratifying the set of infection states passed as template to the model. + model.parameters.get>() = adoption_rates; Parameters ---------- @@ -110,8 +123,9 @@ with :math:`i^{(k)}` the population in region :math:`k` having infection state : Initial conditions ------------------ -Before running a simulation with the stochastic metapopulation model, the initial populations i.e. the number of agents per infection state for every region have to be set. -These populations have the class type **Populations** and can be set via: +Before running a simulation with the stochastic metapopulation model, the initial populations i.e. the number of agents +per infection state for every region have to be set. +These populations have the class type ``Populations`` and can be set via: .. code-block:: cpp @@ -128,7 +142,8 @@ These populations have the class type **Populations** and can be set via: } If individuals should transition between regions, the spatial transition rates of the model have to be initialized as well. -As the spatial transition rates are dependent on infection state, region changes for specific infection states can be prevented. Below, symmetric spatial transition rates are set for every region: +As the spatial transition rates are dependent on infection state, region changes for specific infection states can be +prevented. Below, symmetric spatial transition rates are set for every region: .. code-block:: cpp @@ -140,27 +155,32 @@ As the spatial transition rates are dependent on infection state, region changes if (i != j) { // transition rate has the form {i, k, l, \lambda^{(k,l)}_{i}} transition_rates.push_back( - {InfectionState(s), mio::regions::Region(i), mio::regions::Region(j), 0.01}); + {{InfectionState(s)}, mio::regions::Region(i), mio::regions::Region(j), 0.01}); transition_rates.push_back( - {InfectionState(s), mio::regions::Region(j), mio::regions::Region(i), 0.01}); + {{InfectionState(s)}, mio::regions::Region(j), mio::regions::Region(i), 0.01}); } } } //Initialize model parameter - model.parameters.get>() = transition_rates; + model.parameters.get>() = transition_rates; Nonpharmaceutical interventions -------------------------------- -There are no nonpharmaceutical interventions (NPIs) explicitly implemented in the model. However, NPIs influencing the adoption or spatial transition rates can be realized by resetting the corresponding model parameters. +There are no nonpharmaceutical interventions (NPIs) explicitly implemented in the model. However, NPIs influencing the +adoption or spatial transition rates can be realized by resetting the corresponding model parameters. Simulation ---------- -At the beginning of the simulation, the waiting times for all events (infection state adoptions and spatial transitions) are drawn. Then the time is advanced until the time point of the next event - which can be a spatial transition or an infection state adoption - and the event takes places. The waiting times of the other events are updated and a new waiting time for the event that just happened is drawn. The simulation saves the system state in discrete time steps. +At the beginning of the simulation, the waiting times for all events (infection state adoptions and spatial transitions) +are drawn. Then the time is advanced until the time point of the next event - which can be a spatial transition or an +infection state adoption - and the event takes places. The waiting times of the other events are updated and a new waiting +time for the event that just happened is drawn. The simulation saves the system state in discrete time steps. -To simulate the model from `t0` to `tmax` with given step size `dt`, a **Simulation** has to be created and advanced until `tmax`. The step size is only used to regularly save the system state during the simulation. +To simulate the model from ``t0`` to ``tmax`` with given step size ``dt``, a **Simulation** has to be created and advanced +until ``tmax``. The step size is only used to regularly save the system state during the simulation. .. code-block:: cpp @@ -193,10 +213,58 @@ If one wants to interpolate the aggregated results to a ``mio::TimeSeries`` cont auto interpolated_results = mio::interpolate_simulation_result(sim.get_result()); + +Demographic Stratification +-------------------------- + +It is possible to add multiple indices to the ``Status`` to differentiate multiple groups on the same region. For example this +could represent the human and the mosquito populations in a specific region. To use this feature, one first of all has to +create a new index: + +.. code-block:: cpp + + struct Species : public mio::Index { + Species(size_t val): Index(val) + { + } + }; + +Then, one has to create the multiindex, where we reuse the ``InfectionState`` defined in the first example: + +.. code-block:: cpp + + using Status = mio::Index; + +Define the size for each index dimension that is not an enum class: + +.. code-block:: cpp + + const size_t num_species = 2; + +We can define a model: + +.. code-block:: cpp + + using Model = mio::smm::Model + Model model(Status{Count, Species(num_species)}, Region(num_regions)); + +Now, for accessing the population, all indexes need to be given: + +-- code-block:: cpp + + model.populations[{Region(r), InfectionState::S, Species(0)}] = 100; + // ... + adoption_rates.push_back({{InfectionState::S, Species(0)}, {InfectionState::E, Species(0)}, Region(r), 0.1, {{InfectionState::I, Species(1)}, 1}); + // ... + transition_rates.push_back({{InfectionState::S, Species(0)}, Region(i), Region(j), 0.01}); + + Examples -------- -An example of the stochastic metapopulation model with four regions can be found at: `examples/smm.cpp `_ +A full example with ``Status`` distributed according to ``InfectionState``, ``Age`` and ``Species`` can be found at +`examples/smm.cpp `_ + Overview of the ``smm`` namespace: From 5058c96b158a4cbd05ee62b37f22f3c5b4ab7b17 Mon Sep 17 00:00:00 2001 From: reneSchm <49305466+reneSchm@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:59:21 +0100 Subject: [PATCH 165/169] polish code docs --- cpp/memilio/epidemiology/adoption_rate.h | 6 ++-- cpp/memilio/epidemiology/populations.h | 7 ++-- cpp/memilio/utils/index.h | 41 ++++++++++++++++-------- cpp/models/smm/model.h | 17 +++++----- cpp/models/smm/parameters.h | 21 ++++++------ 5 files changed, 52 insertions(+), 40 deletions(-) diff --git a/cpp/memilio/epidemiology/adoption_rate.h b/cpp/memilio/epidemiology/adoption_rate.h index 9ad4340d9f..8f72d650d2 100644 --- a/cpp/memilio/epidemiology/adoption_rate.h +++ b/cpp/memilio/epidemiology/adoption_rate.h @@ -29,7 +29,7 @@ namespace mio /** * @brief Struct defining an influence for a second-order adoption. * The population having "status" is multiplied with "factor." - * @tparam Status An infection state enum. + * @tparam Status An infection state enum or MultiIndex. */ template struct Influence { @@ -42,8 +42,8 @@ struct Influence { * The AdoptionRate is considered to be of second-order if there are any "influences". * In the d_abm and smm simulations, "from" is implicitly an influence, scaled by "factor". This is multiplied by * the sum over all "influences", which scale their "status" with the respective "factor". - * @tparam Status An infection state enum. - * @tparam Region An (multi)-index. + * @tparam Status An infection state enum or MultiIndex. + * @tparam Region A MultiIndex. */ template struct AdoptionRate { diff --git a/cpp/memilio/epidemiology/populations.h b/cpp/memilio/epidemiology/populations.h index 6bdefaa841..b702333ad9 100644 --- a/cpp/memilio/epidemiology/populations.h +++ b/cpp/memilio/epidemiology/populations.h @@ -288,12 +288,11 @@ class Populations : public CustomIndexArray, Categories...> }; /** - * @brief Population template specialization for Index types. + * @brief Population template specialization, forwarding categories from a MultiIndex to the Population. * - * @tparam FP Floating point type - * @tparam Categories Index categories + * @tparam FP A floating point type, e.g., double. + * @tparam Categories Index categories. */ - template class Populations> : public Populations { diff --git a/cpp/memilio/utils/index.h b/cpp/memilio/utils/index.h index 8388aaba8b..dcc84d267d 100644 --- a/cpp/memilio/utils/index.h +++ b/cpp/memilio/utils/index.h @@ -45,14 +45,14 @@ void is_multi_index_impl(Index); } // namespace details -/// @brief A MultiIndex is an Index any number of categories. Does accept empty or single category indices. +/// @brief A MultiIndex is an Index with any number of categories. Does accept empty or single category indices. template concept IsMultiIndex = requires(Index i) { details::is_multi_index_impl(i); }; namespace details { -/// @brief Obtain a tuple of singular indices from a Index or MultiIndex. +/// @brief Obtain a tuple of single-category indices from a Index or MultiIndex. template std::tuple...> get_tuple(const Index& i) { @@ -64,7 +64,7 @@ std::tuple...> get_tuple(const Index& i) } } -/// @brief Obtain a tuple of one index from an enum value. +/// @brief Obtain a tuple of one single-category index from an enum value. template std::tuple> get_tuple(Enum i) requires std::is_enum::value @@ -72,13 +72,18 @@ std::tuple> get_tuple(Enum i) return std::tuple(Index(i)); } +/// @brief Merge a series of enums or MultIndex%s into a tuple of single-category indices. template requires((std::is_enum_v || IsMultiIndex) && ...) -decltype(auto) merge_indices_impl(IndexArgs&&... args) +decltype(auto) concatenate_indices_impl(IndexArgs&&... args) { return std::tuple_cat(details::get_tuple(args)...); } +/** + * @brief Function declaration that allows type conversion from a tuple of single-category indices to MultiIndex. + * Used together with concatenate_indices_impl, this allows combining categories of multiple args into a single MultiIndex. + */ template Index tuple_to_index(std::tuple...>); @@ -178,25 +183,28 @@ class Index static constexpr size_t size = sizeof...(CategoryTag); static constexpr bool has_duplicates = has_duplicates_v; + /// @brief Construct an Index filled with zeroes. static Index constexpr Zero() { return Index(Index::Zero()...); } - // constructor from Indices + /// @brief Constructor from individual Indices. Index(Index const&... _indices) : indices{_indices...} { } + /// @brief Constructor from mixed Indices and MultiIndices. template requires(sizeof...(IndexArgs) > 1) Index(IndexArgs&&... subindices) - : indices(details::merge_indices_impl(std::forward(subindices)...)) + : indices(details::concatenate_indices_impl(std::forward(subindices)...)) { } private: + /// @brief Internal constructor from a tuple. Index(const std::tuple...>& _indices) : indices(_indices) { @@ -280,7 +288,7 @@ struct index_of_type, Index> { static constexpr std::size_t value = 0; }; -// retrieves the Index at the Ith position for a Index +/// @brief Retrieves the Index (by reference) at the Ith position of a MultiIndex. template constexpr typename std::tuple_element...>>::type& get(Index& i) noexcept @@ -294,7 +302,7 @@ get(Index& i) noexcept } } -// retrieves the Index at the Ith position for a Index, const version +/// @brief Retrieves the Index (by const reference) at the Ith position of a MultiIndex. template constexpr typename std::tuple_element...>>::type const& get(Index const& i) noexcept @@ -308,7 +316,7 @@ get(Index const& i) noexcept } } -// retrieves the Index for the tag Tag of a Index with more than one Tag +/// @brief Retrieves the Index (by reference) by its Tag in a MultiIndex. Requires unique tags. template constexpr Index& get(Index& i) noexcept { @@ -323,7 +331,7 @@ constexpr Index& get(Index& i) noexcept } } -// retrieves the Index for the tag Tag of a Index with more than one Tag +/// @brief Retrieves the Index (by const reference) by its Tag in a MultiIndex. Requires unique tags. template constexpr Index const& get(Index const& i) noexcept { @@ -338,10 +346,16 @@ constexpr Index const& get(Index const& i) noexcept } } +/** + * @brief Combine several Index%s into one MultiIndex. + * @param args Either enum or MultiIndex values. + * @return A MultiIndex with all categories and values of the given Indices concatonated. + */ template -decltype(auto) merge_indices(IndexArgs&&... args) +decltype(auto) concatenate_indices(IndexArgs&&... args) { - using MergedIndex = decltype(details::tuple_to_index(details::merge_indices_impl(std::declval()...))); + using MergedIndex = + decltype(details::tuple_to_index(details::concatenate_indices_impl(std::declval()...))); return MergedIndex(std::forward(args)...); } @@ -392,6 +406,7 @@ inline Index extend_index_impl(const Index& i, const * @tparam SubIndex An Index that contains a subset of the categories from SuperIndex. * @tparam SuperIndex Any Index. * @return A (sub)index with the given categories and values from index. + * @{ */ template decltype(auto) reduce_index(const SuperIndex& index) @@ -405,13 +420,13 @@ decltype(auto) reduce_index(const SuperIndex& index) return details::reduce_index_impl(index, mio::Tag{}); } } - template requires std::is_enum_v Index reduce_index(const SuperIndex& index) { return details::reduce_index_impl(index, mio::Tag>{}); } +/** @} */ /** * @brief Create a SuperIndex by copying values from SubIndex, filling new categories with fill_value. diff --git a/cpp/models/smm/model.h b/cpp/models/smm/model.h index ac4cd4272e..056878cfb7 100644 --- a/cpp/models/smm/model.h +++ b/cpp/models/smm/model.h @@ -34,17 +34,18 @@ namespace mio namespace smm { -template -using age_group = T; - +/// @brief The Index type used to define the SMM population. template -using PopulationIndex = decltype(merge_indices(std::declval(), std::declval())); +using PopulationIndex = decltype(concatenate_indices(std::declval(), std::declval())); /** * @brief Stochastic Metapopulation Model. - * @tparam regions Number of regions. - * @tparam Status An infection state enum. - * @tparam Region An (multi)-index, default is @ref mio::regions::Region. + * The stratification of the population of this model is split between "Status" and "Region". This split is mostly + * arbitrary, with the important distinction, that for second order rates the reference population + * (i.e., the N in S' = S * I / N) is calculated by accumulating subpopulations only over the Status. + * @tparam Comp An enum representing the infection states. Must also be contained in Status + * @tparam Status A MultiIndex allowing to further stratify infection state adoptions. + * @tparam Region A MultiIndex for "spatially" separate subpopulations, default is @ref mio::regions::Region. */ template class Model : public mio::CompartmentalModel>, @@ -59,7 +60,7 @@ class Model : public mio::CompartmentalModel struct AdoptionRates { @@ -49,10 +48,9 @@ struct AdoptionRates { /** * @brief Struct defining a possible regional transition in a Model based on Poisson Processes. - * - * @tparam FP Floating point type - * @tparam Status An infection state enum. - * @tparam Region An (multi)-index. + * @tparam FP A floating point type, e.g., double. + * @tparam Status A MultiIndex, containing the infection state enum. + * @tparam Region A MultiIndex for spatial stratification. */ template struct TransitionRate { @@ -64,10 +62,9 @@ struct TransitionRate { /** * @brief A vector of TransitionRate%s, see mio::TransitionRate - * - * @tparam FP Floating point type - * @tparam Status An infection state enum. - * @tparam Region An (multi)-index. + * @tparam FP A floating point type, e.g., double. + * @tparam Status A MultiIndex, containing the infection state enum. + * @tparam Region A MultiIndex for spatial stratification. */ template struct TransitionRates { From 1604206826b915a195ba14ae388803166098fda9 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:00:05 +0100 Subject: [PATCH 166/169] CHG: Cleanup --- cpp/examples/asym_ex_data.cpp | 89 +++++-------------------- cpp/examples/asym_graph_ben.cpp | 74 +++++++++----------- cpp/examples/asymmetric_graph.cpp | 17 +++-- cpp/examples/asymmetric_params.cpp | 54 +++++++-------- cpp/memilio/data/analyze_result.h | 25 +++++++ cpp/memilio/mobility/graph_simulation.h | 3 +- cpp/memilio/utils/index.h | 7 -- cpp/models/fmd/infection_state.h | 60 +++++++++++++++++ 8 files changed, 172 insertions(+), 157 deletions(-) create mode 100644 cpp/models/fmd/infection_state.h diff --git a/cpp/examples/asym_ex_data.cpp b/cpp/examples/asym_ex_data.cpp index c196120d1e..c20c3d1a53 100644 --- a/cpp/examples/asym_ex_data.cpp +++ b/cpp/examples/asym_ex_data.cpp @@ -26,38 +26,32 @@ #include "memilio/mobility/graph.h" #include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/index.h" #include "memilio/utils/logging.h" #include "memilio/timer/auto_timer.h" #include "memilio/utils/parameter_distributions.h" #include "memilio/utils/random_number_generator.h" #include "smm/simulation.h" #include "smm/parameters.h" +#include "fmd/infection_state.h" +#include "fmd/model.h" #include "thirdparty/csv.h" #include #include -enum class InfectionState -{ - S, - E, - I, - INS, - ICS, - R, - D, - Count -}; - int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; - const auto tmax = 400.; + const auto tmax = 1000.; const auto dt = 1.; //initial time step + using mio::fmd::InfectionState; + using Status = mio::Index; + using mio::regions::Region; //total compartment sizes - using Model = mio::smm::Model; - auto home = mio::regions::Region(0); + using Model = mio::smm::Model; + auto home = Region(0); auto S = InfectionState::S; auto E = InfectionState::E; auto I = InfectionState::I; @@ -66,7 +60,7 @@ int main(int /*argc*/, char** /*argv*/) auto R = InfectionState::R; auto D = InfectionState::D; - std::vector> adoption_rates; + std::vector> adoption_rates; // Adoption rates corresponding to our model, paramters are arbitrary adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); adoption_rates.push_back({E, I, home, 0.2, {}}); @@ -76,26 +70,16 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({ICS, R, home, 0.4, {}}); adoption_rates.push_back({INS, R, home, 0.5, {}}); - mio::GraphBuilder>, - mio::MobilityEdgeDirected> - builder; + mio::fmd::Builder builder; mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; - io::CSVReader<4> farms("../../../../data/trade_network/example_data_NRW/Data/farms.csv"); + io::CSVReader<4> farms("/home/kilian/Documents/data/read_data/farms.csv"); farms.read_header(io::ignore_extra_column, "id_dec", "x", "y", "farm_size"); size_t farm_id, num_cows; double latitude, longitude; while (farms.read_row(farm_id, latitude, longitude, num_cows)) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; + auto curr_model = mio::fmd::create_model(num_cows, adoption_rates); builder.add_node(farm_id, longitude, latitude, curr_model, t0); } } @@ -103,11 +87,12 @@ int main(int /*argc*/, char** /*argv*/) auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; - interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + interesting_indices.push_back( + {Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({home, InfectionState::I})}); // graph.reserve_edges(262144); { mio::timing::AutoTimer<"Graph Edges Generation"> timer; - io::CSVReader<2> edges("../../../../data/trade_network/example_data_NRW/Data/edges.csv"); + io::CSVReader<2> edges("/home/kilian/Documents/data/read_data/edges.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; while (edges.read_row(from, to)) { @@ -117,21 +102,12 @@ int main(int /*argc*/, char** /*argv*/) auto graph = std::move(builder).build(); mio::log_info("Graph generated"); - auto nodes = graph.nodes() | std::views::transform([](const auto& node) { - return &node.property; - }); - auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); - mio::log_info("RTree generated"); - - for (auto& node : graph.nodes()) { - node.property.set_regional_neighbors( - tree.in_range_indices_query(node.property.get_location(), {mio::geo::kilometers(2.0)})); - } + mio::fmd::generate_neighbours(graph, {mio::geo::kilometers(2.0)}); mio::log_info("Neighbors set"); auto sim = mio::make_mobility_sim(t0, dt, std::move(graph)); - io::CSVReader<4> exchanges("../../../../data/trade_network/example_data_NRW/Data/exchanges.csv"); + io::CSVReader<4> exchanges("/home/kilian/Documents/data/read_data/exchanges.csv"); exchanges.read_header(io::ignore_extra_column, "from_dec", "to_dec", "day", "weight"); size_t date, num_animals; @@ -152,39 +128,10 @@ int main(int /*argc*/, char** /*argv*/) sim2.advance(tmax); mio::log_info("Simulation finished"); - // #ifdef MEMILIO_ENABLE_OPENMP - // } - // #endif - - // sim2.get_graph().nodes()[28].property.get_result().print_table({"S", "E", "I", "R", "D"}); - // std::cout << "Second Table" << std::endl; - // sim2.get_graph().nodes()[1].property.get_result().print_table({"S", "E", "I", "R", "D"}); - - // auto& edge_1_0 = sim2.get_graph().edges()[1]; - // auto& results = edge_1_0.property.get_mobility_results(); - // results.print_table({"Commuter Sick", "Commuter Total"}); - - // auto exchange_results = sim2.sum_exchanges(); - // mio::log_info("Sum of exchanged sick animals: {}", exchange_results[0]); - // mio::log_info("Sum of exchanged animals: {}", exchange_results[1]); - auto sth = sim2.exchanges_per_timestep().export_csv("Exchange_statistics.csv", {"Commuter Sick", "Commuter Total"}); - // for (auto node : sim2.get_graph().nodes()) { - // if (node.property.get_result().get_num_time_points() < num_time_points) { - // mio::log_error("Node with inconsistent number of time points in results."); - // } - // } - - // exchange_results = sim2.sum_nodes(); - // mio::log_info("{}, {}, {}, {}", exchange_results[0], exchange_results[1], exchange_results[2], exchange_results[3]); - sth = sim2.statistics_per_timestep().export_csv("Simulation_statistics.csv"); - // // auto combined_results = sim2.combine_node_results(); - // // combined_results.print_table({"S", "E", "I", "R", "D"}); - // // auto ioresult = combined_results.export_csv("Simulation_results.csv"); - // sim2.statistics_per_timestep({0, 1, 2, 3, 4}).print_table({"S", "E", "I", "R", "D"}); mio::log_info("Finished postprocessing"); return 0; diff --git a/cpp/examples/asym_graph_ben.cpp b/cpp/examples/asym_graph_ben.cpp index 823aa0a4f3..5ab35a9aa1 100644 --- a/cpp/examples/asym_graph_ben.cpp +++ b/cpp/examples/asym_graph_ben.cpp @@ -26,30 +26,20 @@ #include "memilio/mobility/graph.h" #include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/index.h" #include "memilio/utils/logging.h" #include "memilio/timer/auto_timer.h" #include "memilio/utils/parameter_distributions.h" #include "memilio/utils/random_number_generator.h" #include "smm/simulation.h" #include "smm/parameters.h" +#include "fmd/infection_state.h" +#include "fmd/model.h" #include "thirdparty/csv.h" #include #include #include -enum class InfectionState -{ - S, - E, - I, - INS, - ICS, - R, - V, - D, - Count -}; - int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; @@ -57,9 +47,12 @@ int main(int /*argc*/, char** /*argv*/) const auto dt = 1.; //initial time step //total compartment sizes + using mio::fmd::InfectionState; + using Status = mio::Index; + using mio::regions::Region; - using Model = mio::smm::Model; - auto home = mio::regions::Region(0); + using Model = mio::smm::Model; + auto home = Region(0); auto S = InfectionState::S; auto E = InfectionState::E; auto I = InfectionState::I; @@ -68,7 +61,7 @@ int main(int /*argc*/, char** /*argv*/) auto R = InfectionState::R; auto D = InfectionState::D; - std::vector> adoption_rates; + std::vector> adoption_rates; // Adoption rates corresponding to our model, paramters are arbitrary adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); adoption_rates.push_back({S, E, home, 0.0, {}}); @@ -110,30 +103,28 @@ int main(int /*argc*/, char** /*argv*/) to_exchanges.push_back(to); } std::vector> interesting_indices; - interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + interesting_indices.push_back( + {Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({home, InfectionState::I})}); for (size_t num_edges_benchmark = 1024; num_edges_benchmark < 16000000; num_edges_benchmark *= 2) { mio::log_info("Running with {} edges", num_edges_benchmark); - mio::Graph>, - mio::MobilityEdgeDirected> - graph; - mio::GraphBuilder>, - mio::MobilityEdgeDirected> - builder; + mio::fmd::Graph graph; + mio::fmd::Builder builder; auto start = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; + Model curr_model(Status{InfectionState::Count}, Region(1)); + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::V}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; graph.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); } auto rng = mio::RandomNumberGenerator(); @@ -148,15 +139,16 @@ int main(int /*argc*/, char** /*argv*/) duration.count()); start = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; + Model curr_model(Status{InfectionState::Count}, Region(1)); + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::V}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; builder.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); } for (size_t i = 0; i < num_edges_benchmark; ++i) { diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index f9ed4193a8..a430934eb8 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -54,10 +54,12 @@ int main(int /*argc*/, char** /*argv*/) const auto t0 = 0.; const auto tmax = 100.; const auto dt = 1.; //initial time step + using Status = mio::Index; + using mio::regions::Region; //total compartment sizes - using Model = mio::smm::Model; + using Model = mio::smm::Model; auto home = mio::regions::Region(0); auto S = InfectionState::S; auto E = InfectionState::E; @@ -67,7 +69,7 @@ int main(int /*argc*/, char** /*argv*/) auto R = InfectionState::R; auto D = InfectionState::D; - std::vector> adoption_rates; + std::vector> adoption_rates; // Adoption rates corresponding to our model, paramters are arbitrary adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); adoption_rates.push_back({S, E, home, 0.0, {}}); @@ -78,8 +80,9 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({ICS, R, home, 0.4, {}}); adoption_rates.push_back({INS, R, home, 0.5, {}}); - mio::GraphBuilder>, - mio::MobilityEdgeDirected> + mio::GraphBuilder< + mio::LocationNode>, + mio::MobilityEdgeDirected> builder; mio::log_info("Starting Graph generation"); { @@ -89,7 +92,7 @@ int main(int /*argc*/, char** /*argv*/) int farm_id, num_cows; double latitude, longitude; while (farms.read_row(farm_id, num_cows, latitude, longitude)) { - Model curr_model; + Model curr_model(Status{InfectionState::Count}, Region(1)); curr_model.populations[{home, InfectionState::S}] = num_cows; curr_model.populations[{home, InfectionState::E}] = 1; curr_model.populations[{home, InfectionState::I}] = 0; @@ -97,7 +100,7 @@ int main(int /*argc*/, char** /*argv*/) curr_model.populations[{home, InfectionState::ICS}] = 0; curr_model.populations[{home, InfectionState::R}] = 0; curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; + curr_model.parameters.get>() = adoption_rates; builder.add_node(farm_id, longitude, latitude, curr_model, t0); } } @@ -105,7 +108,7 @@ int main(int /*argc*/, char** /*argv*/) auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; - interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + interesting_indices.push_back({Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({home, InfectionState::I})}); io::CSVReader<2> edges("../../edges200000.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index aa61b45cbc..1a376b504f 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -28,6 +28,7 @@ #include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" #include "memilio/utils/custom_index_array.h" +#include "memilio/utils/index.h" #include "memilio/utils/logging.h" #include "memilio/timer/auto_timer.h" #include "memilio/utils/parameter_distributions.h" @@ -36,24 +37,13 @@ #include "memilio/utils/base_dir.h" #include "smm/simulation.h" #include "smm/parameters.h" +#include "fmd/infection_state.h" +#include "fmd/model.h" #include "abm/time.h" #include "thirdparty/csv.h" #include #include -enum class InfectionState -{ - S, - E, - I, - INS, - ICS, - R, - V, - D, - Count -}; - template MPI_Datatype mpi_type(); @@ -120,8 +110,12 @@ int main(int /*argc*/, char** /*argv*/) const size_t num_runs = 100000; - using Model = mio::smm::Model; - auto home = mio::regions::Region(0); + using mio::fmd::InfectionState; + using Status = mio::Index; + using mio::regions::Region; + + using Model = mio::smm::Model; + auto home = Region(0); auto S = InfectionState::S; auto E = InfectionState::E; auto I = InfectionState::I; @@ -130,7 +124,7 @@ int main(int /*argc*/, char** /*argv*/) auto R = InfectionState::R; auto D = InfectionState::D; - std::vector> adoption_rates; + std::vector> adoption_rates; // Adoption rates corresponding to our model, paramters are arbitrary adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); adoption_rates.push_back({S, E, home, 0.0, {}}); @@ -141,9 +135,7 @@ int main(int /*argc*/, char** /*argv*/) adoption_rates.push_back({ICS, R, home, 0.4, {}}); adoption_rates.push_back({INS, R, home, 0.5, {}}); - mio::GraphBuilder>, - mio::MobilityEdgeDirected> - builder; + mio::fmd::Builder builder; // usage: // if (rank==0) v = {1,2,3,4}; @@ -201,21 +193,23 @@ int main(int /*argc*/, char** /*argv*/) } for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model; - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; + Model curr_model(Status{InfectionState::Count}, Region(1)); + curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::V}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; builder.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); } auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; - interesting_indices.push_back({Model().populations.get_flat_index({home, InfectionState::I})}); + interesting_indices.push_back( + {Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({home, InfectionState::I})}); for (size_t i = 0; i < froms.size(); ++i) { builder.add_edge(froms[i], tos[i], interesting_indices); } @@ -308,7 +302,7 @@ int main(int /*argc*/, char** /*argv*/) sim2.infectionrisk = 0.1; } auto index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( - {mio::regions::Region(0), InfectionState::E}); + {Region(0), InfectionState::E}); sim2.get_graph().nodes()[145236].property.get_result().get_last_value()[index] = 100; return sim2; }; diff --git a/cpp/memilio/data/analyze_result.h b/cpp/memilio/data/analyze_result.h index 5e80a05773..55964c37c2 100644 --- a/cpp/memilio/data/analyze_result.h +++ b/cpp/memilio/data/analyze_result.h @@ -335,6 +335,31 @@ std::vector> ensemble_percentile(const std::vector +TimeSeries ensemble_percentile(const std::vector>& ensemble_result, FP p) +{ + assert(p > 0.0 && p < 1.0 && "Invalid percentile value."); + + auto num_runs = ensemble_result.size(); + auto num_time_points = ensemble_result[0].get_num_time_points(); + auto num_elements = ensemble_result[0].get_num_elements(); + + TimeSeries percentile = TimeSeries::zero(num_time_points, num_elements); + + std::vector single_element(num_runs); //reused for each element + for (Eigen::Index time = 0; time < num_time_points; time++) { + percentile.get_time(time) = ensemble_result[0].get_time(time); + for (Eigen::Index elem = 0; elem < num_elements; elem++) { + std::transform(ensemble_result.begin(), ensemble_result.end(), single_element.begin(), [=](auto& run) { + return run[time][elem]; + }); + std::sort(single_element.begin(), single_element.end()); + percentile[time][elem] = single_element[static_cast(num_runs * p)]; + } + } + return percentile; +} + template FP result_distance_2norm(const std::vector>& result1, const std::vector>& result2) diff --git a/cpp/memilio/mobility/graph_simulation.h b/cpp/memilio/mobility/graph_simulation.h index 13b5188709..1264773ec3 100644 --- a/cpp/memilio/mobility/graph_simulation.h +++ b/cpp/memilio/mobility/graph_simulation.h @@ -704,7 +704,8 @@ class AsymmetricGraphSimulation : public GraphSimulationBase; - using AdoptionRate = mio::smm::AdoptionRates; + using AdoptionRate = + mio::smm::AdoptionRates; neighbour_property.get_simulation().get_model().parameters.template get()[1].factor += infectionrisk; } diff --git a/cpp/memilio/utils/index.h b/cpp/memilio/utils/index.h index ccd2417f11..dcc84d267d 100644 --- a/cpp/memilio/utils/index.h +++ b/cpp/memilio/utils/index.h @@ -211,13 +211,6 @@ class Index } public: - template ::value>> - Index(IndexArgs... _index_args) - : indices(details::merge_indices(_index_args...)) - { - } - // comparison operators bool operator==(Index const& other) const { diff --git a/cpp/models/fmd/infection_state.h b/cpp/models/fmd/infection_state.h new file mode 100644 index 0000000000..c60acf75f9 --- /dev/null +++ b/cpp/models/fmd/infection_state.h @@ -0,0 +1,60 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 FMD_INFECTION_STATE_H +#define FMD_INFECTION_STATE_H + +#include "memilio/config.h" +#include "memilio/geography/geolocation.h" +#include "memilio/geography/rtree.h" +#include "memilio/geography/distance.h" +#include "memilio/mobility/graph_simulation.h" +#include "memilio/mobility/metapopulation_mobility_asymmetric.h" +#include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" +#include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/logging.h" +#include "memilio/timer/auto_timer.h" +#include "memilio/utils/parameter_distributions.h" +#include "memilio/utils/random_number_generator.h" +#include "smm/simulation.h" +#include "smm/parameters.h" + +namespace mio +{ +namespace fmd +{ + +enum class InfectionState +{ + S, + E, + I, + INS, + ICS, + R, + V, + D, + Count +}; + +} // namespace fmd +} // namespace mio + +#endif // FMD_INFECTION_STATE_H \ No newline at end of file From 541f430caa26a3a8514375dd28cfe64367563948 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:34:48 +0100 Subject: [PATCH 167/169] CHG: Clean up adoption rates --- cpp/examples/asym_ex_data.cpp | 25 ++------ cpp/examples/asym_graph_ben.cpp | 21 +------ cpp/examples/asymmetric_graph.cpp | 66 +++++---------------- cpp/examples/asymmetric_params.cpp | 31 +--------- cpp/models/fmd/adoption_rates.h | 71 ++++++++++++++++++++++ cpp/models/fmd/model.h | 94 ++++++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 117 deletions(-) create mode 100644 cpp/models/fmd/adoption_rates.h create mode 100644 cpp/models/fmd/model.h diff --git a/cpp/examples/asym_ex_data.cpp b/cpp/examples/asym_ex_data.cpp index c20c3d1a53..1149f2dfc5 100644 --- a/cpp/examples/asym_ex_data.cpp +++ b/cpp/examples/asym_ex_data.cpp @@ -35,6 +35,7 @@ #include "smm/parameters.h" #include "fmd/infection_state.h" #include "fmd/model.h" +#include "fmd/adoption_rates.h" #include "thirdparty/csv.h" #include #include @@ -51,24 +52,6 @@ int main(int /*argc*/, char** /*argv*/) //total compartment sizes using Model = mio::smm::Model; - auto home = Region(0); - auto S = InfectionState::S; - auto E = InfectionState::E; - auto I = InfectionState::I; - auto INS = InfectionState::INS; - auto ICS = InfectionState::ICS; - auto R = InfectionState::R; - auto D = InfectionState::D; - - std::vector> adoption_rates; - // Adoption rates corresponding to our model, paramters are arbitrary - adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); - adoption_rates.push_back({E, I, home, 0.2, {}}); - adoption_rates.push_back({I, INS, home, 0.1, {}}); - adoption_rates.push_back({I, ICS, home, 0.1, {}}); - adoption_rates.push_back({ICS, D, home, 0.6, {}}); - adoption_rates.push_back({ICS, R, home, 0.4, {}}); - adoption_rates.push_back({INS, R, home, 0.5, {}}); mio::fmd::Builder builder; mio::log_info("Starting Graph generation"); @@ -79,7 +62,7 @@ int main(int /*argc*/, char** /*argv*/) size_t farm_id, num_cows; double latitude, longitude; while (farms.read_row(farm_id, latitude, longitude, num_cows)) { - auto curr_model = mio::fmd::create_model(num_cows, adoption_rates); + auto curr_model = mio::fmd::create_model(num_cows, mio::fmd::generic_adoption_rates()); builder.add_node(farm_id, longitude, latitude, curr_model, t0); } } @@ -88,8 +71,8 @@ int main(int /*argc*/, char** /*argv*/) std::vector> interesting_indices; interesting_indices.push_back( - {Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({home, InfectionState::I})}); - // graph.reserve_edges(262144); + {Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({Region(0), InfectionState::I})}); + { mio::timing::AutoTimer<"Graph Edges Generation"> timer; io::CSVReader<2> edges("/home/kilian/Documents/data/read_data/edges.csv"); diff --git a/cpp/examples/asym_graph_ben.cpp b/cpp/examples/asym_graph_ben.cpp index 5ab35a9aa1..005fef597f 100644 --- a/cpp/examples/asym_graph_ben.cpp +++ b/cpp/examples/asym_graph_ben.cpp @@ -35,6 +35,7 @@ #include "smm/parameters.h" #include "fmd/infection_state.h" #include "fmd/model.h" +#include "fmd/adoption_rates.h" #include "thirdparty/csv.h" #include #include @@ -53,24 +54,8 @@ int main(int /*argc*/, char** /*argv*/) using Model = mio::smm::Model; auto home = Region(0); - auto S = InfectionState::S; - auto E = InfectionState::E; - auto I = InfectionState::I; - auto INS = InfectionState::INS; - auto ICS = InfectionState::ICS; - auto R = InfectionState::R; - auto D = InfectionState::D; - - std::vector> adoption_rates; - // Adoption rates corresponding to our model, paramters are arbitrary - adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); - adoption_rates.push_back({S, E, home, 0.0, {}}); - adoption_rates.push_back({E, I, home, 0.2, {}}); - adoption_rates.push_back({I, INS, home, 0.1, {}}); - adoption_rates.push_back({I, ICS, home, 0.1, {}}); - adoption_rates.push_back({ICS, D, home, 0.6, {}}); - adoption_rates.push_back({ICS, R, home, 0.4, {}}); - adoption_rates.push_back({INS, R, home, 0.5, {}}); + + auto adoption_rates = mio::fmd::generic_adoption_rates(); mio::log_info("Reading CSVs"); std::vector latitudes, longitudes; diff --git a/cpp/examples/asymmetric_graph.cpp b/cpp/examples/asymmetric_graph.cpp index a430934eb8..3094da5aa0 100644 --- a/cpp/examples/asymmetric_graph.cpp +++ b/cpp/examples/asymmetric_graph.cpp @@ -26,64 +26,38 @@ #include "memilio/mobility/graph.h" #include "memilio/mobility/graph_builder.h" #include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/index.h" #include "memilio/utils/logging.h" #include "memilio/timer/auto_timer.h" #include "memilio/utils/parameter_distributions.h" #include "memilio/utils/random_number_generator.h" #include "smm/simulation.h" #include "smm/parameters.h" +#include "fmd/infection_state.h" +#include "fmd/model.h" +#include "fmd/adoption_rates.h" #include "thirdparty/csv.h" #include #include -enum class InfectionState -{ - S, - E, - I, - INS, - ICS, - R, - V, - D, - Count -}; - int main(int /*argc*/, char** /*argv*/) { const auto t0 = 0.; const auto tmax = 100.; const auto dt = 1.; //initial time step - using Status = mio::Index; + + using mio::fmd::InfectionState; + using Status = mio::Index; using mio::regions::Region; //total compartment sizes - using Model = mio::smm::Model; - auto home = mio::regions::Region(0); - auto S = InfectionState::S; - auto E = InfectionState::E; - auto I = InfectionState::I; - auto INS = InfectionState::INS; - auto ICS = InfectionState::ICS; - auto R = InfectionState::R; - auto D = InfectionState::D; - - std::vector> adoption_rates; - // Adoption rates corresponding to our model, paramters are arbitrary - adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); - adoption_rates.push_back({S, E, home, 0.0, {}}); - adoption_rates.push_back({E, I, home, 0.2, {}}); - adoption_rates.push_back({I, INS, home, 0.1, {}}); - adoption_rates.push_back({I, ICS, home, 0.1, {}}); - adoption_rates.push_back({ICS, D, home, 0.6, {}}); - adoption_rates.push_back({ICS, R, home, 0.4, {}}); - adoption_rates.push_back({INS, R, home, 0.5, {}}); - - mio::GraphBuilder< - mio::LocationNode>, - mio::MobilityEdgeDirected> - builder; + using Model = mio::smm::Model; + auto home = Region(0); + + auto adoption_rates = mio::fmd::generic_adoption_rates(); + + mio::fmd::Builder builder; mio::log_info("Starting Graph generation"); { mio::timing::AutoTimer<"Graph Nodes Generation"> timer; @@ -92,23 +66,15 @@ int main(int /*argc*/, char** /*argv*/) int farm_id, num_cows; double latitude, longitude; while (farms.read_row(farm_id, num_cows, latitude, longitude)) { - Model curr_model(Status{InfectionState::Count}, Region(1)); - curr_model.populations[{home, InfectionState::S}] = num_cows; - curr_model.populations[{home, InfectionState::E}] = 1; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - builder.add_node(farm_id, longitude, latitude, curr_model, t0); + builder.add_node(farm_id, longitude, latitude, mio::fmd::create_model(num_cows, adoption_rates), t0); } } mio::log_info("Nodes added to Graph"); auto rng = mio::RandomNumberGenerator(); std::vector> interesting_indices; - interesting_indices.push_back({Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({home, InfectionState::I})}); + interesting_indices.push_back( + {Model(Status{InfectionState::Count}, Region(1)).populations.get_flat_index({home, InfectionState::I})}); io::CSVReader<2> edges("../../edges200000.csv"); edges.read_header(io::ignore_extra_column, "from", "to"); size_t from, to; diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index 1a376b504f..fb15caf9f3 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -39,6 +39,7 @@ #include "smm/parameters.h" #include "fmd/infection_state.h" #include "fmd/model.h" +#include "fmd/adoption_rates.h" #include "abm/time.h" #include "thirdparty/csv.h" #include @@ -116,24 +117,8 @@ int main(int /*argc*/, char** /*argv*/) using Model = mio::smm::Model; auto home = Region(0); - auto S = InfectionState::S; - auto E = InfectionState::E; - auto I = InfectionState::I; - auto INS = InfectionState::INS; - auto ICS = InfectionState::ICS; - auto R = InfectionState::R; - auto D = InfectionState::D; - std::vector> adoption_rates; - // Adoption rates corresponding to our model, paramters are arbitrary - adoption_rates.push_back({S, E, home, 0.2, {{I, 0.8}, {INS, 0.1}, {ICS, 0.5}}}); - adoption_rates.push_back({S, E, home, 0.0, {}}); - adoption_rates.push_back({E, I, home, 0.2, {}}); - adoption_rates.push_back({I, INS, home, 0.1, {}}); - adoption_rates.push_back({I, ICS, home, 0.1, {}}); - adoption_rates.push_back({ICS, D, home, 0.6, {}}); - adoption_rates.push_back({ICS, R, home, 0.4, {}}); - adoption_rates.push_back({INS, R, home, 0.5, {}}); + auto adoption_rates = mio::fmd::generic_adoption_rates(); mio::fmd::Builder builder; @@ -193,17 +178,7 @@ int main(int /*argc*/, char** /*argv*/) } for (size_t i = 0; i < farm_ids.size(); ++i) { - Model curr_model(Status{InfectionState::Count}, Region(1)); - curr_model.populations[{home, InfectionState::S}] = num_cows_vec[i]; - curr_model.populations[{home, InfectionState::E}] = 0; - curr_model.populations[{home, InfectionState::I}] = 0; - curr_model.populations[{home, InfectionState::INS}] = 0; - curr_model.populations[{home, InfectionState::ICS}] = 0; - curr_model.populations[{home, InfectionState::R}] = 0; - curr_model.populations[{home, InfectionState::V}] = 0; - curr_model.populations[{home, InfectionState::D}] = 0; - curr_model.parameters.get>() = adoption_rates; - builder.add_node(farm_ids[i], longitudes[i], latitudes[i], curr_model, t0); + builder.add_node(farm_ids[i], longitudes[i], latitudes[i], mio::fmd::create_model(num_cows_vec[i], adoption_rates), t0); } auto rng = mio::RandomNumberGenerator(); diff --git a/cpp/models/fmd/adoption_rates.h b/cpp/models/fmd/adoption_rates.h new file mode 100644 index 0000000000..a52bb6c2e2 --- /dev/null +++ b/cpp/models/fmd/adoption_rates.h @@ -0,0 +1,71 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 FMD_ADOPTION_RATES_H +#define FMD_ADOPTION_RATES_H + +#include "memilio/config.h" +#include "memilio/geography/geolocation.h" +#include "memilio/geography/rtree.h" +#include "memilio/geography/distance.h" +#include "memilio/mobility/graph_simulation.h" +#include "memilio/mobility/metapopulation_mobility_asymmetric.h" +#include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" +#include "memilio/utils/compiler_diagnostics.h" +#include "memilio/utils/logging.h" +#include "memilio/timer/auto_timer.h" +#include "memilio/utils/parameter_distributions.h" +#include "memilio/utils/random_number_generator.h" +#include "smm/simulation.h" +#include "smm/parameters.h" +#include "fmd/infection_state.h" +#include + +namespace mio +{ +namespace fmd +{ +using Status = mio::Index; +using mio::regions::Region; + +using AR = mio::AdoptionRate; + +std::vector generic_adoption_rates() +{ + auto home = Region(0); + std::vector adoption_rates; + adoption_rates.push_back({InfectionState::S, + InfectionState::E, + home, + 0.2, + {{InfectionState::I, 0.8}, {InfectionState::INS, 0.1}, {InfectionState::ICS, 0.5}}}); + adoption_rates.push_back({InfectionState::E, InfectionState::I, home, 0.2, {}}); + adoption_rates.push_back({InfectionState::I, InfectionState::INS, home, 0.1, {}}); + adoption_rates.push_back({InfectionState::I, InfectionState::ICS, home, 0.1, {}}); + adoption_rates.push_back({InfectionState::ICS, InfectionState::D, home, 0.6, {}}); + adoption_rates.push_back({InfectionState::ICS, InfectionState::R, home, 0.4, {}}); + adoption_rates.push_back({InfectionState::INS, InfectionState::R, home, 0.5, {}}); + return adoption_rates; +} + +} // namespace fmd +} // namespace mio + +#endif // FMD_ADOPTION_RATES_H \ No newline at end of file diff --git a/cpp/models/fmd/model.h b/cpp/models/fmd/model.h new file mode 100644 index 0000000000..681e749b0d --- /dev/null +++ b/cpp/models/fmd/model.h @@ -0,0 +1,94 @@ +/* +* Copyright (C) 2020-2025 MEmilio +* +* Authors: Kilian Volmer +* +* 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 FMD_MODEL_H +#define FMD_MODEL_H + +#include "memilio/config.h" +#include "memilio/geography/rtree.h" +#include "memilio/geography/distance.h" +#include "memilio/mobility/metapopulation_mobility_asymmetric.h" +#include "memilio/mobility/graph.h" +#include "memilio/mobility/graph_builder.h" +#include "smm/simulation.h" +#include "smm/parameters.h" +#include "fmd/infection_state.h" +#include "fmd/adoption_rates.h" + +namespace mio +{ +namespace fmd +{ + +using Model = mio::smm::Model; + +using Builder = mio::GraphBuilder< + mio::LocationNode>, + mio::MobilityEdgeDirected>; +using Graph = mio::Graph< + mio::LocationNode>, + mio::MobilityEdgeDirected>; + +/** + * @brief Create a model object with given number of susceptible individuals and adoption rates. + * + * @param num_sus Number of susceptible individuals. + * @param adoption_rates Vector of adoption rates. + * @return Model The created model. + */ +Model create_model(size_t num_sus, const std::vector& adoption_rates = generic_adoption_rates()) +{ + auto home = mio::regions::Region(0); + Model curr_model(Status{InfectionState::Count}, mio::regions::Region(1)); + curr_model.populations[{home, InfectionState::S}] = num_sus; + curr_model.populations[{home, InfectionState::E}] = 0; + curr_model.populations[{home, InfectionState::I}] = 0; + curr_model.populations[{home, InfectionState::INS}] = 0; + curr_model.populations[{home, InfectionState::ICS}] = 0; + curr_model.populations[{home, InfectionState::R}] = 0; + curr_model.populations[{home, InfectionState::V}] = 0; + curr_model.populations[{home, InfectionState::D}] = 0; + curr_model.parameters.get>() = adoption_rates; + return curr_model; +} + +/** + * @brief Generate and set regional neighbors for all nodes in the graph based on given distances. + * + * The function constructs an RTree from the node locations and queries it for neighbors within the specified distances. + * + * @param graph The graph for which to generate neighbors. + * @param distances The distances to consider for neighbor generation. + */ +void generate_neighbours(Graph& graph, std::vector distances) +{ + auto nodes = graph.nodes() | std::views::transform([](const auto& node) { + return &node.property; + }); + auto tree = mio::geo::RTree(nodes.begin(), nodes.end()); + + for (auto& node : graph.nodes()) { + node.property.set_regional_neighbors(tree.in_range_indices_query(node.property.get_location(), distances)); + } +} + +} // namespace fmd +} // namespace mio + +#endif // FMD_MODEL_H \ No newline at end of file From 39e4c05a696be0bbd946cd0543417b39ccd18d56 Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:00:16 +0100 Subject: [PATCH 168/169] CHG: different arbitrary adoption rates --- cpp/models/fmd/adoption_rates.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/models/fmd/adoption_rates.h b/cpp/models/fmd/adoption_rates.h index a52bb6c2e2..7a53797d09 100644 --- a/cpp/models/fmd/adoption_rates.h +++ b/cpp/models/fmd/adoption_rates.h @@ -55,7 +55,7 @@ std::vector generic_adoption_rates() InfectionState::E, home, 0.2, - {{InfectionState::I, 0.8}, {InfectionState::INS, 0.1}, {InfectionState::ICS, 0.5}}}); + {{InfectionState::I, 0.3}, {InfectionState::INS, 0.3}, {InfectionState::ICS, 0.8}}}); adoption_rates.push_back({InfectionState::E, InfectionState::I, home, 0.2, {}}); adoption_rates.push_back({InfectionState::I, InfectionState::INS, home, 0.1, {}}); adoption_rates.push_back({InfectionState::I, InfectionState::ICS, home, 0.1, {}}); @@ -68,4 +68,4 @@ std::vector generic_adoption_rates() } // namespace fmd } // namespace mio -#endif // FMD_ADOPTION_RATES_H \ No newline at end of file +#endif // FMD_ADOPTION_RATES_Hs \ No newline at end of file From 11db1d21c25b35fc3db204614ae9635bdcbdcf0a Mon Sep 17 00:00:00 2001 From: Kilian Volmer <13285635+kilianvolmer@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:07:51 +0100 Subject: [PATCH 169/169] CHG: Change params to search for infections via trade network --- cpp/examples/asymmetric_params.cpp | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/cpp/examples/asymmetric_params.cpp b/cpp/examples/asymmetric_params.cpp index fb15caf9f3..2b1d2ccd12 100644 --- a/cpp/examples/asymmetric_params.cpp +++ b/cpp/examples/asymmetric_params.cpp @@ -178,7 +178,8 @@ int main(int /*argc*/, char** /*argv*/) } for (size_t i = 0; i < farm_ids.size(); ++i) { - builder.add_node(farm_ids[i], longitudes[i], latitudes[i], mio::fmd::create_model(num_cows_vec[i], adoption_rates), t0); + builder.add_node(farm_ids[i], longitudes[i], latitudes[i], + mio::fmd::create_model(num_cows_vec[i], adoption_rates), t0); } auto rng = mio::RandomNumberGenerator(); @@ -273,18 +274,28 @@ int main(int /*argc*/, char** /*argv*/) auto get_simulation = [](const auto& sim, ScalarType, ScalarType, size_t run_id) { auto sim2 = sim; sim2.get_rng() = mio::thread_local_rng(); - if (run_id % 2 == 0) { - sim2.infectionrisk = 0.1; - } - auto index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( + int index_farm = mio::UniformIntDistribution::get_instance()(sim2.get_rng(), 0, + int(sim2.get_graph().nodes().size() - 1)); + auto E_index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( {Region(0), InfectionState::E}); - sim2.get_graph().nodes()[145236].property.get_result().get_last_value()[index] = 100; + auto S_index = sim2.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( + {Region(0), InfectionState::S}); + auto num_sus = sim2.get_graph().nodes()[index_farm].property.get_result().get_last_value()[S_index]; + auto num_exposed = std::ceil(0.01 * num_sus); + sim2.get_graph().nodes()[index_farm].property.get_result().get_last_value()[E_index] = num_exposed; + sim2.get_graph().nodes()[index_farm].property.get_result().get_last_value()[S_index] = num_sus - num_exposed; + mio::log_info("Run {}: Infecting {} individuals at node {}.", run_id, num_exposed, index_farm); return sim2; }; auto handle_result = [](auto&& sim, auto&& run) { - auto abs_path = mio::path_join(mio::base_dir(), "example_results"); - auto result = sim.statistics_per_timestep().export_csv( - mio::path_join(abs_path, "AsymmetricParams_run" + std::to_string(run) + ".csv")); + auto infection_numbers = sim.sum_nodes(); + auto dead_index = sim.get_graph().nodes()[0].property.get_simulation().get_model().populations.get_flat_index( + {Region(0), InfectionState::D}); + if (infection_numbers[dead_index] > 50) { + auto abs_path = mio::path_join(mio::base_dir(), "example_results"); + auto result = sim.statistics_per_timestep().export_csv( + mio::path_join(abs_path, "AsymmetricParams_run" + std::to_string(run) + ".csv")); + } return 0; }; auto result = study.run(get_simulation, handle_result);