From a88648f6e0953f71e167c0c1ee74c45262ef0572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Joachim=20K=C3=BChn?= Date: Sun, 29 Mar 2026 19:19:24 +0200 Subject: [PATCH 01/19] [ci skip] interval removed (partially), new options started --- cpp/benchmarks/flow_simulation_ode_secirvvs.h | 53 ++--- cpp/examples/ode_secirvvs.cpp | 1 - cpp/memilio/epidemiology/dynamic_npis.h | 97 +++++++-- cpp/models/ode_secir/analyze_result.h | 105 ++++----- cpp/models/ode_secir/model.h | 47 ++--- cpp/models/ode_secir/parameter_space.h | 1 - cpp/models/ode_secir/parameters.h | 42 +--- cpp/models/ode_secirts/analyze_result.h | 199 ++++++++++-------- cpp/models/ode_secirts/model.h | 49 ++--- cpp/models/ode_secirts/parameters.h | 31 +-- cpp/models/ode_secirvvs/model.h | 49 ++--- cpp/models/ode_secirvvs/parameters.h | 43 +--- cpp/tests/test_odesecir.cpp | 6 - cpp/tests/test_odesecirvvs.cpp | 11 +- docs/source/cpp/models/osecir.rst | 11 +- docs/source/cpp/models/osecirts.rst | 2 +- docs/source/cpp/models/osecirvvs.rst | 5 +- .../bindings/epidemiology/dynamic_npis.h | 6 +- 18 files changed, 343 insertions(+), 415 deletions(-) diff --git a/cpp/benchmarks/flow_simulation_ode_secirvvs.h b/cpp/benchmarks/flow_simulation_ode_secirvvs.h index c1587283a0..a6bcc3e75f 100644 --- a/cpp/benchmarks/flow_simulation_ode_secirvvs.h +++ b/cpp/benchmarks/flow_simulation_ode_secirvvs.h @@ -42,8 +42,8 @@ class FlowlessModel : public CompartmentalModel, - osecirvvs::Parameters>; + mio::Populations, + osecirvvs::Parameters>; public: FlowlessModel(const Populations& pop, const ParameterSet& params) @@ -549,14 +549,11 @@ class Simulation : public Base this->get_model().parameters.template get>(); ScalarType delay_npi_implementation; - auto t = Base::get_result().get_last_time(); - const auto dt = dyn_npis.get_interval().get(); + auto t = Base::get_result().get_last_time(); while (t < tmax) { - auto dt_eff = std::min({dt, tmax - t, m_t_last_npi_check + dt - t}); - if (dt_eff >= 1.0) { - dt_eff = 1.0; - } + auto dt_eff = min(dt, tmax - t); + dt_eff = min(dt_eff, 1.0); if (t == 0) { //this->apply_vaccination(t); // done in init now? @@ -577,32 +574,26 @@ class Simulation : public Base t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (floating_point_greater_equal(t, m_t_last_npi_check + dt)) { - if (t < t_end_dyn_npis) { - auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * - dyn_npis.get_base_value(); - auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); - if (exceeded_threshold != dyn_npis.get_thresholds().end() && - (exceeded_threshold->first > m_dynamic_npi.first || - t > ScalarType(m_dynamic_npi.second))) { //old npi was weaker or is expired - - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); - this->get_model().parameters.get_start_commuter_detection() = t_start.get(); - this->get_model().parameters.get_end_commuter_detection() = t_end.get(); - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); - } + if (t < t_end_dyn_npis) { + auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * + dyn_npis.get_base_value(); + auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); + if (exceeded_threshold != dyn_npis.get_thresholds().end() && + (exceeded_threshold->first > m_dynamic_npi.first || + t > ScalarType(m_dynamic_npi.second))) { //old npi was weaker or is expired + + auto t_start = SimulationTime(t + delay_npi_implementation); + auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); + this->get_model().parameters.get_start_commuter_detection() = t_start.get(); + this->get_model().parameters.get_end_commuter_detection() = t_end.get(); + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); } - m_t_last_npi_check = t; } } - else { - m_t_last_npi_check = t; - } } this->get_model().parameters.template get>() = diff --git a/cpp/examples/ode_secirvvs.cpp b/cpp/examples/ode_secirvvs.cpp index cfdd39b19d..0dc318cbb3 100644 --- a/cpp/examples/ode_secirvvs.cpp +++ b/cpp/examples/ode_secirvvs.cpp @@ -79,7 +79,6 @@ int main() .get>()[{(mio::AgeGroup)0, mio::SimulationDay(i)}] = num_vaccinations; } - model.parameters.get>() = 7; auto& contacts = model.parameters.get>(); auto& contact_matrix = contacts.get_cont_freq_mat(); diff --git a/cpp/memilio/epidemiology/dynamic_npis.h b/cpp/memilio/epidemiology/dynamic_npis.h index 02467fa3c7..d118b2e083 100644 --- a/cpp/memilio/epidemiology/dynamic_npis.h +++ b/cpp/memilio/epidemiology/dynamic_npis.h @@ -97,22 +97,23 @@ class DynamicNPIs } /** - * Get/Set the interval at which the NPIs are checked. + * Get/Set the implementation delay at which the NPIs are implemented after threshold exceedance. + * This parameters imitates delayed reaction times when automatic implementations should be realized. * @{ */ /** - * @return the interval at which the NPIs are checked. + * @return the implementation delay after which the NPIs is implemented upon threshold exceedance. */ - SimulationTime get_interval() const + SimulationTime get_implementation_delay() const { - return m_interval; + return m_delay; } /** - * @param interval The interval at which the NPIs are checked. + * @param delay The implementation delay after which the NPIs is implemented upon threshold exceedance. */ - void set_interval(SimulationTime interval) + void set_implementation_delay(SimulationTime delay) { - m_interval = interval; + m_delay = delay; } /**@}*/ @@ -130,7 +131,7 @@ class DynamicNPIs return m_base; } /** - * @return The base value of the thresholds. + * @param v Sets the base value of the thresholds. */ void set_base_value(FP v) { @@ -138,6 +139,48 @@ class DynamicNPIs } /**@}*/ + /** + * Get/Set the first day of the simulation for which a DynamicNPI *can* be activated. + * This parameter imitates the beginning date of a legal directive. + * @{ + */ + /** + * @return the first day of a legal directive prescribing the DynamicNPI. + */ + SimulationTime get_directive_begin() const + { + return m_directive_begin; + } + /** + * @param begin The first day of a legal directive prescribing the DynamicNPI. + */ + void set_directive_begin(SimulationTime begin) + { + m_directive_begin = begin; + } + /**@}*/ + + /** + * Get/Set the first day of the simulation for which a DynamicNPI *can* be active. + * This parameter imitates the last date of a legal directive and ends all active DynamicNPIs. + * @{ + */ + /** + * @return the last day of a legal directive prescribing the DynamicNPI. + */ + SimulationTime get_directive_end() const + { + return m_directive_end; + } + /** + * @param end The last day of a legal directive prescribing the DynamicNPI. + */ + void set_directive_end(SimulationTime end) + { + m_directive_end = end; + } + /**@}*/ + /** * draw a random sample from the damping distributions */ @@ -160,8 +203,10 @@ class DynamicNPIs auto obj = io.create_object("DynamicNPIs"); obj.add_list("Thresholds", get_thresholds().begin(), get_thresholds().end()); obj.add_element("Duration", get_duration()); - obj.add_element("Interval", get_interval()); + obj.add_element("Delay", get_implementation_delay()); obj.add_element("BaseValue", get_base_value()); + obj.add_element("DirectiveBegin", get_directive_begin()); + obj.add_element("DirectiveEnd", get_directive_end()); } /** @@ -174,27 +219,33 @@ class DynamicNPIs auto obj = io.expect_object("DynamicNPIs"); auto t = obj.expect_list("Thresholds", Tag>>>{}); auto d = obj.expect_element("Duration", Tag>{}); - auto i = obj.expect_element("Interval", Tag>{}); + auto i = obj.expect_element("Delay", Tag>{}); auto b = obj.expect_element("BaseValue", Tag{}); + auto f = obj.expect_element("DirectiveBegin", Tag>{}); + auto l = obj.expect_element("DirectiveEnd", Tag>{}); return apply( io, - [](auto&& t_, auto&& d_, auto&& i_, auto&& b_) { + [](auto&& t_, auto&& d_, auto&& i_, auto&& b_, auto&& f_, auto&& l_) { auto npis = DynamicNPIs(); npis.set_duration(d_); - npis.set_interval(i_); + npis.set_implementation_delay(i_); npis.set_base_value(b_); for (auto&& e : t_) { npis.set_threshold(e.first, e.second); } + npis.set_directive_begin(f_); + npis.set_directive_end(l_); return npis; }, - t, d, i, b); + t, d, i, b, f, l); } private: std::vector>>> m_thresholds; SimulationTime m_duration{14.0}; - SimulationTime m_interval{3.0}; + SimulationTime m_delay{0.0}; + SimulationTime m_directive_begin{-(FP)std::numeric_limits::max}; + SimulationTime m_directive_end{(FP)std::numeric_limits::max}; FP m_base{1.0}; }; @@ -292,13 +343,13 @@ void implement_dynamic_npis(DampingExprGroup& damping_expr_group, const std::vec auto npi_implemented = false; - //add begin of npi if not already bigger + // add begin of npi if not already bigger if ((active.array() < value.array()).any()) { damping_expr.add_damping(max(value, active), level, type, begin); npi_implemented = true; } - //replace dampings during the new npi + // replace dampings during the new npi auto damping_indices = get_damping_indices(damping_expr, level, type, begin, end); for (auto& i : damping_indices) { auto& d = damping_expr.get_dampings()[i]; @@ -306,27 +357,27 @@ void implement_dynamic_npis(DampingExprGroup& damping_expr_group, const std::vec npi_implemented = true; } - //add end of npi to restore active dampings if any change was made + // add end of npi to restore active dampings if any change was made if (npi_implemented) { damping_expr.add_damping(active_end, level, type, end); } } } - //remove duplicates that accumulated because of dampings that become active during the time span - //a damping is obsolete if the most recent damping of the same type and level has the same value + // remove duplicates that accumulated because of dampings that become active during the time span + // a damping is obsolete if the most recent damping of the same type and level has the same value for (auto& damping_expr : damping_expr_group) { - //go from the back so indices aren't invalidated when dampings are removed - //use indices to loop instead of reverse iterators because removing invalidates the current iterator + // go from the back so indices aren't invalidated when dampings are removed + // use indices to loop instead of reverse iterators because removing invalidates the current iterator for (auto i = int(0); i < int(damping_expr.get_dampings().size()) - 1; ++i) { auto it = damping_expr.get_dampings().rbegin() + i; - //look for previous damping of the same type/level + // look for previous damping of the same type/level auto it_prev = std::find_if(it + 1, damping_expr.get_dampings().rend(), [&di = *it](auto& dj) { return di.get_level() == dj.get_level() && di.get_type() == dj.get_type(); }); - //remove if match is found and has same value + // remove if match is found and has same value if (it_prev != damping_expr.get_dampings().rend() && it->get_coeffs() == it_prev->get_coeffs()) { damping_expr.remove_damping(damping_expr.get_dampings().size() - 1 - i); } diff --git a/cpp/models/ode_secir/analyze_result.h b/cpp/models/ode_secir/analyze_result.h index 9e035d2b4c..3c8acfcc40 100644 --- a/cpp/models/ode_secir/analyze_result.h +++ b/cpp/models/ode_secir/analyze_result.h @@ -63,62 +63,65 @@ std::vector ensemble_params_percentile(const std::vector auto& { - return model.populations[{i, (InfectionState)compart}]; - }); + param_percentil( + node, [ compart, i ](auto&& model) -> auto& { + return model.populations[{i, (InfectionState)compart}]; + }); } // times - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { return model.parameters.template get>()[i]; }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); //probs - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { return model.parameters.template get>()[i]; }); + param_percentil( + node, [i](auto&& model) -> auto& { return model.parameters.template get>()[i]; }); } // group independent params - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); for (size_t run = 0; run < num_runs; run++) { auto const& params = ensemble_params[run][node]; diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index 7b4fb2df30..36f73adcfb 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -292,49 +292,40 @@ class Simulation : public BaseT auto& dyn_npis = this->get_model().parameters.template get>(); auto& contact_patterns = this->get_model().parameters.template get>(); - FP delay_npi_implementation; // delay which is needed to implement a NPI that is criterion-dependent - FP t = BaseT::get_result().get_last_time(); - const FP dt = dyn_npis.get_thresholds().size() > 0 ? FP(dyn_npis.get_interval().get()) : FP(tmax); + FP t = BaseT::get_result().get_last_time(); while (t < tmax) { FP dt_eff = min(dt, tmax - t); - dt_eff = min(dt_eff, m_t_last_npi_check + dt - t); BaseT::advance(t + dt_eff); if (t > 0) { delay_npi_implementation = this->get_model().parameters.template get>(); } - else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. + else { // DynamicNPIs for t=0 are 'misused' to be 'from-start NPIs'. I.e., do not enforce delay. delay_npi_implementation = 0; } t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (floating_point_greater_equal(t, m_t_last_npi_check + dt)) { - if (t < t_end_dyn_npis) { - auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * - dyn_npis.get_base_value(); - auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); - if (exceeded_threshold != dyn_npis.get_thresholds().end() && - (exceeded_threshold->first > m_dynamic_npi.first || - t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired - - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(FP(dyn_npis.get_duration())); - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); - } + if (t < t_end_dyn_npis) { + auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * + dyn_npis.get_base_value(); + auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); + if (exceeded_threshold != dyn_npis.get_thresholds().end() && + (exceeded_threshold->first > m_dynamic_npi.first || + t > FP(m_dynamic_npi.second))) { // old npi was weaker or is expired + + auto t_start = SimulationTime(t + delay_npi_implementation); + auto t_end = t_start + SimulationTime(FP(dyn_npis.get_duration())); + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); } - m_t_last_npi_check = t; } } - else { - m_t_last_npi_check = t; - } } return this->get_result().get_last_value(); @@ -678,8 +669,8 @@ auto get_mobility_factors(const Simulation& sim, FP /*t*/, const Eigen auto test_and_trace_capacity = FP(params.template get>()); auto test_and_trace_capacity_max_risk = FP(params.template get>()); auto riskFromInfectedSymptomatic = smoother_cosine(test_and_trace_required, test_and_trace_capacity, - test_and_trace_capacity * test_and_trace_capacity_max_risk, - p_inf.matrix(), p_inf_max.matrix()); + test_and_trace_capacity * test_and_trace_capacity_max_risk, + p_inf.matrix(), p_inf_max.matrix()); //set factor for infected auto factors = Eigen::VectorX::Ones(y.rows()).eval(); diff --git a/cpp/models/ode_secir/parameter_space.h b/cpp/models/ode_secir/parameter_space.h index 2ac6ddab5c..52714c4435 100644 --- a/cpp/models/ode_secir/parameter_space.h +++ b/cpp/models/ode_secir/parameter_space.h @@ -70,7 +70,6 @@ void set_params_distributions_normal(Model& model, FP t0, FP tmax, FP dev_re set_distribution(model.parameters.template get>(), 0.0); set_distribution(model.parameters.template get>()); set_distribution(model.parameters.template get>()); - set_distribution(model.parameters.template get>()); // populations for (auto i = AgeGroup(0); i < model.parameters.get_num_groups(); i++) { diff --git a/cpp/models/ode_secir/parameters.h b/cpp/models/ode_secir/parameters.h index c276e0b9a7..f2292a7eed 100644 --- a/cpp/models/ode_secir/parameters.h +++ b/cpp/models/ode_secir/parameters.h @@ -333,22 +333,6 @@ struct DynamicNPIsInfectedSymptoms { } }; -/** - * @brief The delay with which DynamicNPIs are implemented and enforced after exceedance of threshold. - */ -template -struct DynamicNPIsImplementationDelay { - using Type = UncertainValue; - static Type get_default(AgeGroup /*size*/) - { - return Type(0.0); - } - static std::string name() - { - return "DynamicNPIsImplementationDelay"; - } -}; - /** * @brief capacity to test and trace contacts of infected for quarantine per day. */ @@ -382,14 +366,12 @@ struct TestAndTraceCapacityMaxRisk { }; template -using ParametersBase = - ParameterSet, Seasonality, ICUCapacity, TestAndTraceCapacity, - TestAndTraceCapacityMaxRisk, ContactPatterns, DynamicNPIsImplementationDelay, - DynamicNPIsInfectedSymptoms, TimeExposed, TimeInfectedNoSymptoms, TimeInfectedSymptoms, - TimeInfectedSevere, TimeInfectedCritical, TransmissionProbabilityOnContact, - RelativeTransmissionNoSymptoms, RecoveredPerInfectedNoSymptoms, - RiskOfInfectionFromSymptomatic, MaxRiskOfInfectionFromSymptomatic, - SeverePerInfectedSymptoms, CriticalPerSevere, DeathsPerCritical>; +using ParametersBase = ParameterSet< + StartDay, Seasonality, ICUCapacity, TestAndTraceCapacity, TestAndTraceCapacityMaxRisk, + ContactPatterns, DynamicNPIsInfectedSymptoms, TimeExposed, TimeInfectedNoSymptoms, + TimeInfectedSymptoms, TimeInfectedSevere, TimeInfectedCritical, TransmissionProbabilityOnContact, + RelativeTransmissionNoSymptoms, RecoveredPerInfectedNoSymptoms, RiskOfInfectionFromSymptomatic, + MaxRiskOfInfectionFromSymptomatic, SeverePerInfectedSymptoms, CriticalPerSevere, DeathsPerCritical>; /** * @brief Parameters of an age-resolved SECIR/SECIHURD model. @@ -491,13 +473,6 @@ class Parameters : public ParametersBase corrected = true; } - if (this->template get>() < 0.0) { - log_warning("Constraint check: Parameter DynamicNPIsImplementationDelay changed from {} to {}", - this->template get>(), 0); - this->template set>(0); - corrected = true; - } - if (this->template get>() < 0.0) { log_warning("Constraint check: Parameter TestAndTraceCapacity changed from {} to {}", this->template get>(), 0); @@ -643,11 +618,6 @@ class Parameters : public ParametersBase return true; } - if (this->template get>() < 0.0) { - log_error("Constraint check: Parameter DynamicNPIsImplementationDelay smaller {}", 0); - return true; - } - const FP tol_times = 1e-1; // accepted tolerance for compartment stays for (auto i = AgeGroup(0); i < AgeGroup(m_num_groups); ++i) { diff --git a/cpp/models/ode_secirts/analyze_result.h b/cpp/models/ode_secirts/analyze_result.h index 4970ae932f..496e51e014 100644 --- a/cpp/models/ode_secirts/analyze_result.h +++ b/cpp/models/ode_secirts/analyze_result.h @@ -72,109 +72,124 @@ std::vector ensemble_params_percentile(const std::vector(0); compart < InfectionState::Count; ++compart) { - param_percentil(node, [compart, i](auto&& model) -> auto& { - return model.populations[{i, compart}]; - }); + param_percentil( + node, [ compart, i ](auto&& model) -> auto& { + return model.populations[{i, compart}]; + }); } // times - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { return model.parameters.template get>()[i]; }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); //probs - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { return model.parameters.template get>()[i]; }); + param_percentil( + node, [i](auto&& model) -> auto& { return model.parameters.template get>()[i]; }); //vaccinations - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - - for (auto day = SimulationDay(0); day < num_days; ++day) { - param_percentil(node, [i, day](auto&& model) -> auto& { - return model.parameters.template get>()[{i, day}]; + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; }); - param_percentil(node, [i, day](auto&& model) -> auto& { - return model.parameters.template get>()[{i, day}]; + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; }); - param_percentil(node, [i, day](auto&& model) -> auto& { - return model.parameters.template get>()[{i, day}]; + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + + for (auto day = SimulationDay(0); day < num_days; ++day) { + param_percentil( + node, [ i, day ](auto&& model) -> auto& { + return model.parameters.template get>()[{i, day}]; + }); + param_percentil( + node, [ i, day ](auto&& model) -> auto& { + return model.parameters.template get>()[{i, day}]; + }); + param_percentil( + node, [ i, day ](auto&& model) -> auto& { + return model.parameters.template get>()[{i, day}]; + }); } //virus variants - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); } // group independent params - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); for (size_t run = 0; run < num_runs; run++) { diff --git a/cpp/models/ode_secirts/model.h b/cpp/models/ode_secirts/model.h index 721293fe44..4ade7559b5 100644 --- a/cpp/models/ode_secirts/model.h +++ b/cpp/models/ode_secirts/model.h @@ -764,14 +764,11 @@ class Simulation : public BaseT auto base_infectiousness = this->get_model().parameters.template get>(); FP delay_npi_implementation; - auto t = BaseT::get_result().get_last_time(); - const auto dt = dyn_npis.get_interval().get(); + FP t = BaseT::get_result().get_last_time(); while (t < tmax) { - auto dt_eff = min({dt, tmax - t, m_t_last_npi_check + dt - t}); - if (dt_eff >= 1.0) { - dt_eff = 1.0; - } + auto dt_eff = min(dt, tmax - t); + dt_eff = min(dt_eff, 1.0); BaseT::advance(t + dt_eff); if (t + 0.5 + dt_eff - floor(t + 0.5) >= 1) { @@ -789,32 +786,26 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (floating_point_greater_equal(t, m_t_last_npi_check + dt)) { - if (t < t_end_dyn_npis) { - auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * - dyn_npis.get_base_value(); - auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); - if (exceeded_threshold != dyn_npis.get_thresholds().end() && - (exceeded_threshold->first > m_dynamic_npi.first || - t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired - - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); - this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; - this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); - } + if (t < t_end_dyn_npis) { + auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * + dyn_npis.get_base_value(); + auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); + if (exceeded_threshold != dyn_npis.get_thresholds().end() && + (exceeded_threshold->first > m_dynamic_npi.first || + t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired + + auto t_start = SimulationTime(t + delay_npi_implementation); + auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); + this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; + this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); } - m_t_last_npi_check = t; } } - else { - m_t_last_npi_check = t; - } } // reset TransmissionProbabilityOnContact. This is important for the graph simulation where the advance // function is called multiple times for the same model. diff --git a/cpp/models/ode_secirts/parameters.h b/cpp/models/ode_secirts/parameters.h index 1ef4ea5dbf..2931a3386b 100644 --- a/cpp/models/ode_secirts/parameters.h +++ b/cpp/models/ode_secirts/parameters.h @@ -727,22 +727,6 @@ struct InfectiousnessNewVariant { } }; -/** - * @brief The delay with which DynamicNPIs are implemented and enforced after exceedance of threshold. - */ -template -struct DynamicNPIsImplementationDelay { - using Type = UncertainValue; - static Type get_default(AgeGroup /*size*/) - { - return Type(0.0); - } - static std::string name() - { - return "DynamicNPIsImplementationDelay"; - } -}; - template using ParametersBase = ParameterSet< StartDay, Seasonality, ICUCapacity, TestAndTraceCapacity, TestAndTraceCapacityMaxRiskNoSymptoms, @@ -757,8 +741,7 @@ using ParametersBase = ParameterSet< DailyBoosterVaccinations, ReducExposedPartialImmunity, ReducExposedImprovedImmunity, ReducInfectedSymptomsPartialImmunity, ReducInfectedSymptomsImprovedImmunity, ReducInfectedSevereCriticalDeadPartialImmunity, ReducInfectedSevereCriticalDeadImprovedImmunity, - ReducTimeInfectedMild, InfectiousnessNewVariant, DynamicNPIsImplementationDelay, - StartDayNewVariant>; + ReducTimeInfectedMild, InfectiousnessNewVariant, StartDayNewVariant>; /** * @brief Parameters of the age-resolved SECIRS-type model with high temporary immunity upon immunization and waning immunity over @@ -880,13 +863,6 @@ class Parameters : public ParametersBase corrected = true; } - if (this->template get>() < 0.0) { - log_warning("Constraint check: Parameter DynamicNPIsImplementationDelay changed from {} to {}", - this->template get>(), 0); - this->template set>(0); - corrected = true; - } - const FP tol_times = 1e-1; // accepted tolerance for compartment stays for (auto i = AgeGroup(0); i < AgeGroup(m_num_groups); ++i) { @@ -1142,11 +1118,6 @@ class Parameters : public ParametersBase return true; } - if (this->template get>() < 0.0) { - log_error("Constraint check: Parameter DynamicNPIsImplementationDelay smaller {}", 0); - return true; - } - for (auto i = AgeGroup(0); i < AgeGroup(m_num_groups); ++i) { if (this->template get>()[i] < tol_times) { diff --git a/cpp/models/ode_secirvvs/model.h b/cpp/models/ode_secirvvs/model.h index 956fdd7d79..6323a7cf13 100644 --- a/cpp/models/ode_secirvvs/model.h +++ b/cpp/models/ode_secirvvs/model.h @@ -686,16 +686,11 @@ class Simulation : public BaseT auto base_infectiousness = this->get_model().parameters.template get>(); FP delay_npi_implementation; - FP t = BaseT::get_result().get_last_time(); - const FP dt = dyn_npis.get_thresholds().size() > 0 ? dyn_npis.get_interval().get() : tmax; + FP t = BaseT::get_result().get_last_time(); while (t < tmax) { FP dt_eff = min(dt, tmax - t); - dt_eff = min(dt_eff, m_t_last_npi_check + dt - t); - - if (dt_eff >= 1.0) { - dt_eff = 1.0; - } + dt_eff = min(dt_eff, 1.0); if (t == 0) { //this->apply_vaccination(t); // done in init now? @@ -717,32 +712,26 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (floating_point_greater_equal(t, m_t_last_npi_check + dt)) { - if (t < t_end_dyn_npis) { - auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * - dyn_npis.get_base_value(); - auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); - if (exceeded_threshold != dyn_npis.get_thresholds().end() && - (exceeded_threshold->first > m_dynamic_npi.first || - t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired - - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); - this->get_model().parameters.get_start_commuter_detection() = t_start.get(); - this->get_model().parameters.get_end_commuter_detection() = t_end.get(); - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); - } + if (t < t_end_dyn_npis) { + auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * + dyn_npis.get_base_value(); + auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); + if (exceeded_threshold != dyn_npis.get_thresholds().end() && + (exceeded_threshold->first > m_dynamic_npi.first || + t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired + + auto t_start = SimulationTime(t + delay_npi_implementation); + auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); + this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; + this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); } - m_t_last_npi_check = t; } } - else { - m_t_last_npi_check = t; - } } // reset TransmissionProbabilityOnContact. This is important for the graph simulation where the advance // function is called multiple times for the same model. diff --git a/cpp/models/ode_secirvvs/parameters.h b/cpp/models/ode_secirvvs/parameters.h index a397be975a..aa2a7dabe7 100644 --- a/cpp/models/ode_secirvvs/parameters.h +++ b/cpp/models/ode_secirvvs/parameters.h @@ -185,22 +185,6 @@ struct DynamicNPIsInfectedSymptoms { } }; -/** - * @brief The delay with which DynamicNPIs are implemented and enforced after exceedance of threshold. - */ -template -struct DynamicNPIsImplementationDelay { - using Type = UncertainValue; - static Type get_default(AgeGroup /*size*/) - { - return Type(0.0); - } - static std::string name() - { - return "DynamicNPIsImplementationDelay"; - } -}; - /** * @brief the (mean) latent time in day unit */ @@ -625,14 +609,13 @@ struct InfectiousnessNewVariant { template using ParametersBase = ParameterSet< StartDay, Seasonality, ICUCapacity, TestAndTraceCapacity, TestAndTraceCapacityMaxRiskNoSymptoms, - TestAndTraceCapacityMaxRiskSymptoms, ContactPatterns, DynamicNPIsImplementationDelay, - DynamicNPIsInfectedSymptoms, TimeExposed, TimeInfectedNoSymptoms, TimeInfectedSymptoms, - TimeInfectedSevere, TimeInfectedCritical, TransmissionProbabilityOnContact, - RelativeTransmissionNoSymptoms, RecoveredPerInfectedNoSymptoms, RiskOfInfectionFromSymptomatic, - MaxRiskOfInfectionFromSymptomatic, SeverePerInfectedSymptoms, CriticalPerSevere, DeathsPerCritical, - VaccinationGap, DaysUntilEffectivePartialImmunity, DaysUntilEffectiveImprovedImmunity, - DailyFullVaccinations, DailyPartialVaccinations, ReducExposedPartialImmunity, - ReducExposedImprovedImmunity, ReducInfectedSymptomsPartialImmunity, + TestAndTraceCapacityMaxRiskSymptoms, ContactPatterns, DynamicNPIsInfectedSymptoms, TimeExposed, + TimeInfectedNoSymptoms, TimeInfectedSymptoms, TimeInfectedSevere, TimeInfectedCritical, + TransmissionProbabilityOnContact, RelativeTransmissionNoSymptoms, RecoveredPerInfectedNoSymptoms, + RiskOfInfectionFromSymptomatic, MaxRiskOfInfectionFromSymptomatic, SeverePerInfectedSymptoms, + CriticalPerSevere, DeathsPerCritical, VaccinationGap, DaysUntilEffectivePartialImmunity, + DaysUntilEffectiveImprovedImmunity, DailyFullVaccinations, DailyPartialVaccinations, + ReducExposedPartialImmunity, ReducExposedImprovedImmunity, ReducInfectedSymptomsPartialImmunity, ReducInfectedSymptomsImprovedImmunity, ReducInfectedSevereCriticalDeadPartialImmunity, ReducInfectedSevereCriticalDeadImprovedImmunity, ReducTimeInfectedMild, InfectiousnessNewVariant, StartDayNewVariant>; @@ -756,13 +739,6 @@ class Parameters : public ParametersBase corrected = true; } - if (this->template get>() < 0.0) { - log_warning("Constraint check: Parameter DynamicNPIsImplementationDelay changed from {} to {}", - this->template get>(), 0); - this->template set>(0); - corrected = true; - } - const FP tol_times = 1e-1; // accepted tolerance for compartment stays for (auto i = AgeGroup(0); i < AgeGroup(m_num_groups); ++i) { @@ -979,11 +955,6 @@ class Parameters : public ParametersBase return true; } - if (this->template get>() < 0.0) { - log_error("Constraint check: Parameter DynamicNPIsImplementationDelay smaller {}", 0); - return true; - } - for (auto i = AgeGroup(0); i < AgeGroup(m_num_groups); ++i) { if (this->template get>()[i] < tol_times) { diff --git a/cpp/tests/test_odesecir.cpp b/cpp/tests/test_odesecir.cpp index f837aee4b5..5f4b25c09a 100755 --- a/cpp/tests/test_odesecir.cpp +++ b/cpp/tests/test_odesecir.cpp @@ -1222,12 +1222,6 @@ TEST(TestOdeSecir, apply_constraints_parameters) model.parameters.set>(1.1); EXPECT_EQ(model.parameters.apply_constraints(), 1); EXPECT_EQ(model.parameters.get>()[indx_agegroup], 0); - - model.parameters.set>(-4); - EXPECT_EQ(model.parameters.apply_constraints(), 1); - EXPECT_EQ(model.parameters.get>(), 0); - - EXPECT_EQ(model.parameters.apply_constraints(), 0); } #if defined(MEMILIO_HAS_JSONCPP) diff --git a/cpp/tests/test_odesecirvvs.cpp b/cpp/tests/test_odesecirvvs.cpp index a264174f8d..bc7e288068 100755 --- a/cpp/tests/test_odesecirvvs.cpp +++ b/cpp/tests/test_odesecirvvs.cpp @@ -292,10 +292,10 @@ void set_contact_parameters(mio::osecirvvs::Model::ParameterSet& paramet npis.set_threshold(10.0, {mio::DampingSampling(npi_value, mio::DampingLevel(0), mio::DampingType(0), mio::SimulationTime(0), {0}, npi_groups)}); npis.set_base_value(100'000); - npis.set_interval(mio::SimulationTime(3.0)); + npis.set_implementation_delay(mio::SimulationTime(3.0)); npis.set_duration(mio::SimulationTime(14.0)); - parameters.get_end_dynamic_npis() = 10.0; //required for dynamic NPIs to have effect in this model - parameters.template get>() = 7; + npis.set_directive_end(10.0); // required for dynamic NPIs to have effect in this model + npis.set_implementation_delay(7.0); } void set_covid_parameters(mio::osecirvvs::Model::ParameterSet& params, bool set_invalid_initial_value) @@ -1472,7 +1472,6 @@ TEST(TestOdeSECIRVVS, check_constraints_parameters) EXPECT_EQ(model.parameters.check_constraints(), 0); model.parameters.set>(1); - model.parameters.set>(-4); ASSERT_EQ(model.parameters.check_constraints(), 1); mio::set_log_level(mio::LogLevel::warn); @@ -1604,10 +1603,6 @@ TEST(TestOdeSECIRVVS, apply_constraints_parameters) EXPECT_EQ(model.parameters.apply_constraints(), 1); EXPECT_EQ(model.parameters.get>()[indx_agegroup], 1); - model.parameters.set>(-4); - EXPECT_EQ(model.parameters.apply_constraints(), 1); - EXPECT_EQ(model.parameters.get>(), 0); - EXPECT_EQ(model.parameters.apply_constraints(), 0); mio::set_log_level(mio::LogLevel::warn); } diff --git a/docs/source/cpp/models/osecir.rst b/docs/source/cpp/models/osecir.rst index 58541e2f6f..0d71b29f38 100644 --- a/docs/source/cpp/models/osecir.rst +++ b/docs/source/cpp/models/osecir.rst @@ -258,16 +258,17 @@ A complex lockdown scenario with multiple interventions starting on a specific d A more advanced structure to automatically activate interventions based on threshold criteria is given by **DynamicNPIs**. Dynamic NPIs can be configured to trigger when the number of symptomatic infected individuals exceeds a certain relative threshold in the population. In contrast to static NPIs which are active as long as no other NPI gets implemented, dynamic NPIs are checked at regular intervals and get -activated for a defined duration when the threshold is exceeded. As above, different dampings `contact_dampings` can be assigned to different contact locations -and are then triggered all at once the threshold is exceeded. -The following example shows how to set up dynamic NPIs based on the number of 200 symptomatic infected individuals per 100,000 population. -It will be active for at least 14 days and checked every 3 days. If the last check after day 14 is negative, the NPI will be deactivated. +activated for a defined duration when the threshold is exceeded. For most realistic studies, an additional delay parameter for automated implementation +can be set. This parameter imitates a delayed reaction to exceedance of the considered threshold. +As above, different dampings `contact_dampings` can be assigned to different contact locations and are then triggered all at once the threshold +is exceeded. The following example shows how to set up dynamic NPIs based on the number of 200 symptomatic infected individuals per 100,000 population. +It will be active for at least 14 days. If the last check after day 14 is negative, the NPI will be deactivated. .. code-block:: cpp // Configure dynamic NPIs with thresholds auto& dynamic_npis = params.get>(); - dynamic_npis.set_interval(mio::SimulationTime(3.0)); // Check every 3 days + dynamic_npis.set_implementation_delay(mio::SimulationTime(0.0)); // Simulate no implementation delay dynamic_npis.set_duration(mio::SimulationTime(14.0)); // Apply for 14 days dynamic_npis.set_base_value(100'000); // Per 100,000 population dynamic_npis.set_threshold(200.0, contact_dampings); // Trigger at 200 cases per 100,000 diff --git a/docs/source/cpp/models/osecirts.rst b/docs/source/cpp/models/osecirts.rst index 36dec27a15..d00b3c9409 100644 --- a/docs/source/cpp/models/osecirts.rst +++ b/docs/source/cpp/models/osecirts.rst @@ -312,7 +312,7 @@ The model also supports dynamic NPIs based on epidemic thresholds: // Set threshold-based triggers for NPIs auto& dynamic_npis = model.parameters.get>(); - dynamic_npis.set_interval(mio::SimulationTime(3.0)); // Check every 3 days + dynamic_npis.set_implementation_delay(mio::SimulationTime(0.0)); // Simulate no implementation delay dynamic_npis.set_duration(mio::SimulationTime(14.0)); // Apply for 14 days dynamic_npis.set_base_value(100'000); // Per 100,000 population dynamic_npis.set_threshold(200.0, dampings); // Trigger at 200 cases per 100,000 diff --git a/docs/source/cpp/models/osecirvvs.rst b/docs/source/cpp/models/osecirvvs.rst index edafde4adc..f2b70693ee 100644 --- a/docs/source/cpp/models/osecirvvs.rst +++ b/docs/source/cpp/models/osecirvvs.rst @@ -158,9 +158,6 @@ The model includes all parameters from the basic ODE-SECIR model plus additional * - :math:`TTC_{maxSym}` - ``TestAndTraceCapacityMaxRiskSymptoms`` - Multiplier for test and trace capacity for symptomatic cases. - * - :math:`T_{dyndelay}` - - ``DynamicNPIsImplementationDelay`` - - Delay in days for implementing dynamic NPIs after threshold exceedance. * - :math:`\lambda_{N,i}` - ``ext_inf_force_dummy`` - Force of infection for susceptibles with naive immunity. @@ -328,7 +325,7 @@ The model also supports dynamic NPIs based on epidemic thresholds: // Configure dynamic NPIs auto& dynamic_npis = params.get>(); - dynamic_npis.set_interval(mio::SimulationTime(3.0)); // Check NPI every 3 days + dynamic_npis.set_implementation_delay(mio::SimulationTime(0.0)); // Simulate no implementation delay dynamic_npis.set_duration(mio::SimulationTime(14.0)); // Apply NPI for 14 days dynamic_npis.set_base_value(100'000); // Base value to trigger NPI is population of 100,000 dynamic_npis.set_threshold(200.0, dampings); // Trigger at 200 cases per 100,000 diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h b/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h index 9970dd3b1b..107bd94778 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h +++ b/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h @@ -39,12 +39,12 @@ void bind_dynamicNPI_members(pybind11::module_& m, std::string const& name) bind_class(m, name.c_str()) .def(pybind11::init<>()) .def_property( - "interval", + "implementation_delay", [](mio::DynamicNPIs& self) { - return static_cast(self.get_interval()); + return static_cast(self.get_implementation_delay()); }, [](mio::DynamicNPIs& self, double v) { - self.set_interval(mio::SimulationTime(v)); + self.set_implementation_delay(mio::SimulationTime(v)); }) .def_property( "duration", From b9f719284155c4a2f7baecd3e47c6f947df425be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Joachim=20K=C3=BChn?= Date: Sun, 29 Mar 2026 19:47:01 +0200 Subject: [PATCH 02/19] [ci skip] Continuation, examples, python + minimal tests --- cpp/memilio/epidemiology/dynamic_npis.h | 4 +-- .../metapopulation_mobility_instant.h | 12 ++------ cpp/models/ode_secir/parameters.h | 13 --------- cpp/models/ode_secirts/parameters.h | 13 --------- cpp/models/ode_secirvvs/parameters.h | 13 --------- cpp/tests/test_dynamic_npis.cpp | 28 +++++++++++++++---- cpp/tests/test_odesecirts.cpp | 3 +- .../2020_npis_sarscov2_wildtype_germany.py | 2 +- ...2021_vaccination_sarscov2_delta_germany.py | 7 ++--- .../bindings/epidemiology/dynamic_npis.h | 16 +++++++++++ .../simulation/bindings/models/osecirvvs.cpp | 14 ++-------- 11 files changed, 51 insertions(+), 74 deletions(-) diff --git a/cpp/memilio/epidemiology/dynamic_npis.h b/cpp/memilio/epidemiology/dynamic_npis.h index d118b2e083..43be0ca4d9 100644 --- a/cpp/memilio/epidemiology/dynamic_npis.h +++ b/cpp/memilio/epidemiology/dynamic_npis.h @@ -244,8 +244,8 @@ class DynamicNPIs std::vector>>> m_thresholds; SimulationTime m_duration{14.0}; SimulationTime m_delay{0.0}; - SimulationTime m_directive_begin{-(FP)std::numeric_limits::max}; - SimulationTime m_directive_end{(FP)std::numeric_limits::max}; + SimulationTime m_directive_begin{-std::numeric_limits::max}; + SimulationTime m_directive_end{std::numeric_limits::max}; FP m_base{1.0}; }; diff --git a/cpp/memilio/mobility/metapopulation_mobility_instant.h b/cpp/memilio/mobility/metapopulation_mobility_instant.h index 8e44714e78..3180a1d4e0 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_instant.h +++ b/cpp/memilio/mobility/metapopulation_mobility_instant.h @@ -43,8 +43,7 @@ class SimulationNode using Simulation = Sim; template - requires std::is_constructible_v - SimulationNode(Args&&... args) + requires std::is_constructible_v SimulationNode(Args&&... args) : m_simulation(std::forward(args)...) , m_last_state(m_simulation.get_result().get_last_value()) , m_t0(m_simulation.get_result().get_last_time()) @@ -547,14 +546,8 @@ template void mio::MobilityEdge::apply_mobility(FP t, FP dt, SimulationNode& node_from, SimulationNode& node_to) { - //check dynamic npis - if (m_t_last_dynamic_npi_check == -std::numeric_limits::infinity()) { - m_t_last_dynamic_npi_check = node_from.get_t0(); - } - auto& dyn_npis = m_parameters.get_dynamic_npis_infected(); - if (dyn_npis.get_thresholds().size() > 0 && - floating_point_greater_equal(t, m_t_last_dynamic_npi_check + dyn_npis.get_interval().get())) { + if (dyn_npis.get_thresholds().size() > 0) { auto inf_rel = get_infections_relative(node_from, t, node_from.get_last_state()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); @@ -569,7 +562,6 @@ void mio::MobilityEdge::apply_mobility(FP t, FP dt, SimulationNode& m_parameters.get_coefficients().get_shape(), g); }); } - m_t_last_dynamic_npi_check = t; } //returns diff --git a/cpp/models/ode_secir/parameters.h b/cpp/models/ode_secir/parameters.h index f2292a7eed..f8ad58924d 100644 --- a/cpp/models/ode_secir/parameters.h +++ b/cpp/models/ode_secir/parameters.h @@ -429,18 +429,6 @@ class Parameters : public ParametersBase return m_end_commuter_detection; } - /** - * Time in simulation after which no dynamic NPIs are applied. - */ - FP& get_end_dynamic_npis() - { - return m_end_dynamic_npis; - } - FP get_end_dynamic_npis() const - { - return m_end_dynamic_npis; - } - /** * @brief Checks whether all Parameters satisfy their corresponding constraints and applies them, if they do not. * Time spans cannot be negative and probabilities can only take values between [0,1]. @@ -729,7 +717,6 @@ class Parameters : public ParametersBase FP m_commuter_nondetection = 0.0; FP m_start_commuter_detection = 0.0; FP m_end_commuter_detection = 0.0; - FP m_end_dynamic_npis = std::numeric_limits::max(); }; /** diff --git a/cpp/models/ode_secirts/parameters.h b/cpp/models/ode_secirts/parameters.h index 2931a3386b..efd371d461 100644 --- a/cpp/models/ode_secirts/parameters.h +++ b/cpp/models/ode_secirts/parameters.h @@ -800,18 +800,6 @@ class Parameters : public ParametersBase return m_end_commuter_detection; } - /** - * Time in simulation after which no dynamic NPIs are applied. - */ - FP& get_end_dynamic_npis() - { - return m_end_dynamic_npis; - } - FP get_end_dynamic_npis() const - { - return m_end_dynamic_npis; - } - /** * @brief Checks whether all Parameters satisfy their corresponding constraints and applies them, if they do not. * Time spans cannot be negative and probabilities can only take values between [0,1]. @@ -1321,7 +1309,6 @@ class Parameters : public ParametersBase FP m_commuter_nondetection = 0.0; FP m_start_commuter_detection = 0.0; FP m_end_commuter_detection = 0.0; - FP m_end_dynamic_npis = std::numeric_limits::max(); }; } // namespace osecirts diff --git a/cpp/models/ode_secirvvs/parameters.h b/cpp/models/ode_secirvvs/parameters.h index aa2a7dabe7..0924719ff4 100644 --- a/cpp/models/ode_secirvvs/parameters.h +++ b/cpp/models/ode_secirvvs/parameters.h @@ -676,18 +676,6 @@ class Parameters : public ParametersBase return m_end_commuter_detection; } - /** - * Time in simulation after which no dynamic NPIs are applied. - */ - FP& get_end_dynamic_npis() - { - return m_end_dynamic_npis; - } - FP get_end_dynamic_npis() const - { - return m_end_dynamic_npis; - } - /** * @brief Checks whether all Parameters satisfy their corresponding constraints and applies them, if they do not. * Time spans cannot be negative and probabilities can only take values between [0,1]. @@ -1125,7 +1113,6 @@ class Parameters : public ParametersBase FP m_commuter_nondetection = 0.0; FP m_start_commuter_detection = 0.0; FP m_end_commuter_detection = 0.0; - FP m_end_dynamic_npis = std::numeric_limits::max(); }; } // namespace osecirvvs diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index b545878abc..c8ec99734d 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -63,6 +63,25 @@ TEST(DynamicNPIs, get_threshold) EXPECT_EQ(npis.get_max_exceeded_threshold(0.5), npis.get_thresholds().end()); } +TEST(DynamicNPIs, get_and_set_delay) +{ + mio::DynamicNPIs npis; + EXPECT_EQ(npis.get_implementation_delay(), 0.0); + npis.set_implementation_delay(7.0); + EXPECT_EQ(npis.get_implementation_delay(), 7.0); +} + +TEST(DynamicNPIs, get_and_set_directive_begin_end) +{ + mio::DynamicNPIs npis; + EXPECT_LE(npis.get_directive_begin(), -1e4); + EXPECT_GE(npis.get_directive_end(), 1e4); + npis.set_directive_begin(7.0); + EXPECT_EQ(npis.get_directive_begin(), 7.0); + npis.set_directive_end(14.0); + EXPECT_EQ(npis.get_directive_end(), 14.0); +} + TEST(DynamicNPIs, get_damping_indices) { using Damping = mio::Damping>; @@ -315,7 +334,6 @@ TEST(DynamicNPIs, mobility) Eigen::VectorXd::Ones(2)}}); npis.set_duration(mio::SimulationTime(5.0)); npis.set_base_value(100'000); - npis.set_interval(mio::SimulationTime(3.0)); mio::MobilityCoefficientGroup coeffs(1, 2); mio::MobilityParameters parameters(coeffs); @@ -323,11 +341,11 @@ TEST(DynamicNPIs, mobility) mio::MobilityEdge edge(parameters); - EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), 0); //initial + EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), 0); // initial edge.apply_mobility(0.5, 0.5, node_from, node_to); - EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), 0); //not check at the beginning + EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), 0); // no check at the beginning EXPECT_CALL(node_from.get_simulation(), advance).Times(1).WillOnce([&](auto t) { node_from.get_simulation().result.add_time_point(t, last_state_safe); @@ -336,7 +354,7 @@ TEST(DynamicNPIs, mobility) node_to.advance(3.0, 2.5); edge.apply_mobility(3.0, 2.5, node_from, node_to); - EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), 0); //threshold not exceeded + EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), 0); // threshold not exceeded EXPECT_CALL(node_from.get_simulation(), advance).Times(1).WillOnce([&](auto t) { node_from.get_simulation().result.add_time_point(t, last_state_crit); @@ -346,7 +364,7 @@ TEST(DynamicNPIs, mobility) edge.apply_mobility(4.5, 1.5, node_from, node_to); EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), - 0); //threshold exceeded, but only check every 3 days + 0); // threshold exceeded, but only check every 3 days EXPECT_CALL(node_from.get_simulation(), advance).Times(1).WillOnce([&](auto t) { node_from.get_simulation().result.add_time_point(t, last_state_crit); diff --git a/cpp/tests/test_odesecirts.cpp b/cpp/tests/test_odesecirts.cpp index ced73a9751..33b3bd505c 100755 --- a/cpp/tests/test_odesecirts.cpp +++ b/cpp/tests/test_odesecirts.cpp @@ -464,9 +464,8 @@ void set_contact_parameters(mio::osecirts::Model::ParameterSet& paramete npis.set_threshold(10.0, {mio::DampingSampling(npi_value, mio::DampingLevel(0), mio::DampingType(0), mio::SimulationTime(0), {0}, npi_groups)}); npis.set_base_value(100'000); - npis.set_interval(mio::SimulationTime(3.0)); npis.set_duration(mio::SimulationTime(14.0)); - parameters.get_end_dynamic_npis() = 10.0; //required for dynamic NPIs to have effect in this model + npis.set_directive_end(10.0); //required for dynamic NPIs to have effect in this model } void set_covid_parameters(mio::osecirts::Model::ParameterSet& params, bool set_invalid_initial_value) diff --git a/pycode/examples/simulation/2020_npis_sarscov2_wildtype_germany.py b/pycode/examples/simulation/2020_npis_sarscov2_wildtype_germany.py index efb7d6963f..e0cc43c9d8 100644 --- a/pycode/examples/simulation/2020_npis_sarscov2_wildtype_germany.py +++ b/pycode/examples/simulation/2020_npis_sarscov2_wildtype_germany.py @@ -502,7 +502,7 @@ def senior_awareness(t, min, max): local_npis.append(physical_distancing_work_other(0, 0.6, 0.8)) local_npis.append(senior_awareness(0, 0.0, 0.0)) - dynamic_npis.interval = 3.0 + dynamic_npis.implementation_delay = 0.0 dynamic_npis.duration = 14.0 dynamic_npis.base_value = 100000 dynamic_npis.set_threshold(200.0, local_npis) diff --git a/pycode/examples/simulation/2021_vaccination_sarscov2_delta_germany.py b/pycode/examples/simulation/2021_vaccination_sarscov2_delta_germany.py index 533d2676bd..a055e9ecff 100644 --- a/pycode/examples/simulation/2021_vaccination_sarscov2_delta_germany.py +++ b/pycode/examples/simulation/2021_vaccination_sarscov2_delta_germany.py @@ -506,10 +506,8 @@ def senior_awareness(t, min, max): masks_high = 0.0 masks_narrow = 0.0 - start_open = mio.Date( - 2021, month_open, 1) + start_open = mio.Date(2021, month_open, 1) start_summer = start_open - self.start_date - params.end_dynamic_npis = start_summer if start_open < end_date: dampings.append(contacts_at_home(start_summer, 0.0, 0.0)) @@ -586,7 +584,8 @@ def senior_awareness(t, min, max): physical_distancing_other(0, 0.2 + narrow, 0.4 - narrow)) dynamic_npi_dampings2.append(senior_awareness(0, 0.0, 0.0)) - dynamic_npis.interval = 1.0 + dynamic_npis.implementation_delay = 7.0 + dynamic_npis.directive_end = start_summer dynamic_npis.duration = 14.0 dynamic_npis.base_value = 100000 dynamic_npis.set_threshold(35.0, dynamic_npi_dampings) diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h b/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h index 107bd94778..caae6d6110 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h +++ b/pycode/memilio-simulation/memilio/simulation/bindings/epidemiology/dynamic_npis.h @@ -46,6 +46,22 @@ void bind_dynamicNPI_members(pybind11::module_& m, std::string const& name) [](mio::DynamicNPIs& self, double v) { self.set_implementation_delay(mio::SimulationTime(v)); }) + .def_property( + "directive_begin", + [](mio::DynamicNPIs& self) { + return static_cast(self.get_directive_begin()); + }, + [](mio::DynamicNPIs& self, double v) { + self.set_directive_begin(mio::SimulationTime(v)); + }) + .def_property( + "directive_end", + [](mio::DynamicNPIs& self) { + return static_cast(self.get_directive_end()); + }, + [](mio::DynamicNPIs& self, double v) { + self.set_directive_end(mio::SimulationTime(v)); + }) .def_property( "duration", [](mio::DynamicNPIs& self) { diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp index 288c95fd95..a07991f1ba 100755 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/osecirvvs.cpp @@ -145,14 +145,6 @@ PYBIND11_MODULE(_simulation_osecirvvs, m) [](mio::osecirvvs::Parameters& self, double v) { self.get_end_commuter_detection() = v; }) - .def_property( - "end_dynamic_npis", - [](const mio::osecirvvs::Parameters& self) { - return self.get_end_dynamic_npis(); - }, - [](mio::osecirvvs::Parameters& self, double v) { - self.get_end_dynamic_npis() = v; - }) .def("check_constraints", &mio::osecirvvs::Parameters::check_constraints) .def("apply_constraints", &mio::osecirvvs::Parameters::apply_constraints); @@ -239,9 +231,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); From da73e77fa45d7c27c0287a04d1f98fe44cd75696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Joachim=20K=C3=BChn?= Date: Sun, 29 Mar 2026 20:18:42 +0200 Subject: [PATCH 03/19] replace directive_end and delay parameter from model through NPI --- cpp/benchmarks/flow_simulation_ode_secirvvs.h | 9 ++++----- cpp/models/ode_secir/model.h | 6 ++---- cpp/models/ode_secir/parameter_space.h | 2 -- cpp/models/ode_secirts/model.h | 6 ++---- cpp/models/ode_secirvvs/model.h | 6 ++---- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/cpp/benchmarks/flow_simulation_ode_secirvvs.h b/cpp/benchmarks/flow_simulation_ode_secirvvs.h index a6bcc3e75f..11b0b5f0df 100644 --- a/cpp/benchmarks/flow_simulation_ode_secirvvs.h +++ b/cpp/benchmarks/flow_simulation_ode_secirvvs.h @@ -539,7 +539,6 @@ class Simulation : public Base */ Eigen::Ref> advance(ScalarType tmax) { - auto& t_end_dyn_npis = this->get_model().parameters.get_end_dynamic_npis(); auto& dyn_npis = this->get_model().parameters.template get>(); auto& contact_patterns = this->get_model().parameters.template get>(); @@ -566,21 +565,21 @@ class Simulation : public Base } if (t > 0) { - delay_npi_implementation = 7; + delay_npi_implementation = dyn_npis.get_implementation_delay(); } - else { + else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. delay_npi_implementation = 0; } t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t < t_end_dyn_npis) { + if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); if (exceeded_threshold != dyn_npis.get_thresholds().end() && (exceeded_threshold->first > m_dynamic_npi.first || - t > ScalarType(m_dynamic_npi.second))) { //old npi was weaker or is expired + t > ScalarType(m_dynamic_npi.second))) { // old npi was weaker or is expired auto t_start = SimulationTime(t + delay_npi_implementation); auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index 36f73adcfb..fbba15c7be 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -288,7 +288,6 @@ class Simulation : public BaseT Eigen::Ref> advance(FP tmax) { using std::min; - auto& t_end_dyn_npis = this->get_model().parameters.get_end_dynamic_npis(); auto& dyn_npis = this->get_model().parameters.template get>(); auto& contact_patterns = this->get_model().parameters.template get>(); @@ -299,8 +298,7 @@ class Simulation : public BaseT BaseT::advance(t + dt_eff); if (t > 0) { - delay_npi_implementation = - this->get_model().parameters.template get>(); + delay_npi_implementation = dyn_npis.get_implementation_delay(); } else { // DynamicNPIs for t=0 are 'misused' to be 'from-start NPIs'. I.e., do not enforce delay. delay_npi_implementation = 0; @@ -308,7 +306,7 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t < t_end_dyn_npis) { + if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); diff --git a/cpp/models/ode_secir/parameter_space.h b/cpp/models/ode_secir/parameter_space.h index 52714c4435..bbaf8ba657 100644 --- a/cpp/models/ode_secir/parameter_space.h +++ b/cpp/models/ode_secir/parameter_space.h @@ -215,8 +215,6 @@ Graph, MobilityParameters> draw_sample(Graph, MobilityPa shared_contacts.draw_sample_dampings(); auto& shared_dynamic_npis = shared_params_model.parameters.template get>(); shared_dynamic_npis.draw_sample(); - auto& shared_dynamic_npis_delay = shared_params_model.parameters.template get>(); - shared_dynamic_npis_delay.draw_sample(); for (auto& params_node : graph.nodes()) { auto& node_model = params_node.property; diff --git a/cpp/models/ode_secirts/model.h b/cpp/models/ode_secirts/model.h index 4ade7559b5..a7a98952e1 100644 --- a/cpp/models/ode_secirts/model.h +++ b/cpp/models/ode_secirts/model.h @@ -754,7 +754,6 @@ class Simulation : public BaseT using std::floor; using std::min; - auto& t_end_dyn_npis = this->get_model().parameters.get_end_dynamic_npis(); auto& dyn_npis = this->get_model().parameters.template get>(); auto& contact_patterns = this->get_model().parameters.template get>(); // const size_t num_groups = (size_t)this->get_model().parameters.get_num_groups(); @@ -776,8 +775,7 @@ class Simulation : public BaseT } if (t > 0) { - delay_npi_implementation = - this->get_model().parameters.template get>(); + delay_npi_implementation = dyn_npis.get_implementation_delay(); } else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. @@ -786,7 +784,7 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t < t_end_dyn_npis) { + if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); diff --git a/cpp/models/ode_secirvvs/model.h b/cpp/models/ode_secirvvs/model.h index 6323a7cf13..f05b6b8490 100644 --- a/cpp/models/ode_secirvvs/model.h +++ b/cpp/models/ode_secirvvs/model.h @@ -676,7 +676,6 @@ class Simulation : public BaseT using std::floor; using std::min; - auto& t_end_dyn_npis = this->get_model().parameters.get_end_dynamic_npis(); auto& dyn_npis = this->get_model().parameters.template get>(); auto& contact_patterns = this->get_model().parameters.template get>(); // const size_t num_groups = (size_t)this->get_model().parameters.get_num_groups(); @@ -703,8 +702,7 @@ class Simulation : public BaseT } if (t > 0) { - delay_npi_implementation = - this->get_model().parameters.template get>(); + delay_npi_implementation = dyn_npis.get_implementation_delay(); } else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. delay_npi_implementation = 0; @@ -712,7 +710,7 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t < t_end_dyn_npis) { + if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); From 4d6970ba1e36d52958e6311b0e630b91567e26dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Joachim=20K=C3=BChn?= Date: Mon, 30 Mar 2026 21:27:37 +0200 Subject: [PATCH 04/19] resolve compilation errors --- cpp/benchmarks/flow_simulation_ode_secirvvs.h | 4 +- cpp/memilio/epidemiology/dynamic_npis.h | 4 +- .../metapopulation_mobility_instant.h | 2 +- cpp/models/ode_secir/model.h | 8 +- cpp/models/ode_secirts/model.h | 8 +- cpp/models/ode_secirvvs/analyze_result.h | 205 ++++++++++-------- cpp/models/ode_secirvvs/model.h | 8 +- cpp/models/ode_secirvvs/parameter_space.h | 2 - cpp/tests/test_dynamic_npis.cpp | 45 ++-- cpp/tests/test_odesecir.cpp | 7 - cpp/tests/test_odesecirts.cpp | 2 +- cpp/tests/test_odesecirvvs.cpp | 4 +- cpp/tests/test_uncertain_ad_compatibility.cpp | 7 +- 13 files changed, 158 insertions(+), 148 deletions(-) diff --git a/cpp/benchmarks/flow_simulation_ode_secirvvs.h b/cpp/benchmarks/flow_simulation_ode_secirvvs.h index 11b0b5f0df..85e0a3198f 100644 --- a/cpp/benchmarks/flow_simulation_ode_secirvvs.h +++ b/cpp/benchmarks/flow_simulation_ode_secirvvs.h @@ -565,7 +565,7 @@ class Simulation : public Base } if (t > 0) { - delay_npi_implementation = dyn_npis.get_implementation_delay(); + delay_npi_implementation = ScalarType(dyn_npis.get_implementation_delay()); } else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. delay_npi_implementation = 0; @@ -573,7 +573,7 @@ class Simulation : public Base t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { + if (t >= ScalarType(dyn_npis.get_directive_begin()) && t < ScalarType(dyn_npis.get_directive_end())) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); diff --git a/cpp/memilio/epidemiology/dynamic_npis.h b/cpp/memilio/epidemiology/dynamic_npis.h index 43be0ca4d9..d17ae1d86f 100644 --- a/cpp/memilio/epidemiology/dynamic_npis.h +++ b/cpp/memilio/epidemiology/dynamic_npis.h @@ -244,8 +244,8 @@ class DynamicNPIs std::vector>>> m_thresholds; SimulationTime m_duration{14.0}; SimulationTime m_delay{0.0}; - SimulationTime m_directive_begin{-std::numeric_limits::max}; - SimulationTime m_directive_end{std::numeric_limits::max}; + SimulationTime m_directive_begin{SimulationTime(std::numeric_limits::lowest())}; + SimulationTime m_directive_end{SimulationTime(std::numeric_limits::max())}; FP m_base{1.0}; }; diff --git a/cpp/memilio/mobility/metapopulation_mobility_instant.h b/cpp/memilio/mobility/metapopulation_mobility_instant.h index 3180a1d4e0..4f7a485711 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_instant.h +++ b/cpp/memilio/mobility/metapopulation_mobility_instant.h @@ -471,7 +471,7 @@ void calculate_mobility_returns(Eigen::Ref::Vector> mobi } /** - * get the percantage of infected people of the total population in the node + * Get the percentage of infected people of the total population in the node. * If dynamic NPIs are enabled, there needs to be an overload of get_infections_relative(model, y) * for the Model type that can be found with argument-dependent lookup. Ideally define get_infections_relative * in the same namespace as the Model type. diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index fbba15c7be..1d40ebabf3 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -291,14 +291,14 @@ class Simulation : public BaseT auto& dyn_npis = this->get_model().parameters.template get>(); auto& contact_patterns = this->get_model().parameters.template get>(); + FP delay_npi_implementation; FP t = BaseT::get_result().get_last_time(); - while (t < tmax) { - FP dt_eff = min(dt, tmax - t); + FP dt_eff = min(1.0, tmax - t); BaseT::advance(t + dt_eff); if (t > 0) { - delay_npi_implementation = dyn_npis.get_implementation_delay(); + delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } else { // DynamicNPIs for t=0 are 'misused' to be 'from-start NPIs'. I.e., do not enforce delay. delay_npi_implementation = 0; @@ -306,7 +306,7 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { + if (t >= FP(dyn_npis.get_directive_begin()) && t < FP(dyn_npis.get_directive_end())) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); diff --git a/cpp/models/ode_secirts/model.h b/cpp/models/ode_secirts/model.h index a7a98952e1..b879b415e2 100644 --- a/cpp/models/ode_secirts/model.h +++ b/cpp/models/ode_secirts/model.h @@ -765,9 +765,7 @@ class Simulation : public BaseT FP delay_npi_implementation; FP t = BaseT::get_result().get_last_time(); while (t < tmax) { - - auto dt_eff = min(dt, tmax - t); - dt_eff = min(dt_eff, 1.0); + auto dt_eff = min(1.0, tmax - t); BaseT::advance(t + dt_eff); if (t + 0.5 + dt_eff - floor(t + 0.5) >= 1) { @@ -775,7 +773,7 @@ class Simulation : public BaseT } if (t > 0) { - delay_npi_implementation = dyn_npis.get_implementation_delay(); + delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. @@ -784,7 +782,7 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { + if (t >= FP(dyn_npis.get_directive_begin()) && t < FP(dyn_npis.get_directive_end())) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); diff --git a/cpp/models/ode_secirvvs/analyze_result.h b/cpp/models/ode_secirvvs/analyze_result.h index 8c9cab6a42..2544bb9a98 100644 --- a/cpp/models/ode_secirvvs/analyze_result.h +++ b/cpp/models/ode_secirvvs/analyze_result.h @@ -67,109 +67,128 @@ std::vector ensemble_params_percentile(const std::vector(0); compart < InfectionState::Count; ++compart) { - param_percentil(node, [compart, i](auto&& model) -> auto& { - return model.populations[{i, compart}]; - }); + param_percentil( + node, [ compart, i ](auto&& model) -> auto& { + return model.populations[{i, compart}]; + }); } // times - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { return model.parameters.template get>()[i]; }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); //probs - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); //vaccinations - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); - - for (auto day = SimulationDay(0); day < num_days; ++day) { - param_percentil(node, [i, day](auto&& model) -> auto& { - return model.parameters.template get>()[{i, day}]; + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; }); - param_percentil(node, [i, day](auto&& model) -> auto& { - return model.parameters.template get>()[{i, day}]; + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); + + for (auto day = SimulationDay(0); day < num_days; ++day) { + param_percentil( + node, [ i, day ](auto&& model) -> auto& { + return model.parameters.template get>()[{i, day}]; + }); + param_percentil( + node, [ i, day ](auto&& model) -> auto& { + return model.parameters.template get>()[{i, day}]; + }); } //virus variants - param_percentil(node, [i](auto&& model) -> auto& { - return model.parameters.template get>()[i]; - }); + param_percentil( + node, [i](auto&& model) -> auto& { + return model.parameters.template get>()[i]; + }); } // group independent params - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); - param_percentil(node, [](auto&& model) -> auto& { - return model.parameters.template get>(); - }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); + param_percentil( + node, [](auto&& model) -> auto& { return model.parameters.template get>(); }); for (size_t run = 0; run < num_runs; run++) { auto const& params = ensemble_params[run][node]; diff --git a/cpp/models/ode_secirvvs/model.h b/cpp/models/ode_secirvvs/model.h index f05b6b8490..acb6deabda 100644 --- a/cpp/models/ode_secirvvs/model.h +++ b/cpp/models/ode_secirvvs/model.h @@ -687,9 +687,7 @@ class Simulation : public BaseT FP delay_npi_implementation; FP t = BaseT::get_result().get_last_time(); while (t < tmax) { - - FP dt_eff = min(dt, tmax - t); - dt_eff = min(dt_eff, 1.0); + FP dt_eff = min(1.0, tmax - t); if (t == 0) { //this->apply_vaccination(t); // done in init now? @@ -702,7 +700,7 @@ class Simulation : public BaseT } if (t > 0) { - delay_npi_implementation = dyn_npis.get_implementation_delay(); + delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. delay_npi_implementation = 0; @@ -710,7 +708,7 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= dyn_npis.get_directive_begin() && t < dyn_npis.get_directive_end()) { + if (t >= FP(dyn_npis.get_directive_begin()) && t < FP(dyn_npis.get_directive_end())) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); diff --git a/cpp/models/ode_secirvvs/parameter_space.h b/cpp/models/ode_secirvvs/parameter_space.h index 875c08f5ef..a9078fadcb 100644 --- a/cpp/models/ode_secirvvs/parameter_space.h +++ b/cpp/models/ode_secirvvs/parameter_space.h @@ -176,8 +176,6 @@ Graph, MobilityParameters> draw_sample(Graph, MobilityPa shared_contacts.draw_sample_dampings(); auto& shared_dynamic_npis = shared_params_model.parameters.template get>(); shared_dynamic_npis.draw_sample(); - auto& shared_dynamic_npis_delay = shared_params_model.parameters.template get>(); - shared_dynamic_npis_delay.draw_sample(); for (auto& params_node : graph.nodes()) { auto& node_model = params_node.property; diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index c8ec99734d..2c6f0b88e8 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -66,20 +66,20 @@ TEST(DynamicNPIs, get_threshold) TEST(DynamicNPIs, get_and_set_delay) { mio::DynamicNPIs npis; - EXPECT_EQ(npis.get_implementation_delay(), 0.0); - npis.set_implementation_delay(7.0); - EXPECT_EQ(npis.get_implementation_delay(), 7.0); + EXPECT_EQ(npis.get_implementation_delay(), mio::SimulationTime(0.0)); + npis.set_implementation_delay(mio::SimulationTime(7.0)); + EXPECT_EQ(npis.get_implementation_delay(), mio::SimulationTime(7.0)); } TEST(DynamicNPIs, get_and_set_directive_begin_end) { mio::DynamicNPIs npis; - EXPECT_LE(npis.get_directive_begin(), -1e4); - EXPECT_GE(npis.get_directive_end(), 1e4); - npis.set_directive_begin(7.0); - EXPECT_EQ(npis.get_directive_begin(), 7.0); - npis.set_directive_end(14.0); - EXPECT_EQ(npis.get_directive_end(), 14.0); + EXPECT_LE(npis.get_directive_begin(), mio::SimulationTime(-1e4)); + EXPECT_GE(npis.get_directive_end(), mio::SimulationTime(1e4)); + npis.set_directive_begin(mio::SimulationTime(7.0)); + EXPECT_EQ(npis.get_directive_begin(), mio::SimulationTime(7.0)); + npis.set_directive_end(mio::SimulationTime(14.0)); + EXPECT_EQ(npis.get_directive_end(), mio::SimulationTime(14.0)); } TEST(DynamicNPIs, get_damping_indices) @@ -463,8 +463,8 @@ TEST(DynamicNPIs, secir_threshold_exceeded) Eigen::VectorXd::Ones(1)}}); npis.set_duration(mio::SimulationTime(5.0)); npis.set_base_value(50'000); - model.parameters.get>() = npis; - model.parameters.get>() = 0.0; + npis.set_implementation_delay(mio::SimulationTime(0.0)); + model.parameters.get>() = npis; EXPECT_EQ(model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), 0); @@ -504,7 +504,8 @@ TEST(DynamicNPIs, secir_delayed_implementation) 0); // start with t0 = 0.0 - model.parameters.get>() = 3.0; + npis.set_implementation_delay(mio::SimulationTime(3.0)); + model.parameters.get>() = npis; mio::osecir::Simulation> sim(model, 0.0); sim.advance(3.0); mio::ContactMatrixGroup const& contact_matrix = @@ -513,8 +514,9 @@ TEST(DynamicNPIs, secir_delayed_implementation) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // second simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 6.0 - const auto tmax = 4.0; - model.parameters.get>() = 2.0; + const auto tmax = 4.0; + npis.set_implementation_delay(mio::SimulationTime(2.0)); + model.parameters.get>() = npis; mio::osecir::Simulation> sim_2(model, 1.0); sim_2.advance(tmax); mio::ContactMatrixGroup const& contact_matrix_sim_2 = @@ -523,7 +525,8 @@ TEST(DynamicNPIs, secir_delayed_implementation) EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 0.5); // third simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 14.0 - model.parameters.get>() = 10.0; + npis.set_implementation_delay(mio::SimulationTime(10.0)); + model.parameters.get>() = npis; mio::osecir::Simulation> sim_3(model, 1.0); sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = @@ -591,8 +594,8 @@ TEST(DynamicNPIs, secirvvs_threshold_exceeded) Eigen::VectorXd::Ones(1)}}); npis.set_duration(mio::SimulationTime(5.0)); npis.set_base_value(50'000); - model.parameters.get>() = npis; - model.parameters.get>() = 0.0; + npis.set_implementation_delay(mio::SimulationTime(0.0)); + model.parameters.get>() = npis; EXPECT_EQ( model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), @@ -648,8 +651,9 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // second simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 6.0 - const auto tmax = 4.0; - model.parameters.get>() = 2.0; + const auto tmax = 4.0; + npis.set_implementation_delay(mio::SimulationTime(2.0)); + model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_2(model, 1.0); sim_2.advance(tmax); mio::ContactMatrixGroup const& contact_matrix_sim_2 = @@ -658,7 +662,8 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 0.5); // third simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 14.0 - model.parameters.get>() = 10.0; + npis.set_implementation_delay(mio::SimulationTime(10.0)); + model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_3(model, 1.0); sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = diff --git a/cpp/tests/test_odesecir.cpp b/cpp/tests/test_odesecir.cpp index 5f4b25c09a..a1a5edb09b 100755 --- a/cpp/tests/test_odesecir.cpp +++ b/cpp/tests/test_odesecir.cpp @@ -1139,13 +1139,6 @@ TEST(TestOdeSecir, check_constraints_parameters) model.parameters.set>(0.5); model.parameters.set>(1.1); ASSERT_EQ(model.parameters.check_constraints(), 1); - - model.parameters.set>(1.0); - model.parameters.set>(-4); - ASSERT_EQ(model.parameters.check_constraints(), 1); - - model.parameters.set>(3); - EXPECT_EQ(model.parameters.check_constraints(), 0); } TEST(TestOdeSecir, apply_constraints_parameters) diff --git a/cpp/tests/test_odesecirts.cpp b/cpp/tests/test_odesecirts.cpp index 33b3bd505c..b3969b6cde 100755 --- a/cpp/tests/test_odesecirts.cpp +++ b/cpp/tests/test_odesecirts.cpp @@ -465,7 +465,7 @@ void set_contact_parameters(mio::osecirts::Model::ParameterSet& paramete mio::SimulationTime(0), {0}, npi_groups)}); npis.set_base_value(100'000); npis.set_duration(mio::SimulationTime(14.0)); - npis.set_directive_end(10.0); //required for dynamic NPIs to have effect in this model + npis.set_directive_end(mio::SimulationTime(10.0)); //required for dynamic NPIs to have effect in this model } void set_covid_parameters(mio::osecirts::Model::ParameterSet& params, bool set_invalid_initial_value) diff --git a/cpp/tests/test_odesecirvvs.cpp b/cpp/tests/test_odesecirvvs.cpp index bc7e288068..ebe3b75612 100755 --- a/cpp/tests/test_odesecirvvs.cpp +++ b/cpp/tests/test_odesecirvvs.cpp @@ -294,8 +294,8 @@ void set_contact_parameters(mio::osecirvvs::Model::ParameterSet& paramet npis.set_base_value(100'000); npis.set_implementation_delay(mio::SimulationTime(3.0)); npis.set_duration(mio::SimulationTime(14.0)); - npis.set_directive_end(10.0); // required for dynamic NPIs to have effect in this model - npis.set_implementation_delay(7.0); + npis.set_directive_end(mio::SimulationTime(10.0)); // required for dynamic NPIs to have effect in this model + npis.set_implementation_delay(mio::SimulationTime(7.0)); } void set_covid_parameters(mio::osecirvvs::Model::ParameterSet& params, bool set_invalid_initial_value) diff --git a/cpp/tests/test_uncertain_ad_compatibility.cpp b/cpp/tests/test_uncertain_ad_compatibility.cpp index ed14eb2139..d928b2c8f5 100644 --- a/cpp/tests/test_uncertain_ad_compatibility.cpp +++ b/cpp/tests/test_uncertain_ad_compatibility.cpp @@ -138,10 +138,9 @@ TEST(TestUncertainADCompatibility, create_model) mio::osecirvvs::Model model(num_age_groups); auto& params = model.parameters; - params.template get>() = 100; - params.template get>() = 0.0143; - params.template get>() = 0.2; - params.template get>() = 7; + params.template get>() = 100; + params.template get>() = 0.0143; + params.template get>() = 0.2; using std::floor; size_t tmax_days = static_cast(floor(tmax)); From b1d28165fbaddd5b91098a8ee6df4c52f7b84a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Joachim=20K=C3=BChn?= Date: Mon, 30 Mar 2026 22:48:44 +0200 Subject: [PATCH 05/19] Fixed dynamicNPI tests --- cpp/tests/test_dynamic_npis.cpp | 38 +++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index 2c6f0b88e8..b1b24a1a0b 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -503,17 +503,18 @@ TEST(DynamicNPIs, secir_delayed_implementation) EXPECT_EQ(model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), 0); - // start with t0 = 0.0 + // start with t0 = 0.0 so dynamicNPIs are active from the start (with one day delay for smoothing of contacts) npis.set_implementation_delay(mio::SimulationTime(3.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim(model, 0.0); sim.advance(3.0); mio::ContactMatrixGroup const& contact_matrix = sim.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // second simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 6.0 + // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + one day delay for smoothing of contacts const auto tmax = 4.0; npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; @@ -521,18 +522,18 @@ TEST(DynamicNPIs, secir_delayed_implementation) sim_2.advance(tmax); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); - // third simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 14.0 + // third simulation; NPIs are implemented at t0 + delay = 11.0 + one day delay for smoothing of contacts npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_3(model, 1.0); - sim_3.advance(4.0); + sim_3.advance(tmax); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(13.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(14.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); } TEST(DynamicNPIs, secirvvs_threshold_safe) @@ -642,15 +643,16 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), 0); - // start with t0 = 0.0 + // start with t0 = 0.0 so dynamicNPIs are active from the start (with one day delay for smoothing of contacts) mio::osecirvvs::Simulation> sim(model, 0.0); sim.advance(3.0); mio::ContactMatrixGroup const& contact_matrix = sim.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // second simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 6.0 + // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + one day delay for smoothing of contacts const auto tmax = 4.0; npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; @@ -658,16 +660,16 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) sim_2.advance(tmax); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); - // third simulation with t0 = 1.0, so the NPIs are implemented at tmax + delay = 14.0 + // third simulation; NPIs are implemented at t0 + delay = 11.0 + one day delay for smoothing of contacts npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_3(model, 1.0); - sim_3.advance(4.0); + sim_3.advance(tmax); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(13.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(14.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); } From 7b3e4d91aa22df1a6658f6c76fb8da43fb5ec90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Joachim=20K=C3=BChn?= Date: Mon, 30 Mar 2026 23:52:19 +0200 Subject: [PATCH 06/19] tests for directive begin and end in secir --- cpp/benchmarks/flow_simulation_ode_secirvvs.h | 29 +++--- cpp/models/ode_secir/model.h | 21 ++-- cpp/models/ode_secirts/model.h | 25 +++-- cpp/models/ode_secirvvs/model.h | 25 +++-- cpp/tests/test_dynamic_npis.cpp | 95 +++++++++++++++++-- cpp/tests/test_odesecirts.cpp | 2 +- cpp/tests/test_odesecirvvs.cpp | 2 +- 7 files changed, 151 insertions(+), 48 deletions(-) diff --git a/cpp/benchmarks/flow_simulation_ode_secirvvs.h b/cpp/benchmarks/flow_simulation_ode_secirvvs.h index 85e0a3198f..0ee938070b 100644 --- a/cpp/benchmarks/flow_simulation_ode_secirvvs.h +++ b/cpp/benchmarks/flow_simulation_ode_secirvvs.h @@ -551,8 +551,7 @@ class Simulation : public Base auto t = Base::get_result().get_last_time(); while (t < tmax) { - auto dt_eff = min(dt, tmax - t); - dt_eff = min(dt_eff, 1.0); + auto dt_eff = min(1.0, tmax - t); if (t == 0) { //this->apply_vaccination(t); // done in init now? @@ -573,7 +572,9 @@ class Simulation : public Base t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= ScalarType(dyn_npis.get_directive_begin()) && t < ScalarType(dyn_npis.get_directive_end())) { + ScalarType direc_begin = ScalarType(dyn_npis.get_directive_begin()); + ScalarType direc_end = ScalarType(dyn_npis.get_directive_end()); + if (floating_point_greater_equal(t, direc_begin, 1e-10) && t < direc_end) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); @@ -581,15 +582,19 @@ class Simulation : public Base (exceeded_threshold->first > m_dynamic_npi.first || t > ScalarType(m_dynamic_npi.second))) { // old npi was weaker or is expired - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); - this->get_model().parameters.get_start_commuter_detection() = t_start.get(); - this->get_model().parameters.get_end_commuter_detection() = t_end.get(); - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); + if (t + delay_npi_implementation < direc_end) { + auto t_start = SimulationTime(t + delay_npi_implementation); + // set the end to the minimum of start+delay and the end of the directive + auto t_end = SimulationTime( + min(direc_end, ScalarType(t_start + dyn_npis.get_duration()))); + this->get_model().parameters.get_start_commuter_detection() = t_start.get(); + this->get_model().parameters.get_end_commuter_detection() = t_end.get(); + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); + } } } } diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index 1d40ebabf3..7636894a8c 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -306,7 +306,9 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= FP(dyn_npis.get_directive_begin()) && t < FP(dyn_npis.get_directive_end())) { + FP direc_begin = FP(dyn_npis.get_directive_begin()); + FP direc_end = FP(dyn_npis.get_directive_end()); + if (floating_point_greater_equal(t, direc_begin, 1e-10) && t < direc_end) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); @@ -314,13 +316,16 @@ class Simulation : public BaseT (exceeded_threshold->first > m_dynamic_npi.first || t > FP(m_dynamic_npi.second))) { // old npi was weaker or is expired - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(FP(dyn_npis.get_duration())); - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); + if (t + delay_npi_implementation < direc_end) { + auto t_start = SimulationTime(t + delay_npi_implementation); + // set the end to the minimum of start+delay and the end of the directive + auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); + } } } } diff --git a/cpp/models/ode_secirts/model.h b/cpp/models/ode_secirts/model.h index b879b415e2..baf595f55c 100644 --- a/cpp/models/ode_secirts/model.h +++ b/cpp/models/ode_secirts/model.h @@ -782,7 +782,9 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= FP(dyn_npis.get_directive_begin()) && t < FP(dyn_npis.get_directive_end())) { + FP direc_begin = FP(dyn_npis.get_directive_begin()); + FP direc_end = FP(dyn_npis.get_directive_end()); + if (floating_point_greater_equal(t, direc_begin, 1e-10) && t < direc_end) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); @@ -790,15 +792,18 @@ class Simulation : public BaseT (exceeded_threshold->first > m_dynamic_npi.first || t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); - this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; - this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); + if (t + delay_npi_implementation < direc_end) { + auto t_start = SimulationTime(t + delay_npi_implementation); + // set the end to the minimum of start+delay and the end of the directive + auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); + this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; + this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); + } } } } diff --git a/cpp/models/ode_secirvvs/model.h b/cpp/models/ode_secirvvs/model.h index acb6deabda..72c64945ab 100644 --- a/cpp/models/ode_secirvvs/model.h +++ b/cpp/models/ode_secirvvs/model.h @@ -708,7 +708,9 @@ class Simulation : public BaseT t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { - if (t >= FP(dyn_npis.get_directive_begin()) && t < FP(dyn_npis.get_directive_end())) { + FP direc_begin = FP(dyn_npis.get_directive_begin()); + FP direc_end = FP(dyn_npis.get_directive_end()); + if (floating_point_greater_equal(t, direc_begin, 1e-10) && t < direc_end) { auto inf_rel = get_infections_relative(*this, t, this->get_result().get_last_value()) * dyn_npis.get_base_value(); auto exceeded_threshold = dyn_npis.get_max_exceeded_threshold(inf_rel); @@ -716,15 +718,18 @@ class Simulation : public BaseT (exceeded_threshold->first > m_dynamic_npi.first || t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired - auto t_start = SimulationTime(t + delay_npi_implementation); - auto t_end = t_start + SimulationTime(dyn_npis.get_duration()); - this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; - this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; - m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { - return make_contact_damping_matrix(g); - }); + if (t + delay_npi_implementation < direc_end) { + auto t_start = SimulationTime(t + delay_npi_implementation); + // set the end to the minimum of start+delay and the end of the directive + auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); + this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; + this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; + m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, + t_start, t_end, [](auto& g) { + return make_contact_damping_matrix(g); + }); + } } } } diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index b1b24a1a0b..bb2c861498 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -515,11 +515,10 @@ TEST(DynamicNPIs, secir_delayed_implementation) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + one day delay for smoothing of contacts - const auto tmax = 4.0; npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_2(model, 1.0); - sim_2.advance(tmax); + sim_2.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); @@ -529,13 +528,98 @@ TEST(DynamicNPIs, secir_delayed_implementation) npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_3(model, 1.0); - sim_3.advance(tmax); + sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); } +TEST(DynamicNPIs, secir_implementation_with_directives) +{ + mio::osecir::Model model(1); + model.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedSymptoms}] = 10; + model.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecir::InfectionState::Susceptible}, 100); + + mio::ContactMatrixGroup& cm = model.parameters.get>(); + cm[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 1.0)); + + mio::DynamicNPIs npis; + npis.set_threshold(0.05 * 50'000, {mio::DampingSampling{0.5, + mio::DampingLevel(0), + mio::DampingType(0), + mio::SimulationTime(0), + {0}, + Eigen::VectorXd::Ones(1)}}); + npis.set_duration(mio::SimulationTime(5.0)); + npis.set_base_value(50'000); + model.parameters.get>() = npis; + + EXPECT_EQ(model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), + 0); + + // directive begin is after the simulation, so no NPI is implemented + npis.set_directive_begin(mio::SimulationTime(5.0)); + model.parameters.get>() = npis; + mio::osecir::Simulation> sim(model, 0.0); + sim.advance(3.0); + mio::ContactMatrixGroup const& contact_matrix = + sim.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + + // directive begin is satisfied + npis.set_implementation_delay(mio::SimulationTime(2.0)); // not used as t0=0 + npis.set_directive_begin(mio::SimulationTime(0.0)); + model.parameters.get>() = npis; + mio::osecir::Simulation> sim_2(model, 0.0); + sim_2.advance(3.0); + mio::ContactMatrixGroup const& contact_matrix_sim_2 = + sim_2.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 1.0); // lifted after duration + + // directive begin is satisfied but directive end ends the NPI earlier + npis.set_directive_end(mio::SimulationTime(3.0)); + model.parameters.get>() = npis; + mio::osecir::Simulation> sim_3(model, 0.0); + sim_3.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_3 = + sim_3.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + + // directive begin is satisfied (now with delay>0 as t0=1) + npis.set_implementation_delay(mio::SimulationTime(2.0)); + npis.set_directive_begin(mio::SimulationTime(0.0)); + npis.set_directive_end(mio::SimulationTime(1000000.)); + model.parameters.get>() = npis; + mio::osecir::Simulation> sim_4(model, 1.0); + sim_4.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_4 = + sim_4.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(9.0))(0, 0), 1.0); // lifted after duration + + // directive begin is satisfied but directive end ends the NPI earlier (now with delay>0 as t0=1) + npis.set_directive_end(mio::SimulationTime(5.0)); + model.parameters.get>() = npis; + mio::osecir::Simulation> sim_5(model, 1.0); + sim_5.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_5 = + sim_5.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // starts lifting then + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); +} + TEST(DynamicNPIs, secirvvs_threshold_safe) { mio::osecirvvs::Model model(1); @@ -653,11 +737,10 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + one day delay for smoothing of contacts - const auto tmax = 4.0; npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_2(model, 1.0); - sim_2.advance(tmax); + sim_2.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); @@ -667,7 +750,7 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_3(model, 1.0); - sim_3.advance(tmax); + sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); diff --git a/cpp/tests/test_odesecirts.cpp b/cpp/tests/test_odesecirts.cpp index b3969b6cde..7982c95e2f 100755 --- a/cpp/tests/test_odesecirts.cpp +++ b/cpp/tests/test_odesecirts.cpp @@ -465,7 +465,7 @@ void set_contact_parameters(mio::osecirts::Model::ParameterSet& paramete mio::SimulationTime(0), {0}, npi_groups)}); npis.set_base_value(100'000); npis.set_duration(mio::SimulationTime(14.0)); - npis.set_directive_end(mio::SimulationTime(10.0)); //required for dynamic NPIs to have effect in this model + // npis.set_directive_end(mio::SimulationTime(10.0)); // --> can probably be removed??? } void set_covid_parameters(mio::osecirts::Model::ParameterSet& params, bool set_invalid_initial_value) diff --git a/cpp/tests/test_odesecirvvs.cpp b/cpp/tests/test_odesecirvvs.cpp index ebe3b75612..b1fb9e6ee1 100755 --- a/cpp/tests/test_odesecirvvs.cpp +++ b/cpp/tests/test_odesecirvvs.cpp @@ -294,7 +294,7 @@ void set_contact_parameters(mio::osecirvvs::Model::ParameterSet& paramet npis.set_base_value(100'000); npis.set_implementation_delay(mio::SimulationTime(3.0)); npis.set_duration(mio::SimulationTime(14.0)); - npis.set_directive_end(mio::SimulationTime(10.0)); // required for dynamic NPIs to have effect in this model + // npis.set_directive_end(mio::SimulationTime(10.0)); // --> can probably be removed??? npis.set_implementation_delay(mio::SimulationTime(7.0)); } From a23f791d4b4517725460179c39a5ef275d4edbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Joachim=20K=C3=BChn?= Date: Tue, 31 Mar 2026 00:39:38 +0200 Subject: [PATCH 07/19] order dynamicNPIs and advance changed --- cpp/benchmarks/flow_simulation_ode_secirvvs.h | 25 ++++---- cpp/models/ode_secir/model.h | 14 ++++- cpp/models/ode_secirts/model.h | 14 ++--- cpp/models/ode_secirvvs/model.h | 24 ++++---- cpp/tests/test_dynamic_npis.cpp | 58 +++++++++---------- 5 files changed, 70 insertions(+), 65 deletions(-) diff --git a/cpp/benchmarks/flow_simulation_ode_secirvvs.h b/cpp/benchmarks/flow_simulation_ode_secirvvs.h index 0ee938070b..b00a623d67 100644 --- a/cpp/benchmarks/flow_simulation_ode_secirvvs.h +++ b/cpp/benchmarks/flow_simulation_ode_secirvvs.h @@ -551,25 +551,16 @@ class Simulation : public Base auto t = Base::get_result().get_last_time(); while (t < tmax) { - auto dt_eff = min(1.0, tmax - t); - - if (t == 0) { - //this->apply_vaccination(t); // done in init now? - this->apply_variant(t, base_infectiousness); - } - Base::advance(t + dt_eff); - if (t + 0.5 + dt_eff - std::floor(t + 0.5) >= 1) { - this->apply_vaccination(t + 0.5 + dt_eff); - this->apply_variant(t, base_infectiousness); - } - if (t > 0) { delay_npi_implementation = ScalarType(dyn_npis.get_implementation_delay()); } else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. delay_npi_implementation = 0; } - t = t + dt_eff; + if (t == 0) { + //this->apply_vaccination(t); // done in init now? + this->apply_variant(t, base_infectiousness); + } if (dyn_npis.get_thresholds().size() > 0) { ScalarType direc_begin = ScalarType(dyn_npis.get_directive_begin()); @@ -598,6 +589,14 @@ class Simulation : public Base } } } + + auto dt_eff = min(1.0, tmax - t); + Base::advance(t + dt_eff); + if (t + 0.5 + dt_eff - std::floor(t + 0.5) >= 1) { + this->apply_vaccination(t + 0.5 + dt_eff); + this->apply_variant(t, base_infectiousness); + } + t = t + dt_eff; } this->get_model().parameters.template get>() = diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index 7636894a8c..1d1bec8a6e 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -294,16 +294,20 @@ class Simulation : public BaseT FP delay_npi_implementation; FP t = BaseT::get_result().get_last_time(); while (t < tmax) { - FP dt_eff = min(1.0, tmax - t); + // FP t_next_damp = t; + // for (auto&& mat : contact_patterns.get_cont_freq_mat()) { + // for (auto&& damp : mat.get_dampings()) { + // FP t_damp = damp.get_time().get(); + // if(t_damp > t && t_damp < + // } + // } - BaseT::advance(t + dt_eff); if (t > 0) { delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } else { // DynamicNPIs for t=0 are 'misused' to be 'from-start NPIs'. I.e., do not enforce delay. delay_npi_implementation = 0; } - t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { FP direc_begin = FP(dyn_npis.get_directive_begin()); @@ -329,6 +333,10 @@ class Simulation : public BaseT } } } + + FP dt_eff = min(1.0, tmax - t); + BaseT::advance(t + dt_eff); + t = t + dt_eff; } return this->get_result().get_last_value(); diff --git a/cpp/models/ode_secirts/model.h b/cpp/models/ode_secirts/model.h index baf595f55c..cdf314947d 100644 --- a/cpp/models/ode_secirts/model.h +++ b/cpp/models/ode_secirts/model.h @@ -765,12 +765,6 @@ class Simulation : public BaseT FP delay_npi_implementation; FP t = BaseT::get_result().get_last_time(); while (t < tmax) { - auto dt_eff = min(1.0, tmax - t); - - BaseT::advance(t + dt_eff); - if (t + 0.5 + dt_eff - floor(t + 0.5) >= 1) { - this->apply_variant(t, base_infectiousness); - } if (t > 0) { delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); @@ -779,7 +773,6 @@ class Simulation : public BaseT // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. delay_npi_implementation = 0; } - t = t + dt_eff; if (dyn_npis.get_thresholds().size() > 0) { FP direc_begin = FP(dyn_npis.get_directive_begin()); @@ -807,6 +800,13 @@ class Simulation : public BaseT } } } + + auto dt_eff = min(1.0, tmax - t); + BaseT::advance(t + dt_eff); + if (t + 0.5 + dt_eff - floor(t + 0.5) >= 1) { + this->apply_variant(t, base_infectiousness); + } + t = t + dt_eff; } // reset TransmissionProbabilityOnContact. This is important for the graph simulation where the advance // function is called multiple times for the same model. diff --git a/cpp/models/ode_secirvvs/model.h b/cpp/models/ode_secirvvs/model.h index 72c64945ab..be50b738b8 100644 --- a/cpp/models/ode_secirvvs/model.h +++ b/cpp/models/ode_secirvvs/model.h @@ -687,17 +687,6 @@ class Simulation : public BaseT FP delay_npi_implementation; FP t = BaseT::get_result().get_last_time(); while (t < tmax) { - FP dt_eff = min(1.0, tmax - t); - - if (t == 0) { - //this->apply_vaccination(t); // done in init now? - this->apply_variant(t, base_infectiousness); - } - BaseT::advance(t + dt_eff); - if (t + 0.5 + dt_eff - floor(t + 0.5) >= 1) { - this->apply_vaccination(t + 0.5 + dt_eff); - this->apply_variant(t, base_infectiousness); - } if (t > 0) { delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); @@ -705,7 +694,10 @@ class Simulation : public BaseT else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. delay_npi_implementation = 0; } - t = t + dt_eff; + if (t == 0) { + //this->apply_vaccination(t); // done in init now? + this->apply_variant(t, base_infectiousness); + } if (dyn_npis.get_thresholds().size() > 0) { FP direc_begin = FP(dyn_npis.get_directive_begin()); @@ -733,6 +725,14 @@ class Simulation : public BaseT } } } + + FP dt_eff = min(1.0, tmax - t); + BaseT::advance(t + dt_eff); + if (t + 0.5 + dt_eff - floor(t + 0.5) >= 1) { + this->apply_vaccination(t + 0.5 + dt_eff); + this->apply_variant(t, base_infectiousness); + } + t = t + dt_eff; } // reset TransmissionProbabilityOnContact. This is important for the graph simulation where the advance // function is called multiple times for the same model. diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index bb2c861498..f3b541fa3f 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -503,36 +503,36 @@ TEST(DynamicNPIs, secir_delayed_implementation) EXPECT_EQ(model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), 0); - // start with t0 = 0.0 so dynamicNPIs are active from the start (with one day delay for smoothing of contacts) + // start with t0 = 0.0 so dynamicNPIs are active from the start npis.set_implementation_delay(mio::SimulationTime(3.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim(model, 0.0); sim.advance(3.0); mio::ContactMatrixGroup const& contact_matrix = sim.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + one day delay for smoothing of contacts + // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_2(model, 1.0); sim_2.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // third simulation; NPIs are implemented at t0 + delay = 11.0 + one day delay for smoothing of contacts + // third simulation; NPIs are implemented at t0 + delay = 11.0 npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_3(model, 1.0); sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); } TEST(DynamicNPIs, secir_implementation_with_directives) @@ -577,10 +577,9 @@ TEST(DynamicNPIs, secir_implementation_with_directives) sim_2.advance(3.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted after duration // directive begin is satisfied but directive end ends the NPI earlier npis.set_directive_end(mio::SimulationTime(3.0)); @@ -589,8 +588,7 @@ TEST(DynamicNPIs, secir_implementation_with_directives) sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); @@ -603,21 +601,21 @@ TEST(DynamicNPIs, secir_implementation_with_directives) sim_4.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_4 = sim_4.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(9.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(7.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 1.0); // lifted after duration // directive begin is satisfied but directive end ends the NPI earlier (now with delay>0 as t0=1) - npis.set_directive_end(mio::SimulationTime(5.0)); + npis.set_directive_end(mio::SimulationTime(4.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_5(model, 1.0); sim_5.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_5 = sim_5.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // starts lifting then - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // starts lifting then + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); } TEST(DynamicNPIs, secirvvs_threshold_safe) @@ -727,32 +725,32 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), 0); - // start with t0 = 0.0 so dynamicNPIs are active from the start (with one day delay for smoothing of contacts) + // start with t0 = 0.0 so dynamicNPIs are active from the start mio::osecirvvs::Simulation> sim(model, 0.0); sim.advance(3.0); mio::ContactMatrixGroup const& contact_matrix = sim.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + one day delay for smoothing of contacts + // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_2(model, 1.0); sim_2.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // third simulation; NPIs are implemented at t0 + delay = 11.0 + one day delay for smoothing of contacts + // third simulation; NPIs are implemented at t0 + delay = 11.0 npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_3(model, 1.0); sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); } From d78d25ab7d78e16de09f9af016795e14a8c1ece4 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:30:16 +0200 Subject: [PATCH 08/19] Some documentation + assert in setters in dynamic_npis.h --- cpp/memilio/epidemiology/dynamic_npis.h | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cpp/memilio/epidemiology/dynamic_npis.h b/cpp/memilio/epidemiology/dynamic_npis.h index d17ae1d86f..73f2a1e968 100644 --- a/cpp/memilio/epidemiology/dynamic_npis.h +++ b/cpp/memilio/epidemiology/dynamic_npis.h @@ -23,6 +23,8 @@ #include "memilio/epidemiology/damping_sampling.h" #include "memilio/utils/stl_util.h" +#include + namespace mio { @@ -54,7 +56,7 @@ class DynamicNPIs */ auto get_max_exceeded_threshold(FP value) { - //thresholds are sorted by value descending, so upper_bound returns the first threshold that is smaller using binary search + // thresholds are sorted by value descending, so upper_bound returns the first threshold that is smaller using binary search auto iter_max_exceeded_threshold = std::upper_bound(m_thresholds.begin(), m_thresholds.end(), value, [](auto& val, auto& t2) { return val > t2.first; @@ -98,21 +100,22 @@ class DynamicNPIs /** * Get/Set the implementation delay at which the NPIs are implemented after threshold exceedance. - * This parameters imitates delayed reaction times when automatic implementations should be realized. + * This parameter imitates delayed reaction times when automatic implementations should be realized. * @{ */ /** - * @return the implementation delay after which the NPIs is implemented upon threshold exceedance. + * @return the implementation delay after which the NPIs are implemented upon threshold exceedance. */ SimulationTime get_implementation_delay() const { return m_delay; } /** - * @param delay The implementation delay after which the NPIs is implemented upon threshold exceedance. + * @param delay The implementation delay after which the NPIs are implemented upon threshold exceedance. */ void set_implementation_delay(SimulationTime delay) { + assert(delay >= SimulationTime(0.0) && "Implementation delay must be non-negative."); m_delay = delay; } /**@}*/ @@ -156,12 +159,13 @@ class DynamicNPIs */ void set_directive_begin(SimulationTime begin) { + assert(begin < m_directive_end && "Directive begin must be before directive end."); m_directive_begin = begin; } /**@}*/ /** - * Get/Set the first day of the simulation for which a DynamicNPI *can* be active. + * Get/Set the last day of the simulation for which a DynamicNPI *can* be active. * This parameter imitates the last date of a legal directive and ends all active DynamicNPIs. * @{ */ @@ -177,6 +181,7 @@ class DynamicNPIs */ void set_directive_end(SimulationTime end) { + assert(m_directive_begin < end && "Directive end must be strictly after directive begin."); m_directive_end = end; } /**@}*/ @@ -338,7 +343,7 @@ void implement_dynamic_npis(DampingExprGroup& damping_expr_group, const std::vec auto active = get_active_damping(damping_expr, level, type, begin); auto active_end = get_active_damping(damping_expr, level, type, end) - .eval(); //copy because it may be removed or changed + .eval(); // copy because it may be removed or changed auto value = make_matrix(npi.get_value().value() * npi.get_group_weights()); auto npi_implemented = false; From 6327b88fb67afa362b2c4dab1b0f2b4ac10b8e6a Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:32:08 +0200 Subject: [PATCH 09/19] [ci skip] some doc fixes in metapop instant --- .../mobility/metapopulation_mobility_instant.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cpp/memilio/mobility/metapopulation_mobility_instant.h b/cpp/memilio/mobility/metapopulation_mobility_instant.h index 4f7a485711..dadef6739e 100644 --- a/cpp/memilio/mobility/metapopulation_mobility_instant.h +++ b/cpp/memilio/mobility/metapopulation_mobility_instant.h @@ -387,10 +387,10 @@ class MobilityEdge /** @} */ /** - * compute mobility from node_from to node_to. - * mobility is based on coefficients. + * @brief Compute mobility from node_from to node_to. + * Mobility is based on coefficients. * The mobile population is added to the current state of node_to, subtracted from node_from. - * on return, the mobile population (adjusted for infections) is subtracted from node_to, added to node_from. + * On return, the mobile population (adjusted for infections) is subtracted from node_to, added to node_from. * @param t current time * @param dt last time step (fixed to 0.5 for mobility model) * @param node_from node that people changed from, return to @@ -447,7 +447,7 @@ void MobilityEdge::add_mobility_result_time_point(const FP t) } /** - * adjust number of people that changed node when they return according to the model. + * @brief Adjust number of people that changed node when they return according to the model. * E.g. during the time in the other node, some people who left as susceptible will return exposed. * Implemented for general compartmentmodel simulations, overload for your custom model if necessary * so that it can be found with argument-dependent lookup, i.e. in the same namespace as the model. @@ -471,7 +471,7 @@ void calculate_mobility_returns(Eigen::Ref::Vector> mobi } /** - * Get the percentage of infected people of the total population in the node. + * @brief Get the percentage of infected people of the total population in the node. * If dynamic NPIs are enabled, there needs to be an overload of get_infections_relative(model, y) * for the Model type that can be found with argument-dependent lookup. Ideally define get_infections_relative * in the same namespace as the Model type. @@ -494,7 +494,7 @@ FP get_infections_relative(const SimulationNode& node, FP t, const Eige } /** - * Get an additional mobility factor. + * @brief Get an additional mobility factor. * The absolute mobility for each compartment is computed by c_i * y_i * f_i, wher c_i is the coefficient set in * MobilityParameters, y_i is the current compartment population, f_i is the factor returned by this function. * This factor is optional, default 1.0. If you need to adjust mobility in that way, overload get_mobility_factors(model, t, y) @@ -519,7 +519,7 @@ auto get_mobility_factors(const SimulationNode& node, FP t, const Eigen } /** - * Test persons when moving from their source node. + * @brief Test persons when moving from their source node. * May transfer persons between compartments, e.g., if an infection was detected. * This feature is optional, default implementation does nothing. * In order to support this feature for your model, implement a test_commuters overload @@ -643,7 +643,7 @@ 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. @@ -679,7 +679,7 @@ make_mobility_sim(FP t0, FP dt, Graph, MobilityEdge> /** @} */ /** - * 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. From 0ac1fff547c1948687ec69bd24e86e4d16a31b98 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:58:32 +0200 Subject: [PATCH 10/19] base advance if no dynamic npis used, secirts test for dynamic npis --- cpp/models/ode_secir/model.h | 8 +- cpp/tests/test_dynamic_npis.cpp | 152 +++++++++++++++++++++++++++++++- cpp/tests/test_odesecirvvs.cpp | 7 +- 3 files changed, 159 insertions(+), 8 deletions(-) diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index 1d1bec8a6e..22f9d3bdef 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -288,7 +288,13 @@ class Simulation : public BaseT Eigen::Ref> advance(FP tmax) { using std::min; - auto& dyn_npis = this->get_model().parameters.template get>(); + auto& dyn_npis = this->get_model().parameters.template get>(); + + // If no dynamic NPI thresholds are set, directly use the base advance + if (dyn_npis.get_thresholds().size() == 0) { + return BaseT::advance(tmax); + } + auto& contact_patterns = this->get_model().parameters.template get>(); FP delay_npi_implementation; diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index f3b541fa3f..f1142c702f 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -22,6 +22,7 @@ #include "memilio/utils/compiler_diagnostics.h" #include "ode_secir/model.h" #include "ode_secirvvs/model.h" +#include "ode_secirts/model.h" #include "matchers.h" #include @@ -364,7 +365,7 @@ TEST(DynamicNPIs, mobility) edge.apply_mobility(4.5, 1.5, node_from, node_to); EXPECT_EQ(edge.get_parameters().get_coefficients()[0].get_dampings().size(), - 0); // threshold exceeded, but only check every 3 days + 2); // threshold exceeded, NPI immediately applied EXPECT_CALL(node_from.get_simulation(), advance).Times(1).WillOnce([&](auto t) { node_from.get_simulation().result.add_time_point(t, last_state_crit); @@ -754,3 +755,152 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 1.0); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); } + +TEST(DynamicNPIs, osecirts_delayed_implementation) +{ + mio::osecirts::Model model(1); + model.populations[{mio::AgeGroup(0), mio::osecirts::InfectionState::InfectedSymptomsNaive}] = 10; + model.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecirts::InfectionState::SusceptibleNaive}, + 100); + + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + + mio::ContactMatrixGroup& cm = model.parameters.get>(); + cm[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 1.0)); + + mio::DynamicNPIs npis; + npis.set_threshold(0.05 * 50'000, {mio::DampingSampling{0.5, + mio::DampingLevel(0), + mio::DampingType(0), + mio::SimulationTime(0), + {0}, + Eigen::VectorXd::Ones(1)}}); + npis.set_duration(mio::SimulationTime(5.0)); + npis.set_base_value(50'000); + model.parameters.get>() = npis; + + EXPECT_EQ( + model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), 0); + + // start with t0 = 0.0 so dynamicNPIs are active from the start + mio::osecirts::Simulation> sim(model, 0.0); + sim.advance(3.0); + mio::ContactMatrixGroup const& contact_matrix = + sim.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + + // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + npis.set_implementation_delay(mio::SimulationTime(2.0)); + model.parameters.get>() = npis; + mio::osecirts::Simulation> sim_2(model, 1.0); + sim_2.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_2 = + sim_2.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + + // third simulation; NPIs are implemented at t0 + delay = 11.0 + npis.set_implementation_delay(mio::SimulationTime(10.0)); + model.parameters.get>() = npis; + mio::osecirts::Simulation> sim_3_secirts(model, 1.0); + sim_3_secirts.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_3_secirts = + sim_3_secirts.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_3_secirts.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3_secirts.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); +} + +TEST(DynamicNPIs, osecirts_implementation_with_directives) +{ + mio::osecirts::Model model(1); + model.populations[{mio::AgeGroup(0), mio::osecirts::InfectionState::InfectedSymptomsNaive}] = 10; + model.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecirts::InfectionState::SusceptibleNaive}, + 100); + + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + + mio::ContactMatrixGroup& cm = model.parameters.get>(); + cm[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 1.0)); + + mio::DynamicNPIs npis; + npis.set_threshold(0.05 * 50'000, {mio::DampingSampling{0.5, + mio::DampingLevel(0), + mio::DampingType(0), + mio::SimulationTime(0), + {0}, + Eigen::VectorXd::Ones(1)}}); + npis.set_duration(mio::SimulationTime(5.0)); + npis.set_base_value(50'000); + model.parameters.get>() = npis; + + // directive begin is after the simulation, so no NPI is implemented + npis.set_directive_begin(mio::SimulationTime(5.0)); + model.parameters.get>() = npis; + mio::osecirts::Simulation> sim(model, 0.0); + sim.advance(3.0); + mio::ContactMatrixGroup const& contact_matrix = + sim.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + + // directive begin is satisfied (t0=0, so delay is not enforced) + npis.set_implementation_delay(mio::SimulationTime(2.0)); // not used as t0=0 + npis.set_directive_begin(mio::SimulationTime(0.0)); + model.parameters.get>() = npis; + mio::osecirts::Simulation> sim_2(model, 0.0); + sim_2.advance(3.0); + mio::ContactMatrixGroup const& contact_matrix_sim_2 = + sim_2.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted after duration + + // directive begin is satisfied but directive end ends the NPI earlier + npis.set_directive_end(mio::SimulationTime(3.0)); + model.parameters.get>() = npis; + mio::osecirts::Simulation> sim_3(model, 0.0); + sim_3.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_3 = + sim_3.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + + // directive begin is satisfied (now with delay>0 as t0=1) + npis.set_implementation_delay(mio::SimulationTime(2.0)); + npis.set_directive_begin(mio::SimulationTime(0.0)); + npis.set_directive_end(mio::SimulationTime(1000000.)); + model.parameters.get>() = npis; + mio::osecirts::Simulation> sim_4(model, 1.0); + sim_4.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_4 = + sim_4.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(7.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 1.0); // lifted after duration + + // directive end ends the NPI earlier (now with delay>0 as t0=1) + npis.set_directive_end(mio::SimulationTime(4.0)); + model.parameters.get>() = npis; + mio::osecirts::Simulation> sim_5(model, 1.0); + sim_5.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_5 = + sim_5.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // starts lifting then + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); +} diff --git a/cpp/tests/test_odesecirvvs.cpp b/cpp/tests/test_odesecirvvs.cpp index b1fb9e6ee1..a8f67f7864 100755 --- a/cpp/tests/test_odesecirvvs.cpp +++ b/cpp/tests/test_odesecirvvs.cpp @@ -292,10 +292,8 @@ void set_contact_parameters(mio::osecirvvs::Model::ParameterSet& paramet npis.set_threshold(10.0, {mio::DampingSampling(npi_value, mio::DampingLevel(0), mio::DampingType(0), mio::SimulationTime(0), {0}, npi_groups)}); npis.set_base_value(100'000); - npis.set_implementation_delay(mio::SimulationTime(3.0)); - npis.set_duration(mio::SimulationTime(14.0)); - // npis.set_directive_end(mio::SimulationTime(10.0)); // --> can probably be removed??? npis.set_implementation_delay(mio::SimulationTime(7.0)); + npis.set_duration(mio::SimulationTime(14.0)); } void set_covid_parameters(mio::osecirvvs::Model::ParameterSet& params, bool set_invalid_initial_value) @@ -1471,9 +1469,6 @@ TEST(TestOdeSECIRVVS, check_constraints_parameters) model.parameters.set>(1); EXPECT_EQ(model.parameters.check_constraints(), 0); - model.parameters.set>(1); - ASSERT_EQ(model.parameters.check_constraints(), 1); - mio::set_log_level(mio::LogLevel::warn); } From b91914610e90fe9a56d10d2e4c2362c538742276 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:20:00 +0200 Subject: [PATCH 11/19] new h5 file for secirvvs stability test --- cpp/tests/data/results_osecirvvs.h5 | Bin 33312 -> 33312 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/cpp/tests/data/results_osecirvvs.h5 b/cpp/tests/data/results_osecirvvs.h5 index 0e3e79575e3f4aa17ab577b9088f06507e988954..25780abf67c9f06f259bfd98f9aba1ea1316e938 100644 GIT binary patch literal 33312 zcmeF(1yoeu|1W&HrMp`|NjiHU#^rx&{HhQJuEc-m`}pU_{`c;Ie;>bpk5g~L`1!kahUWKf{<(hy zf8WMxFQM)uR0Sd;!~53PEwJr>kMIB1csg@Z|L~tNOc;Jdf`oDV_s{?PIR5*6{8RtS z9v~*7_^Zze9Y;(!q7f7S^@IRn_rEJCw*Ai*CP5(dJt9)1B~sib^glWF?4Rut`+Gb~Q2xjJ{r&k!Vf~Ny`=7kk<*$E#2?*~4{Z*+5m61?c36+yj zc?ngJP=yIqlu*S9Rf15Z2vwR;WeHWDQ1=q*K0;L>R7FDFPpC?Ss!XUVgnEEbRS8v% zP}K=lgHSaImFS<_KRxhI5B$>u|Mb8=J@9{J5B%5Xm4BVF5dU>rNc8vlBoCo?{{8X) zhx1IXzg|*;`#wt^a3ey%ZWerpZ@sk%7v78 zyf+h z$gQ+@7gsJGjV;xz@Colma*-42W7ZPL;MKXxMD^WBHXuM1HaSsF7hNB59v7l=C5_-e z&5hL7 zXTf`Z7b>*y!ajrID=lJe>)Nhsv_dL};y=EQ4}#*9;0O0a!ob{)g7wz3H^7ab{d`scjLL2^|2>Bp=Ag9vJd#NB4a!YCoP*g;@Y7!B^)K)4?(dU=Fg zP`8o=<>)XgA9()*vK4Eoy(`vX+f;7g2G0f%htDNV)Qh6v0AF#-JQ1Yy!$eHrjtH`` zUn)IZD}oe6pPNzl3!{&`G$%C_MG&)={j=Q8S;%X(P}gN$hsvPadN_pH2zGSCGs?SznggZA-kEqM&x+1CY*(MS%8UwJuhO3vV?q(U zdF*mbjL6Df^XJr$c@QdRY>wfa0d4E58_xs3LIc-Z?#41PWZ=D-n@@q{eyxj-1ef9N_zT)qw|TfcdFAm; z(N75fkR=dyi3qhfw4S)~m;{B#8lR|6eSqk@wd{_Pr-Hb+tow(xbVx6vnIA%12gZXXpSD$k5UD%DtmzS1M_Q#e-jW_t8?U1#w%Jso

w@j;b-(PRx!gsG5hnMS8)DCeu*uOG;uZ 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 literal 33312 zcmeF(2T&B-8ZUZs&N*k098^R^`4(URMG+(!5iubs3<45VKoBq?A_#(jAPNFXMxx}L zbIv(u0ZFetd-kh)_btw?y06}QH=MDGZ+g0!nqIZ&S!@0J?=#J#$7!j!s0jW%C@2U> z2pInL^zR=#_=mugzh3!!`|tf%@lOcx)f-=9i2iy)Nbu($0Rad8bw>RA|MNI~y<^7+ zup|F||Gz8#*9iJa|9VF8_Y422|3|fe<}tOie@@sKf7XBZ(sF!1`@1pZ@lXElkD7n) z_#dph^~Q^oxb_q8-Jdk zu(7hUR>0T2__~jPK;O#N)DqkOcmMwH^`~=ZbdLV%!}#t;Ab{_;fB*hJ`|;nO<3IJk zZUI69vcK9K-*ANZZ!|)}zh2uK_@Rb8!dGJ*L zUxn~h1YgDQRUBU>@l_gMW${%WUls6mFTU=>*Zugah_46mRS91Y;;S;g9>P}@d_9b> zNAOh@UkU#6_)iP`rv?7g0{>}&|FppWkuC7wjw}Bfu@L?>EF}1QJjso3oqulsKNx3n z{`HpPT>oXm|F`4J+yCY6|JR@Y|JNQd*zD@h<3H{3rxl0^>relw|7nlEW-i2pYq$Pq zY>&S_(|>;crv?7g0{_dlfV>U**JsbiSAu`e%jN#sPr!z+f6xE9@Y{dS{{`{eLiqak z`T*ZQjuXaj|GiEiir-$tuRoCBw+Zm|?{x?o{5F0)0$Kfi9Kk;yQgi}0RrzgDDR1w% zbg~T!NmCttK_!k@t9m*T_e-F`oP$E4ZhO$-Crs(p2RPAK&P7`KDNgjuYMjfr( z$dRySFroU3(k)!MOo)2F#XGGwX5{4VNxKm(fI`0oT6|>^M78N=#?DECD0uQ#Xau_` z5>Zkkd}Abvo*I5qZz2*yZ|y7x)~1)hgzmnE)J+0pbk@{uOz$^5;hWe=m?J?h9GAlR zTq%(G&DW`9lhnwLy^+|q${HOSsEQ4a@rN%m!T0Y92Lan8CCjZBAz*ds#}pr(4YDVB zlSvt6gHE1jyr%F~80`#g@`UaaLB?9i22%t=h=Mp%Vl$ruRSIzt+g{~B^*V1(Jj`K7 z)Jg3Dwxgto_*G_ZRx}Ax^K8m2=_N%|hv$o>C52H*o|8sSqY$d3CCI!pErc!(J63XS ziy-^&1e+dyBFN^wrRi5HQM3`@!#fc=4?4G%hTe|-h8JZDaPIpGNMBkpomL}4_sjic zV=PJ0Ng1~+S|bWnmXrRAwF`$_D5k_>3_n4))tS5-AHM)y)P>fN@~@C27RuYRg+t1b z!7L%~Y*6v5FH%Fg0!TQhfyw2L0J42OFJ{usjn);u*%oK9A!;0%Z*2f8ax<8orjcMl zT%Ps^l*8A7uV{k8yZ9Fb*Q$44<6eVDj|j9!SVWP6^N%Q{Sz(mvdUE8lvoM-GMpAgR zLKrPdN8hCE7D9?50%wlw7e+K{=u}+m6tE~hCo^XG1&kmDXB57@=BzL;vnwI!+INp<_#G?D#U@iDvcX`R{~@+=%0@kNQU6;*1V-z9D1%+ zJ#_rJ4KlTe4-LG*jY6zqWr8=jkbGEob8I;)a{YDF%wLWLNw*y~efXLQE$4SaO^@;veDuAR zs8HmMunnST1JKH=@;K{j7c4mnY-^wFfnmbRl8JsXBr-okS)?I?nD;F&|7;UNUKFe} z)ye{BeI@mCg$plw&O++UJIRY~@tFFxR1QIX+bGeQvKeS;EQ6#v^tbsuE zW8TVFn_yfF#ll?AT#dSq|*=3))`PTjm?6TseT(ate+-7 zO_OVLv_T2(b=zNKm{8!kefd$fj7VPn)dl}(Dir9cUi6WU4n2LCQ1wij3O&4+wTCE( z6j?2kwGX&AL)d+W^CXXJ;fZ(GP>@JH=ue#u*f%VOTsJN$Nu&v*(~8-f?a2y_pW#*c+5+{FgCvUpOgZ>R31H zRUkyKE8pZ?Cn$&O!^)!awfUfSQJYe|ssPfiM7ck~iJ=}QO~ymmbz|hQvqVyX04ne5 zr4P{OL^KgK?I(EHP)p}e9a1wk^hRbv=XQ8El%*bs@8cbY$sjaQnbr>#(KbS%=NDlQ zPe%Un>=g)EwkobMUxUk!U(>Uc+aZc0Z7h#_GvLnY`0Euax$uTF_^6~`K3KkFxg%bK zLn;AhP0!r3LA?d>XCK5+qA+WM19mOs=$#=|!geMBA~DNQ@GvAr=JoX(EOmtF)-5^X zc%uz?SzLG0pCt#zagY2JJd@$c1sZG7&=hzRc$EC{XHoP>*nqYpT@Weh%&5fnQ)HcUJJcI`TF1WVhcXw{@V(VJ z(C1kEaBguCf-K@?tzZRmAD&tEH?&7R>a0>eZCQ}rB=_KeUmhGjC3Bm2umEI_3_P?d z!yz6YwRT@~8}u{dl*MaDQZ$c_Mx06^M$XlS(|KLLVd~Mc1{x+r2OryA{$RKT<*eg8 z#ic7SscczM=SCjq2Y>Ed})od=P0 zDHDfXVMc`w8M42$8Bn#T`SOFu42bisC=oAd3#dCM7*rnWg8Tf52Oe&=LeJYO&tq*< z;QUZ^O4xlKu*;tl*`r0+tRU;#+hUIbKc(y2zRm$luhi-3ntXU|VNi5}tO!0U>)P@Z z;n2Vb?SPFNHb|pEElH|?5b>X8b5G?bK+!n^!|$C}AUiZkMef%YoY{7G{2lu{E?n>T z>@Zt^>*}qmSxTRPe*Wl(dbfA*Sj)fYXLBGNf91{?cuN>@^2E>0Ug1L`dDK?r?>P~} z=e*ARJ_h7Tni!TiL5=iu=FJU`&>*T2)?-QBjR0gl>P>fAK@SNwsIfNz{YEm`Z`E<2 zd1CElQ!)*T;bZ`3~KPD`w8dJ<K72vJj$HZ1 zi+wKAqLBVd|3Bg%7ff1BGeKO3L$WyHa~x$7=B%fWl_$> zp=;0f=yRFcAbRs#xZdtnptXKZx1+ZLl3c=NOSma`_u2Qm$fsqv;UVmAcwipLr=%}M zi;lu&cZ=eM)6XIFJJN|Yb_UMTZ|n}s58-oXq90?401`Q0rLUsMiQZ?JRk{YVpo_#z zm)zYck$$c$H_L4jLH4dt0CiLRp=HXYbWQz3pS#)8>^x@O5wC(6vDz zlG*>6qG1GDRh3n&^~XUwa8YfC&;c3!qUgB{`9LlEWZz5SB6!5`i<&R81h)6OQZ;Ac zkX6i%6rYI=st}Qg<@~t>SMT-7Xg*m0Z_it^vkIfoMM_2M%d`OE(OWt`o|7Qy!0uGM;c~h-Ya6OoKAvXhu zqL+_NUAt_9s9TbX1@6v+<1?BMxS454@N8kbz%~d6-(Q!lKQ;}c_ijm-=8eEBl6bvY z_iixAt*#Cruz~TPYHITzt^!TGz8g=VDKz!dMN}#BpvK62R`Po+NSKnarxZiv=gs3>hJ|28|1Ml`unUfx zYoMH zJP12LHpgSsyQ~YM-J_!}kvgK5biW|Gz5?J^L(fwfNL{ zqLUeP&DxN0%j|k5900Qw-Xmi^N>CzPF_uWeig?NHo_ak^gQA;?gAS>aq1!$?kK%;Z zL8m`_Df7i5d_LaxiLh`H%(SeJm|JJVHCHW*B)uOn<1t~fav=>Q*rHCpGH(PfpLTJd zpDpm&HI?>?a68-)6`(HL=ZNxf2AK-th0t4yCgx)soK zxoD8L2Lb7^3u-q!#o@uHdv58g5^(g%=*#UHCM0*vE}S!x5~*^bH|hVEt}Zft@2Zt_zBL`Xvd6Y9n3? zJTbz?Q9WbsC---QfQ`9O6V@)hH|5<12daVUyViU@LmLS7u9B)qHh_Kk!~ST(65zb3 z+g_5j586Yf?t4&hg9DxK>5y<9`0~V<_B$aX3KWn3E|!46o;%+>3d%vSaua?e;t=`hvE*z+ z8x%Ug(!0Rh4%{{T^Ts~SaPvmLYq?W7%rFEq(>`f}j-2ed?1^fi`)wE@`Mdz!&K`H- zX59m4d?hD!s%gPm?vx(oD|*OMDn5AqF&zqXdwlUDF$rq(N+-2l*n&eH&N4E$=Yde$ zNiu|W4Bntu`8?;x!0AlB_SoxW*t)#E_2o`1xR|UH+cSIxaq3$>D|+QnLf}@M{<<34 z=v>V?ztn;1%>Xrra7PsN%rHNvwh-#urMo*NOM&s!ry?GL3h=WKG#E+1p}6X+!L-+r$|{ zZn*gMmJe7GKoL**J0CMbVAHZ* zN*jX4mk~9qvqNyxG|bxQTq2~heJ5rd{0uEC*ir}SXQ*_O8F(XC0#2$%)CLXZpwhWV zqVZuBtn$#i^k+Gulhq;z{nrXXjM0~9_F^ftS2JZe9jJh5QY{J@?7Y;9H}h-_Y*55A zzX`(n2AG}GTn^e(3qi}38 zL$C34TLj4f)Ui8KRp!J4iEAl$if|+h>aOLuL`8r~m)Ik2+d|-eB<-I1x&(|-t_49~ z8C2ZTAscLTMB4Anev|SRfrFn$@EgxkuskU&kbI*8?v_RKzW9!<1012Uo7J~LYiGAA zI`V5FfW*sxYPkY-D)>wkzy1WK$@WKcd&=QZwA8ne>;mld`XlBdY0&j??17;(5o~l2 z#_wAmQhod~NH#%WL{&F7Msu-{6h+vW|M;c90U}>7zRz4-f(hm5hJ5o8nAe~z>doo~ zv$C|d9=aa5V?nfc@XS|8p{!{wWDkS3OsOB+pF)8nQtcNvb3QQD=!zC(6@nnW(d6T- zVtCd~KRY(&i1JBl^NJ4_K`(AW{X<+Sw5*-TN%pCLsupgNfp0jZc=m;fDRy3=W@AOd z=2b9r)cW03#xhvUQ!(yV%!Z>jkE@KTi@_+eM3U4Qn~$2!=4WyxLr52ElUewRs_~QP zn9tiSs-Fbx{JXW^35JjB&edastJr)RHjnEjdaD_g05_Vd+*q|IR2>*? zpLwd5t6my$dJ-B}q1sHvC0BWZ5Y7GgSu4-I0{a53qu&SuYE+jJ-wJU7iHmk-|5GN%c$et>|8*cuUSJlvD-b&3e;Q5ERB z+M|`3rTXPKcleJ}IjVXO^ITt*ZNsQz{qYg(`*(BG>w1pkB%G%YcPU=zf(Jqe6C(Q? zfRZif{3uH!Y!qsLv^0o-lM$!rAHW;%W!%T67xM-@BQ;g!)YCvZm%=l`I|Bl5-EeRh z%m$ipAMr~ZPN<3^D%$mF5uEbJDQ)kR!Xgb(F3ofWur_blP+{k_CHR}ROAotle6%~s z7*q@gS>(H(D-}RliEgax$yAWE>3uP_kOLnaqCW>IrvW8ns_+2)SCGo>SKvC^peh!Z z;=5-1P4#+{{tqAd1l8kw8S%m98}Q`EUe&hK^ROYYQa+B2&o*2SJ60TS2c^L7L_5z~ zn3R3ko!nOo9Hm{Heg0vPAaJDTUGht~bj;8vyXY0H^%p!|dYTNDX4o>y=``SKW42a& z^#gS3uDYa(IH7}~O6MvAi@-qQV>3N#8N~U>)EE<2!nX^DntbB0?=ST^H(Tty!~)gn zPP{9G!3N8FlN5PyI;c_c*QsQ%x=lK0@;eg_`orbz)RTxMvz(-+vJS1~xHR;(KA zv%t@N_@gT8mjU?z`3O~c8teCqch&%xGtT?;`!t-lp_Ol!9){L8Y!nIDeBbzIu=fM{ zN{BYn;g7yp36DB%UH01yhT4NpDoMj$pmNpH`wRCoSk=%yCnS;xJ>+DjGmny?c*S+$ zE-n>>Ue&h0lygF%Uowg_ql2= zg}|=M5Xo_%2uzg@RTxkPfcD-B!??ivc){bg>*pr zEaoZc2ea5Jx+KuzhfMS4R7pgeZkatqEsm%R2jV9ZIFYoZ%EbQDT3v;62eU>l(I8k~z@!}sRnl_9lOvIdM zw$uM#PV|x~`(IAe$q@YiwCWWo9FE!seE<+@Y9N$WR_y}A`(zmDV_Zvl*F<<3tHDM0tOl=rCQ9a6lst?Fie-{&Au!YLNfOiFC(V+x|F_+2QYF)|EK)oAI;; zouv)ZO0&A4_(lX-0n=b4<~u$OxUdn&j@H{IEk7;o?nF;M zMV=3d7DZyxI$5_3MbI;DsU?Z9-JR(E8!c(y_9fO{c)nHY{tny*TXmV zZU%p@hm)czqMD(YOGO%G(7X{u6@=c-Gu)i0pdr?>e~=yVaTQNKvtdOZy*EYo=QHl= zM0dM!HbFP{AbzH=MS-HCDBpCE=2QIcPQ?E{JbJ}?4Ek7Ag%u#Rg z70ijg9V2M{<3xid5&eIhXkycK?qbw^)G!<1q=h*Vqx0aWKTf2PW4QgtiEc|}u-p0J zP=d76sqiB<$jJ4^T@gzGG-K5|*?EHxWpG_neG%Bit z6vh0zY{3PGCT%iSj>y^|o#9t?Qs;Qkr6m)=Bdwh1zPIDq@&*PZqC>{$y~2!g??jC3 z>1IIMr;qh(eDukf6J<3T<^ORa zJ!&znSj>sekpJGm2T4H~V&# z9GyCJgZ-!(JxZ%>I;U`r64iepJReI$h<0-#=_o7v(~1)4eVygo?;Ij1LOr$U)J1_^ zov2&dRa`u`9m-$y2|B*+hd~7@yV$HQpb4!^IlVRqmp5HXu3}EKT`g_vggKE=e79e- zwLN;R+jAfab0RBk8ct=*i5Rx1-AOPfI&^O{I93mb23vX`_)*xPTjcZdqIFD&;qgvv ztsev0917VAYa>SWaz#k+T7KN&R?@$&oG8f0C{_z|q5+38It@)6Vj|Z{9oxa7Z(@TD zANMdI754^3do>!=Mj3ty7f*oZ`5I^pfC922Icp(t?ueXRy%u}Da-dD z?>Ej&A8CaUdm`(|{wD5So#<%Z@h84+*gD~CvD9PxR+#MIWNQ{|0`Ivc(QGsh3fU*> zzFT7xB1?EK6%3&JU1s_rdyV%!w*X$`lnbC%RA2tN#{rq9-(mOLXb zw&KY@0ydj$0|fLFfUNY0VlgL@qtmfmoqd2fKTNm0#GI%`c;t*M=0v$Jsk$YY6Y1T5 zekNHNhjfQkyNi}^XqNr*7b`JxSB# zLse-{E1`7H$MvEcgCMH@^T1`yi9S_7aLd7*DBEzaOn{*SisK_M3dNjAndD5&Tg-_L zHjKR$!JMd`?6~L0eK=$vTDd>6U!vyQ( zI}q_K_T1N(t8l0G;Td~^>0O6TRe&cOA?w z2i`Xq3V4+(V3-AG9LZM-=k~52mm%wiHTBWfXv~Q?C!Od%V@`DT!YB8~DGsPI*TZA~ z&wO|pWx5lMIZ@JF*w~#vP86&it0s#>T_V5kD^KFk7kdwO%M}7tx^w7u71I_xj{iVd_Z~g{0CS>>S10O@VNNuW`%1eVbD|-ALwz?194f9po2WjDLmM7#B&^C?P)}K1 z9Jjm%gr3Fc_m543UXTyFW8*KlKr+=A%enw8Jh87JNaTb9OQ8>O@J~B(k&Edf5%(VMPy>0uakA4mmUM6OuHVF7MTC2fv;2KAD1UXyfdu z-n-TZc4g*~zt1|N-%nE-pS&yp7Oug`Da?rmq-PGQV@~8LGBTnqibJ-pzwGh{ap*)~ zLbJs2br87x@a0kW6*x#B%0j=6U5})a&&`D`foxqW$;P1>*ne2*oh$9Yu1;jl`CC93 zyRRmyPP5Zzl@-0_xm2|-L$|9Fb&m4Jb5It*3(75>^`|+I6(ln7v^^7w&r3>b%C&&- zefh0qq7EoN%6Rijc^5D&dMxU{bwtTB%;d8D1)ySg=u0`~M5n~62?a4HYW^Bf@Kz9q zF5GEfjq1b3zpt)qugtE%HdVt~X6Pauev`vekT3$#uPBD4C+30Z%X`&S_6dmb7*{x` z+Ow+@-5RB{A8O}CBEcseqK25!Wg$v&mS0r6IuY)yURA~6pWx&CMab>r53u{_ZtfYA z20s|rHYMyDLEod}L`Pl=Ox{6t??u|7JHz$1d#xjqDiz#ek|>1lzxdONFegg1F$xdC zoM>>fS@i@T4vCedk_~s`5OV3=%41)GcR^L&XC&ugSuv(ubrS2(I$Q6ERc65Zh3kIu zuo18?l~!+6?AXxVV|Xc<4x9j7z{F@93O9l3yRz4sed;^K=y>4Ijt>cWm260t^uT%!6D?9?Hn{6V zM~rrJqD|koB|4Z->OR<;nMM=`>{s|6-23(w#x4qk9Iz^f=%24y>-?(Wwbj~F;jeY@ z?tOzX(i1oE!RYAc3z0OOa$1Lkr=@SI-r;swn8N-2Kq(G02I)rr)s7|#FBVnLLt z&*H<_s8HHmh@$)g0ou)phA-AGurj28$4;E5s{I$(dZ}2H6aE>JzPng&XqUjU-!0s; z=5nB%$}-z=sRG`ih4b2OPU!nh?SAFog)rpZd7Af9DMY@MQ;Wu&=vBrfwK)q8`A;w; zerd*`husX(HIAbY%t_eTGdc(-D@q7WLRz8rokONdVL$KzC*7lvcDQcyx-OEpW>+VA z|EW#7zJUoH?)!f05jG#FUmBhh=HA}biHy!g?T@8Mg7kLWN1BvKI3r9kSFREXQlDP7 z-u5emZ~4YLTYe>Qw?yc%)?gU~y!a|@5$S~DmZf6{`HGX^;F&zB*m2vK}V)cT0$7s--z~Tcx*Mn`MK`)qsBdOO3EvO#j^$G zRgRd6zpvQUi8%Xnj|R{&BL9q=II(FG^b>d8?QPEQU7e_pE4eGr{yTOZ6YVgN3I}zX zAgz^(Fj(ZU>Y?154<{`W=GlH0g51tdM0aj6Os?PG?9X#TCEp%)cc>PDdTM558|Fl& zheLOoq<8B1FS zXg4>El`b~I0sjTEnwZjEohUFkC{?4M4waE;EgnoGLPmE+>oRgycXgs5%a84iYH{%3 zFj@FAVKB`0iGMmr9t=C>p^SPc2XKuPxSA9B;F#SxG)Ytl2l8xwPIfq<;*J}yvn~{Y z^)pL*s_IfOjzB9fFee(Ad2}a=4u_H?PHMK);ZQ(sro(D;KeU}@6_r-*2KB4HYKQhV z0Jo9x_?T=PSPI*cuaGrB?!i^J)rZBqIuZN+=iy<~)abOP%^bq+UsPNFE$z>;va1uN zSpSGpyBPzdPel=bRse8ho)z$-cn9hln&L8_GoWpcRBX|S9FP|?jkHa?FI}8> zLKYeEnbh}-pp0rCGtqb{%(j$dI%7_B@#2TvCK?=CNYrN0t;HelKC2C)eSJWFXY8Dl za3?qg$9RX6)I*)6tidq`%&ETkf9NBqgTU19$>Jr2yE@SrwdS)GM2S9>>uV)XZo%^9 zQFY_o#a*2!bLSd!LC0qZzMZV>Bkc#)!~A`+-~7N)MO}B=H4UuIK31H1odFj1XKvgP z&jzViVb@(qozajtU7fl&<|+0HJlX`<{Z&`APZ?uQ^nPwNOr9EtE*+2`U#!OFU;UGB zLJGUVw|=&Yg0&s2saU*9V{71(PMb#)YJ$Db8$$10tA;$g`QFc31-m+tbFuE*@OCnk zewmGSr2IEL$`D|^@*VS{U7Y9$6}Q5Zx1V6ot3<+aoDUQ=$jc3;zJ}nMQ+=Yv$-of$ ztL5#vG{CJp9boqV0rE#Zd%M}4QCSliMQLymOry3&Gt7yy*Y~8vVov1wJFF{>5{Fo? zP~1ze!u(+-t8VyqCu~%o&FJE4h3Q+LXZByLf-4_R@ZSH_0HZ1$PWj}O;B!c)Kx{B? zS0`fimDv;ejRaYUn(V6}Tmun%5rfd&nO&Xe#3{MC+7}Tpx^_?V_OoX|r@OCgap^f! zv3o1pm?pv%k{)(bk7SsMlFT~jkP3q4c1qgf&M1Yu^YUzL5nKu$wMoUCNUmUrb`o=< z!_~1zOek=uTeV9l2wV3Zqw%73l5K|z_7GU6(hQy1V&WAd6~N`>+uA5m54rEV7;rsh zP&nxBPGXw7s}ot9=&%%G>$BhATorM8j@^%YMq_RM<>anTH1{%5CF(&ql-E~ROLORL+fX>|;e8J#87BJ&=<( z_r3;Bt8i$}ca}iWmSKYudG@YOG%9`K?4eZxwDWz0(eWawL{7wf{Yd5=%!$^OIXVshjT0$9`EAlqAc$_gKUsb6h9FX(2Dc)VI8H=Q zZqiSJAkgK6lm!JETGEm>ZlOlyo_ja%rC6gZ^Vbq~>fhiBQtF~$PXgI@bE@8MDM0kS zxi-y!JpxD{1?Pk3SM{fK)&MiTNUOr%2)I3RwTFzdxS>8{9iWwdf zR%7?8rEv0;a9_*@)b%!sWIGS~I&)s0J6s5dM~=ND$F3JYBF=24Ti76zg|QOW5E0~H z%E;OHOBn5y)xTXI!iPHMa!+Jm=R}KvtNZ;E*wJ8o%3Ioh<3x1IX6eto#1O-V^z06g zC~DsK=+3FrB1nHlw4Yp01U-n+!dSG$DJSf;rLAJoljzS`@KM zp2+WqjQhcxa+`pQT|WE@b3Uu<_nn;%+oE;n z9rgHw)o{}!11IyM9!?R^SxeCY_`sc z-nuG!$Tu;dDC1XLzGN(@?yko8?Fj}1xnZe8ITYwFK2P-FA9ES_CUYJ~OX z(0jN;EkM~x>zaBO%W>#1g)?J0%Tu2(Pm`8%ptBarqa`QV&_iPO8(zcI=y$;#dFFOT zq!IY^!2>H=MD>LRXZV-|`QYQwwwO4P4#q+gGe z5Ix7|i6;J$C)(D%+;KHq5K zb;NO9*&coKQhBI=t>=_QMG0{2ZH3E{Pd@rfbpX4|Xzly6I5f_iK-WoTgZhtqCHx>} zMxFET_s0KZKt@D6HsfQ&XlGvxOC$EZk(d=}R3ku!WbW=v&suN72tH3#{f|76Q;6ki zoTMZ=U{L5TbVdZ(nI&wUUE)UtuZKLhSy@obivJzXUIrvy7Qb&VmIow1e|10Z$sn|) zjs7C~F$iMSQSStD`$5IV_~gK=DIkiPCmdFq2PeKG{7dqSFcaOj8BuDFF4-&mXyR!G zn{9Tr520;K88aLiF4r_Sbp@&WbkC!MFv!O>VVm+AQ~jVM;qLe zM}Y2pZqxf{M}cU%T8NHF5hIi1cWC^ESHTRQCt|?oi5}teMC{&G+#mHMkXOF%`m};D zY8>8TZ@lMmAvJdw2GmBcv!) z$w$S>ngFp-4QS63FM|O-PjvPld7?dUC@y5GizDry`?6QI1d)GQMc}?sE`+nMW6E)% zLw+l#`M&2!LT&CbO<$1LOIS&v`ApzM_mR4vSxfU2b(*blloq;tMdO$R1%9TI~hdQHP zBzr92&{{~k)QAT;y3wYvzNC-kk&`V6QtVbiEorWM`7{yY_4kXtzQOnyIJ{S8 zoDy|FUePZNj}ElLYJ=#YYfmRcOO7?p-|Pjis|3XP@;I~}k~4jJ8grtewAZ)zNRZ+L zQTZ($Li9nSIWAyq2?E?y2uf^rAScqrcn8Z-jdsCuk>oUR;`2mr|B)wp{@TGcKS2~3 zzI*rZ>;xZ@D_^`rFUE!hVqN=G=*f|*MG@yg9wH>*p}8`1k_cs_8lBEd?u2Z1XDTD6 zW^l>0QhY?$1e-2n>erY0z-|4OzFRovV7IN-Cd-E*P{oIh^Qi;UVxPJ~>fZ)g*76SO z-@CwZ^Bf=D**=igJYBP23iG@wCerU?SiUVMipuyDA&RzrR{0`w3zDvd1c{R_fI>|~ zjwQhcyzx4sa@cSgWkEcd`Z4!}P%erqWYg zAb#(|(YpQpFvB6MIL9~$QZyu5;hhes^Tm>vlzcmc-xF7uGwTMn+cM`a(e?v_gjo1V zF&wht7vJlT<(SEJuE@A`Z$Z!OqVU~E>rhgEf7SiW40v5xew(Pc3iq2^@>We2pn+P^ zlR#k@9PxRg4t$=-6rU%GoQu_K?-WAl;}zcaIZmWoU1Aur#E9b34$rOLBt-NFqU|Id zegj*hshkkD{-*odhTw2kD`<}>u-$rI1D5Rmp=|xtU?@NMW&3tJ+m`%)Lo)Y%moOS{8l+II} z41xpe5crkYj@2LQ$B}qc=i!CsF{uij>tXg zrx&Sw2h4g$`uV@;0Uf$$voy*5P{PGQ$B22#+V8B2bj(vex7>b-bNdCO*He^CIhKHB z(WCqc$0&#iwzvmqEWk?hyiaA=1bpA|G;NCN0YiMAC=Z_}x`xjaeSKe(_?b}vRo=9X zzIlrkwHbKuELYK>v~9h&=T))w(Y`J>SA!++m}gU_^I8IKI&Dtd(@j9UJ~v8HTLS9Q zr71MiCD{G#tDpIj8-aiQ&gSv+E%1ie%epzQ4RV+th`jykh;lwoyn7hb0m^JFt7^o( z(AQSe{b{)$hA&JQM{;5Fm?Ju}9UV9%s)F4m8?gkbgRnY={r?Id<_z2CbcaBOJ*F;d zbOsi0zS|?pHwvB3z4squ>-HV^JP{Q>Ph^kJ6B%kM{d98WMMj2(&xWOd_7Bzs(({mT-3JSrn^~hB*EXVL|V*PpQ z)p{7$=Q@1DuMt|BLl;&)H-l1eW!me0N7ODcu(P_<0RfuEr%fz+Ved7ca&OrI2st9r z5x|b+D>En@PPbw6%S9!`BfS7LGMlvCL^H6Or+=##JAd5+_LnPHC*bE>MK=wJAuuy3 z&ZmM_V8-W(dhvN8dwiaVv9W+xl7Jg6HnPcnuwz8QH0{T4ERZ1%vpC`5vLz@VE)=1{ zzGtljV%RP{ora2sOJ1Y~_3(3B=Hy9{d|)JFS)_^22XpG9CE@ASFr+x_nz*MPdMJ5l zT9z6hKP}>U0;3ZmyESeqbhH!p>F})0m-K>Z`o)0Ws{^3tF{IFsxx-^}mAzCgI26&6 zOW@5r3o2a01p(J5ppa!%xVEbY`1u6P?LtSf`}@QwzDD=MkvX$EZWQB_rwagdfB*A?5T zf!+^StvRAGG>PSt9yt%63c~Wy!iBcUDa@uykIO2rL$RCK^ zd)^7het8#Wu=K&pNmAj+mjiH?ThIIf<_;5E`E=x%J7i9KO^?}6!Zqd(43wIqU}tx) zw(VjkTw&#Ucp`5IB7G_IeqiI9mu5Buc02X3jL#F9;`2ns_&iaV%7?Uv_t_AO3iEAJ z?7p2>l(zn`p9o!DKVWdZegTq*=;bf^o+q{CTuJqKHhB9O&B z`*29T1QafFFm}H$1HYB9reH5D?{M_>>*L#*3=B!%iOPU6t)-3M+9n2jH zOn!AmVea63`oTNGSHtj8HoXEg27oF|{-J+dGnlF;N1o8{h5hR+QyC0%>1PqQCBfE*he35$QlRwyo~;tb0?>;t(Vu@<2#NfZBc4x- zA;Gap;O7@7w69UNq(`O;KA-Dwi7W2|E{&v!XY&J)6zP?D4ReQkgqLC#YjCJB=CXB6 z;UN4-cqeh_VINrSf6`L^yAhV3eI-`I#{b`id*42bYy#W*7IuKy0s`9b0((}}QVs;Wvv{u7K=t3AfCycC`3mt@)Y0>~ZsZEsNMgmR50 zNYahE;M)Ug`=2X)&?cF91JcwgIsZND?UJH~+VeW7xBsKlJ?*LHlv00qD-vcdM z`%V!JG=R3H$7BHmHV-hDA2Lm7fPFb?QC3)v?mRwE^za{fqFc*uN;nHTIwf>s=<6FY%{#D6i^tLWRrBs$LJffNeS1#(}OMI^2#{ zo#r2eou$_X#@PCNS;ghce%N~3hZCPIH__+-TPz#IZy#?)Mc%NEJJ!94b zQ=6W7I~9$vD*9xR9qSLrNL#w!zbODY5e>rt@@QBur%4v={R%-n?+E*j{{V)jfW_#u z84#Z0cPDEz3#1Gmn8srM^?#Lh=FwEO@BcRyA(@9{%#=)-L+84vB!pxLk+EoyqKQJL zI7d=wR;GGPnG-S(MN~4S$Z#^xWJtt!|JKv<&*yjkx!2xnpR?9pd$_OreZAkWlkg?! z;p~$xICAlgd9X+y7*G$=YbsNLpX2Dq_>IK8CE5`sO6c?P-gmv(#fke}r1xl<_E#u5 zTxxvb&qpvB)3|b-s|^xmA1A40kMYhjcZXftUJ1doO(@Y_8#~W}ixi>o?xq z6C?cao65NMr3M!O+YOPa7$P6o*EyBOc;z)rP_AkKgCCl6?nwzOCio-WMI4q2eLy#5 z-l3|C1-msz1X;N!*CM?z1j+hlslA5=ZzF;d8N>ZNF|Q z*#QpxLYoY|8bM0GcQ~}L5x5!+Wnapc!AQo{smFSW&``f|qkdB|RB{YPIrHU#qC%OW zxpN*c+IU*dxfj5tOauK7JboxDu*RI#LOfq;zU>;?eMIirx8;`^706o)lalDqW7wkk zo20jdzmh9@PVvlF*nT@%UrWCo==hJs#yY%*sm(h}H%~M}Oha*y4B?Nk2>yVzS>@pO zH&4X)KY1d`&7ii~Fb1Tqc4p+0}B4XSP_kA;S?x*?xK7~0n zf}4dYXrWnp%IOaU4cZd;e$43MCT%S?>OXr?T;fZJ{*W#P#YIIFNvhy`&V`KAi7NQJ zUm}COTLn9(4}^(u%VAMOHKYp4A^XP!r>Sar^f`Vs{*K&k%!$lXKUKdQN5#umeR8z$ z;%%>?_f3F_GsTQ_Jcy~~7nTZQ5&4HwIi!er!9t7GIIE8zN#p8IC)WwRPuo1C_qYgV zh(8hgW-E^Bf^Kd$V-h%ED&Ey19)YgjFm`jH2*Tqw##P9a0jId%Kzwo~d>>T#)I3K< z)!_^6FTPSx^LzcT9S(@0T~B^8`XkzYHhM{-)4@JD<*ut{Dk$EpFYnc_jOvu-c8d+l zSRdl-^@YfNgdDlI?ZJvL=B79pai1jeQOm(@ulDZ2H`aeVXfkxL$*5O;SFR2gxqONd zD*=>pOgt;e1$Zx(t3t!3rxF{RZ<(`6_fU^vUmyzom6B+ADN_(0+An>=|>JI+C{>vvrF-jEWdL z&^dBSgIHggXKy^g#ewr%l_kmCoEUPC_CDQVMAgp^rJ8d{$l`uA(IbBk#v)&y!Q38H zpfy{F2-87FlZPh~nD$^v#Zz2vfz3*?iTvYhg!dWl%>&Z z!)b9($2VmnnB44|Ll%g{`cf0+(v!`=HoBf*>DRcQGD813X-B=&9({;&(7)q?G#WS;H3H@BW>!HPg3U0i3Oq939i=WWr z;}x$Y3u05a>ql0XTR2|#N%~4y8)Oa9W?f_Jgn)Q?&pEp;FuQmm>Qyiq6+<`6Jh@K6 zd$bxIBuRB-E-BQkd!vf(B}^eUg7V1pVylMgr~-ZqelhLGz7v(qy&j1rNaBSDXRn43 z^OK`hW{*+DJY2GSP+q)5@UpJ%i@9Z`hhk{fX1E1$H3z8Mh4x^~+Xcso2bzel&3^S; zt0S9`EH&r3I?C@|Xq&r3gTm|QCa{zd4LDcrU#1hh4%x%`o}8R`PN_(x!kh=+)-&9` zxrHD59JlNCM@OM+%T~9cZJn^Y;`+NJgKp?MabINR3CxTYgPjzukNP)^&DS?&iD5(?5v~5qv>0%{K+&}yD5YBzTXyHJZm+b`G6CDac|tS zah4n7OaebnrQAmU`P03*I$dDhy{$r;@H4mSDQmh&Qo&*SoQ2U*GQo39D86Mw!6foX zo~^PH(#IVS^0+FGm0t$}sD!>Q^|ET%T3QCllCKYT^GV?PpGP*Ic!hA>`s^(u8Y_>xvLT?V->RExLHo0Y^Gc1@pvwiinI>9@uf_~B6 zoTyZ9 zEsNJ>1FJEoJ zplw?#k5)#bEAKJp^&NfS(P2aP_zzJRp4?*86gCLRe{^Kl!Dq|+~E&p&~r>D=6wB&kFQavZ| zA)y>f;KjK^7I5hQ^$d%4E=ZydAw&#KR{P1gN}q{ zh=XAVE^=Nxl-v0ek|w114zDc2i#x3Xi#;=A zr-mZ8N!>xowe?gZ<$k#DdqApAZ2+o&FnZ1p48aM?FWPh&GCHSM`%Lem;P2Ha!}VMV zg5$@?=t&Ys$>6B@%{;vLq@mJ(W2Xq__T^q$6cfO{V3yngf}eEHrONw3Ybmf6s9ZeO zp98`g*_^j~b3rB45u-(QkxgkEkOHmSA8LPax z&q(6hVoF+M`YcfGZJE+Fe!?DeTUxr$Gw`%f(ebtz9aicIXvIOZITy_kMP z93_s^G#$G$4F>IMlES<*5bAWgV?J;Kn7$rW5GH&MVOst%jhS?K!_D}x&UXg9bNgDY zR{I@f`fx7Lwx0@-ttv?#%0%DRI?BvIcofw2+2e1k<4E9Jz2|DacMaq!U=GERBbNU;l1u<+EIwl1PRJJlsx zaFEP{iB3)~ON5@}96$~}LgW^H1#pjDwqir!D4&4Ht8~ai-LGH9eIGKugv$9p4ug2n zCEDrfoA4y1vO7Cl2cLb_i*$;j_D2c1pzB-Fdg0(7x%#ab zIpX?s;mj!HL{=R0Aau$g>rcbkQ3D{47S;xY&(Gps`xCW8tMEimk8gsq4%_ZOs^WLP zi$OJqgcx@WK;8&L{#^brC{!NYU(z%N_clw83Tz@{5rGeR&PTyEHeJ1RZ5}ktQl?=r z;>3L(wnrbI-+<5GPezJPv*T8)_P`6zHetG;%lfO&Yp^D$lHW%|0f(nlC-?Cyz)81W zmG0~dZjM$WH2-MfL*EbeXWNMS>-DYMk;MFW+3klZv#t;ZEJ~#wNa97;G8NYGeqPM6 z{pUpB;1HxZjp<|)@3n+ICJG|{{UE$a*KcHU9u{Bdy=X}I1w%RElP07LjLmu*wMV4eT4;n7DzXErL{6M%D|m_FiXdv*bG8{EB1I+j3(?}VmG%Uv{l zs4e9sJ_wg>6-MX3j1YD2hD=V*3FwjQy?tOU0wv#Hraoh%V229(AvxPE*v+IXV%flq zndzNQI}fizW!y>Oz#JwVw>#x;>`jO4AvZtJepvw1)~l*4eiy)#otBQ}fCD@lvo-c- zbcE-J5}Ey9s3R-kcMOxHTo(7<6r{ z<Zs2yAobEO(fU!lESC^9OMgVEeY^SHp4y&NZ@F^)pj&Elw$#S%3xS(<$8-IT>+0 zba?bN(Wj(7arK>)-GC{ttKHoE*MYT%BdzfE9E5OcGP68$gQi=CjX$!@fcZ+tS^12^ z;P|O~TBA}G*Q!dK)B1Me(p`(!>^HV!e^}f02~jTG*L+LWSDh7y%bhF3i8|N7w*0JP zcOUq#{N(+d+yTFzkc!{Vv>armKk;PJTH4AieUaQ&p6gTU1dCzfPm@aWUK zL@am^{Hda+R|`g=N=@LyIO8O|_ZkgvUWmYbbAp}Gj1&wvw?3vD$Vkj*-diMp(i5Si zcYki5SOQ13XqHSuU!Q8-eI;}CH<(Q9PUS40hWD(JZJ9G zLa#6ciyCjvon{+@$N4vk3{OwOH<`k7MspEZdw=V`L|*zBS(OT-YqDC1=towJ>hGhGk7dBl6TDlRJ}}_PL?P3| z54&LVKSAGLi8MoiV}G+z{U=D0)il@MN#wFlmAk12j{yIZ9@ERpUa{3%W)n`Xhpu_nHpEsu9=o%EV!UFIX7AKPm=K_^ZXv zW)1lB>4>5=a~5rN$jvWJV-$yjM%`CwqzI=s%eNzNhGRmYAe zec)7?hPL?Wx`4+^@GE1TblYJTdQ5(*AH6mJr46ErE2s*r{*Nt>C-H;iPC2S2&sKOP zo!9u*Lk_FGBxJ7^+?ogJ&}Erj%2Oa`?t5TccRy^t>Q52LlZMCvLnb?E7T~94*soW~3i_un zX=xPhz&Q7pa+XA&nPENt`%Y^fY_ypn3Ccg^a!e$G*?f{en+zbjopx^H7j>n(!elx8Io_O^*Oi8vm|#v9=**= z63CmjaphNp0J2OC>n`T7qoEe<%5iX=SiiyYQyo-F9f=)oJ81>x5bRya?mTZ=m_rq&s0S2IrFBWu_b*2I|mF zt~SSUn0fD$SVNrx(o=~EmLCzwWIeNgWQmMICC4;1E#{%ea&4iTWfla!F)Ual4S=8e zc08{z0V5ppzN5JVz}|7~dFlI3i0C;g#=cAkf}8n<2NbBJ4RYOLh4_Qi9vN|CaabIO zro+r@2rlv9q*_^J*k&w}xzg5t_ZKXeQfV}&r@>38O4m?g2D)BldG_xA2%QYo)7z$B zL%Q3pQZ`C4l&@CnoAk7RzvQvz_ag1^meO%W%JmEE{GjnXXdwo>Z?m3~zB~*LR(Efm z3>t?*H9NcCjMMNU&px5AF9OFb4mR*Fl2OC>yZ!H`IWRZ%5!4{A&wk#kQ6Gu^|FPhE z`*EJZqRHeHN_ij4`UTW z)O_WwV!(|ln#Hp*+nKRdwI=BM;RTpTScyv7PdpDhzM9Y78Hc%r%7bq9jS$W4^rz9G z09M89c)b}42@bMBihocujF;XvleBFC_Y@A!^uulNP&Caikvgs&>_DcNT=z& z+zf`H{&9h-mUKT9*w5}1t7(H$!Ow=$zeY*+m0@Q}_}-EzPXjicc6~?MUwd6ajPN77 zGre$s#LtOOS6qYyb~2($Tce|Vz#OpZZ_ zg`{M*V2SlS`1p$bbKg)C)S(A=EcFxEof6@;c<~v!q>oCfvd5x_+A!>ReQ#y9 zXdJegc~_j?Hw|yD1u^XS9)Y~#9v*9i?wbWC&T*6Hc&V&`5M*JX)P<86h-`q#k3l;YC)XR zx^u8#2Rp{9SxO2-6TZK?z#np~1Sjfwtp3LzKfwO_vrQKT2Y{mT^hvk%Td@4)Z7NQ1 z)1qvbO##-lVZ4M`JDN{MyAElzZ`{RIU0wf)>w6HERK7Z7IAc zd{XF4QO_m}i)09P+e?GX6j@$BnGv8y%6?&?{|=`)1}!x#zQe0ij5em))xaR6WZ9Pd z911IpPfSOqf-gM*zdTa|r^RL7*A3LdC$8f70ICqY#UI4iDSt`-VYlbF)L; O+&El{xrRX@)9` Date: Wed, 1 Apr 2026 11:57:37 +0200 Subject: [PATCH 12/19] benchmark to quantify overhead of specific to generic advance --- cpp/benchmarks/CMakeLists.txt | 3 + cpp/benchmarks/secirvvs_advance.cpp | 93 +++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 cpp/benchmarks/secirvvs_advance.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 731fe8d566..cb2d6a8bbf 100755 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -34,5 +34,8 @@ target_link_libraries(simulation_benchmark PRIVATE memilio ode_secir benchmark:: add_executable(graph_simulation_benchmark graph_simulation.cpp) target_link_libraries(graph_simulation_benchmark PRIVATE memilio ode_secirvvs benchmark::benchmark) +add_executable(secirvvs_advance_benchmark secirvvs_advance.cpp) +target_link_libraries(secirvvs_advance_benchmark PRIVATE memilio ode_secirvvs benchmark::benchmark) + add_executable(abm_benchmark abm.cpp) target_link_libraries(abm_benchmark PRIVATE abm benchmark::benchmark) diff --git a/cpp/benchmarks/secirvvs_advance.cpp b/cpp/benchmarks/secirvvs_advance.cpp new file mode 100644 index 0000000000..e4a432371b --- /dev/null +++ b/cpp/benchmarks/secirvvs_advance.cpp @@ -0,0 +1,93 @@ +/* +* Copyright (C) 2020-2026 MEmilio +* +* Authors: Henrik Zunker +* +* 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. +*/ + +/** + * Benchmark comparing the overhead of the model-specific osecirvvs::Simulation::advance() + * (which uses substeps and calls apply_vaccination / apply_variant / dynamic NPI checks) + * versus the generic mio::Simulation::advance() (a single integrator call for the entire range). + */ + +#include "ode_secirvvs/model.h" +#include "memilio/compartments/simulation.h" +#include "memilio/utils/logging.h" +#include "benchmark/benchmark.h" + +using FP = double; +using Model = mio::osecirvvs::Model; + +static Model make_model(bool with_npis = false) +{ + constexpr int tmax = 10; + Model model(1); + model.populations[{mio::AgeGroup(0), mio::osecirvvs::InfectionState::InfectedSymptomsNaive}] = 100.0; + model.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecirvvs::InfectionState::SusceptibleNaive}, + 10000.0); + model.parameters.get>().resize(mio::SimulationDay(size_t(tmax + 1))); + model.parameters.get>().resize(mio::SimulationDay(size_t(tmax + 1))); + if (with_npis) { + auto& npis = model.parameters.get>(); + npis.set_threshold(0.01 * 100'000, {mio::DampingSampling{1.0, + mio::DampingLevel(0), + mio::DampingType(0), + mio::SimulationTime(0), + {0}, + Eigen::VectorXd::Ones(1)}}); + npis.set_duration(mio::SimulationTime(14.0)); + npis.set_base_value(100'000); + } + return model; +} + +// Generic advance: single integrator call for the full [t0, tmax] range. +static void BM_generic(benchmark::State& state) +{ + mio::set_log_level(mio::LogLevel::off); + auto model = make_model(); + for (auto _ : state) { + mio::simulate(0., 10., 0.1, model); + } +} + +// Model-specific advance without dynamic NPIs: 1-day loop with apply_vaccination + apply_variant, +// dynamic NPI threshold check is skipped (thresholds empty). +static void BM_secirvvs_no_npis(benchmark::State& state) +{ + mio::set_log_level(mio::LogLevel::off); + auto model = make_model(/*with_npis=*/false); + for (auto _ : state) { + mio::osecirvvs::simulate(0., 10., 0.1, model); + } +} + +// Model-specific advance with dynamic NPIs: same as above plus get_infections_relative + +// threshold comparison on every day step. +static void BM_secirvvs_with_npis(benchmark::State& state) +{ + mio::set_log_level(mio::LogLevel::off); + auto model = make_model(/*with_npis=*/true); + for (auto _ : state) { + mio::osecirvvs::simulate(0., 10., 0.1, model); + } +} + +BENCHMARK(BM_generic)->Name("SECIRVVS generic advance"); +BENCHMARK(BM_secirvvs_no_npis)->Name("SECIRVVS model-specific advance (no dynamic NPIs)"); +BENCHMARK(BM_secirvvs_with_npis)->Name("SECIRVVS model-specific advance (with dynamic NPIs)"); +BENCHMARK_MAIN(); From 86136fd19635f229050de454e17ef4a44b29b5fd Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:40:30 +0200 Subject: [PATCH 13/19] adapt dynamic npi structure in benchmark --- cpp/benchmarks/flow_simulation_ode_secirvvs.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cpp/benchmarks/flow_simulation_ode_secirvvs.h b/cpp/benchmarks/flow_simulation_ode_secirvvs.h index b00a623d67..be893af609 100644 --- a/cpp/benchmarks/flow_simulation_ode_secirvvs.h +++ b/cpp/benchmarks/flow_simulation_ode_secirvvs.h @@ -713,6 +713,15 @@ void setup_model(Model& model) model.parameters.template get>()[AgeGroup(0)] = 0.9; model.parameters.template get>() = 0.2; + + auto& npis = model.parameters.template get>(); + auto npi_groups = Eigen::VectorXd::Ones(contact_matrix[0].get_num_groups()); + npis.set_threshold(0.01 * 100'000, {DampingSampling(0.5, DampingLevel(0), DampingType(0), + SimulationTime(0), {0}, npi_groups)}); + npis.set_base_value(100'000); + npis.set_implementation_delay(SimulationTime(7.0)); + npis.set_duration(SimulationTime(14.0)); + // The function apply_constraints() ensures that all parameters are within their defined bounds. // Note that negative values are set to zero instead of stopping the simulation. model.apply_constraints(); From 7e8e4e18d80b46ba5f63ef11fe9f3429e3ff7ef7 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:40:46 +0200 Subject: [PATCH 14/19] missing test for secirvvs --- cpp/tests/test_dynamic_npis.cpp | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index f1142c702f..b2f3756ee1 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -756,6 +756,90 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); } +TEST(DynamicNPIs, secirvvs_implementation_with_directives) +{ + mio::osecirvvs::Model model(1); + model.populations[{mio::AgeGroup(0), mio::osecirvvs::InfectionState::InfectedSymptomsNaive}] = 10; + model.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecirvvs::InfectionState::SusceptibleNaive}, + 100); + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + model.parameters.get>().resize(mio::SimulationDay(size_t(1000))); + model.parameters.get>().array().setConstant(0); + + mio::ContactMatrixGroup& cm = model.parameters.get>(); + cm[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 1.0)); + + mio::DynamicNPIs npis; + npis.set_threshold(0.05 * 50'000, {mio::DampingSampling{0.5, + mio::DampingLevel(0), + mio::DampingType(0), + mio::SimulationTime(0), + {0}, + Eigen::VectorXd::Ones(1)}}); + npis.set_duration(mio::SimulationTime(5.0)); + npis.set_base_value(50'000); + + // directive begin is after the simulation, so no NPI is implemented + npis.set_directive_begin(mio::SimulationTime(5.0)); + model.parameters.get>() = npis; + mio::osecirvvs::Simulation> sim(model, 0.0); + sim.advance(3.0); + mio::ContactMatrixGroup const& contact_matrix = + sim.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + + // directive begin is satisfied + npis.set_implementation_delay(mio::SimulationTime(2.0)); // not used as t0=0 + npis.set_directive_begin(mio::SimulationTime(0.0)); + model.parameters.get>() = npis; + mio::osecirvvs::Simulation> sim_2(model, 0.0); + sim_2.advance(3.0); + mio::ContactMatrixGroup const& contact_matrix_sim_2 = + sim_2.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted after duration + + // directive begin satisfied but directive end cuts NPI earlier + npis.set_directive_end(mio::SimulationTime(3.0)); + model.parameters.get>() = npis; + mio::osecirvvs::Simulation> sim_3(model, 0.0); + sim_3.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_3 = + sim_3.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + + // directive begin satisfied (with delay > 0 as t0 = 1) + npis.set_implementation_delay(mio::SimulationTime(2.0)); + npis.set_directive_begin(mio::SimulationTime(0.0)); + npis.set_directive_end(mio::SimulationTime(1000000.)); + model.parameters.get>() = npis; + mio::osecirvvs::Simulation> sim_4(model, 1.0); + sim_4.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_4 = + sim_4.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(7.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 1.0); // lifted after duration + + // directive begin satisfied but directive end cuts NPI earlier (with delay > 0 as t0 = 1) + npis.set_directive_end(mio::SimulationTime(4.0)); + model.parameters.get>() = npis; + mio::osecirvvs::Simulation> sim_5(model, 1.0); + sim_5.advance(4.0); + mio::ContactMatrixGroup const& contact_matrix_sim_5 = + sim_5.get_model().parameters.template get>(); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); +} + TEST(DynamicNPIs, osecirts_delayed_implementation) { mio::osecirts::Model model(1); From b05d08009ad6365a214398f8100b4fe5d03bd9c5 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:22:25 +0200 Subject: [PATCH 15/19] update python test, rm t_next_damp comment --- cpp/models/ode_secir/model.h | 8 -------- pycode/memilio-simulation/tests/test_dynamic_npis.py | 8 ++++++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index dc9c54aab7..14659358ee 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -302,14 +302,6 @@ class Simulation : public BaseT FP delay_npi_implementation; FP t = BaseT::get_result().get_last_time(); while (t < tmax) { - // FP t_next_damp = t; - // for (auto&& mat : contact_patterns.get_cont_freq_mat()) { - // for (auto&& damp : mat.get_dampings()) { - // FP t_damp = damp.get_time().get(); - // if(t_damp > t && t_damp < - // } - // } - if (t > 0) { delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } diff --git a/pycode/memilio-simulation/tests/test_dynamic_npis.py b/pycode/memilio-simulation/tests/test_dynamic_npis.py index de8fc83ec4..61dd1971d0 100644 --- a/pycode/memilio-simulation/tests/test_dynamic_npis.py +++ b/pycode/memilio-simulation/tests/test_dynamic_npis.py @@ -32,10 +32,14 @@ def test_dynamic_npis(self): """ """ model = Model(0) dynamic_npis = model.parameters.DynamicNPIsInfectedSymptoms - dynamic_npis.interval = 3.0 + dynamic_npis.implementation_delay = 3.0 + dynamic_npis.directive_begin = 1.0 + dynamic_npis.directive_end = 10.0 dynamic_npis.duration = 14.0 dynamic_npis.base_value = 100000 - self.assertEqual(dynamic_npis.interval, 3.0) + self.assertEqual(dynamic_npis.implementation_delay, 3.0) + self.assertEqual(dynamic_npis.directive_begin, 1.0) + self.assertEqual(dynamic_npis.directive_end, 10.0) self.assertEqual(dynamic_npis.duration, 14.0) self.assertEqual(dynamic_npis.base_value, 100000) self.assertEqual(len(dynamic_npis.threshold), 0) From 7f36b26879d9d29dc78ebc76a1bfb866cfaaa001 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:09:04 +0200 Subject: [PATCH 16/19] +1 time offset for dynamic npis --- cpp/models/ode_secir/model.h | 10 ++- cpp/models/ode_secirts/model.h | 10 ++- cpp/models/ode_secirvvs/model.h | 10 ++- cpp/tests/test_dynamic_npis.cpp | 139 +++++++++++++++++++------------- 4 files changed, 107 insertions(+), 62 deletions(-) diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index 14659358ee..8c0b33abe3 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -322,11 +322,17 @@ class Simulation : public BaseT if (t + delay_npi_implementation < direc_end) { auto t_start = SimulationTime(t + delay_npi_implementation); - // set the end to the minimum of start+delay and the end of the directive + // set the end to the minimum of start+duration and the end of the directive auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + // For t_start > 0: shift dampings by +1 so the smooth transition window + // [t_start, t_start+1] lies in the future, consistent with predefined dampings. + // For t_start = 0: window [-1, 0] is in the past, so keep as is. + auto t_start_damping = + (FP(t_start) > FP(0)) ? SimulationTime(FP(t_start) + FP(1)) : t_start; + auto t_end_damping = (FP(t_start) > FP(0)) ? SimulationTime(FP(t_end) + FP(1)) : t_end; implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { + t_start_damping, t_end_damping, [](auto& g) { return make_contact_damping_matrix(g); }); } diff --git a/cpp/models/ode_secirts/model.h b/cpp/models/ode_secirts/model.h index bcc53bf3d5..c7c48ed7ad 100644 --- a/cpp/models/ode_secirts/model.h +++ b/cpp/models/ode_secirts/model.h @@ -792,13 +792,19 @@ class Simulation : public BaseT if (t + delay_npi_implementation < direc_end) { auto t_start = SimulationTime(t + delay_npi_implementation); - // set the end to the minimum of start+delay and the end of the directive + // set the end to the minimum of start+duration and the end of the directive auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + // For t_start > 0: shift dampings by +1 so the smooth transition window + // [t_start, t_start+1] lies in the future, consistent with predefined dampings. + // For t_start = 0: window [-1, 0] is in the past, so keep as is. + auto t_start_damping = + (FP(t_start) > FP(0)) ? SimulationTime(FP(t_start) + FP(1)) : t_start; + auto t_end_damping = (FP(t_start) > FP(0)) ? SimulationTime(FP(t_end) + FP(1)) : t_end; implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { + t_start_damping, t_end_damping, [](auto& g) { return make_contact_damping_matrix(g); }); } diff --git a/cpp/models/ode_secirvvs/model.h b/cpp/models/ode_secirvvs/model.h index d29f8359ce..605f30314d 100644 --- a/cpp/models/ode_secirvvs/model.h +++ b/cpp/models/ode_secirvvs/model.h @@ -717,13 +717,19 @@ class Simulation : public BaseT if (t + delay_npi_implementation < direc_end) { auto t_start = SimulationTime(t + delay_npi_implementation); - // set the end to the minimum of start+delay and the end of the directive + // set the end to the minimum of start+duration and the end of the directive auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); + // For t_start > 0: shift dampings by +1 so the smooth transition window + // [t_start, t_start+1] lies in the future, consistent with predefined dampings. + // For t_start = 0: window [-1, 0] is in the past, so keep as is. + auto t_start_damping = + (FP(t_start) > FP(0)) ? SimulationTime(FP(t_start) + FP(1)) : t_start; + auto t_end_damping = (FP(t_start) > FP(0)) ? SimulationTime(FP(t_end) + FP(1)) : t_end; implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, - t_start, t_end, [](auto& g) { + t_start_damping, t_end_damping, [](auto& g) { return make_contact_damping_matrix(g); }); } diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index b2f3756ee1..c3224d6080 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -505,6 +505,7 @@ TEST(DynamicNPIs, secir_delayed_implementation) 0); // start with t0 = 0.0 so dynamicNPIs are active from the start + // t0=0 is special: delay is not enforced, t_start=0, smooth window [-1, 0] -> full effect at t=0 npis.set_implementation_delay(mio::SimulationTime(3.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim(model, 0.0); @@ -515,25 +516,27 @@ TEST(DynamicNPIs, secir_delayed_implementation) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + // second simulation with t0 = 1.0: NPI check at t=1, delay=2 -> t_start=3, damping at t_start+1=4 + // smooth window [3, 4]: at t=3 still 1.0, at t=4 full effect 0.5 npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_2(model, 1.0); - sim_2.advance(4.0); + sim_2.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); // before ramp + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // full effect - // third simulation; NPIs are implemented at t0 + delay = 11.0 + // third simulation: delay=10 -> t_start=11, damping at t_start+1=12 + // smooth window [11, 12]: at t=11 still 1.0, at t=12 full effect 0.5 npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_3(model, 1.0); sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); // before ramp + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); // full effect } TEST(DynamicNPIs, secir_implementation_with_directives) @@ -571,6 +574,8 @@ TEST(DynamicNPIs, secir_implementation_with_directives) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // directive begin is satisfied + // t0=0: delay not enforced, t_start=0, smooth window [-1,0] -> full effect immediately at t=0 + // duration=5 -> t_end=5, t_end_damping=6: smooth window [5,6] -> back to 1.0 at t=6 npis.set_implementation_delay(mio::SimulationTime(2.0)); // not used as t0=0 npis.set_directive_begin(mio::SimulationTime(0.0)); model.parameters.get>() = npis; @@ -578,45 +583,52 @@ TEST(DynamicNPIs, secir_implementation_with_directives) sim_2.advance(3.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), + 0.5); // full effect at t=0 (window in past) EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), + 1.0); // lifted after duration+1 // directive begin is satisfied but directive end ends the NPI earlier + // t_start=0, t_end=min(3,5)=3, t_end_damping=4: window [3,4] -> 1.0 at t=4 npis.set_directive_end(mio::SimulationTime(3.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_3(model, 0.0); - sim_3.advance(4.0); + sim_3.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); // lifted at t_end+1=4 // directive begin is satisfied (now with delay>0 as t0=1) + // t_start=3, t_start_damping=4, smooth window [3,4] -> 0.5 at t=4 + // duration=5 -> t_end=8, t_end_damping=9, smooth window [8,9] -> 1.0 at t=9 npis.set_implementation_delay(mio::SimulationTime(2.0)); npis.set_directive_begin(mio::SimulationTime(0.0)); npis.set_directive_end(mio::SimulationTime(1000000.)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_4(model, 1.0); - sim_4.advance(4.0); + sim_4.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_4 = sim_4.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(7.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // ramp not yet done + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // full effect + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 0.5); // still active + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(9.0))(0, 0), 1.0); // lifted at t_end+1=9 // directive begin is satisfied but directive end ends the NPI earlier (now with delay>0 as t0=1) + // t_start=3, t_end=min(4,8)=4, t_end_damping=5: 1.0 at t=5 npis.set_directive_end(mio::SimulationTime(4.0)); model.parameters.get>() = npis; mio::osecir::Simulation> sim_5(model, 1.0); - sim_5.advance(4.0); + sim_5.advance(6.0); mio::ContactMatrixGroup const& contact_matrix_sim_5 = sim_5.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // starts lifting then - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), + 0.5); // full effect at t_start+1=4 + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted at t_end+1=5 } TEST(DynamicNPIs, secirvvs_threshold_safe) @@ -727,6 +739,7 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) 0); // start with t0 = 0.0 so dynamicNPIs are active from the start + // t0=0: delay not enforced, t_start=0, smooth window [-1,0] -> full effect at t=0 mio::osecirvvs::Simulation> sim(model, 0.0); sim.advance(3.0); mio::ContactMatrixGroup const& contact_matrix = @@ -735,25 +748,26 @@ TEST(DynamicNPIs, secirvvs_delayed_implementation) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + // second simulation with t0 = 1.0: t_start=3, t_start_damping=4, smooth window [3,4] + // -> at t=3 still 1.0, at t=4 full effect 0.5 npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_2(model, 1.0); - sim_2.advance(4.0); + sim_2.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // ramp not yet done + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // full effect - // third simulation; NPIs are implemented at t0 + delay = 11.0 + // third simulation: delay=10 -> t_start=11, t_start_damping=12 npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_3(model, 1.0); sim_3.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); // ramp start + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); // full effect } TEST(DynamicNPIs, secirvvs_implementation_with_directives) @@ -792,6 +806,8 @@ TEST(DynamicNPIs, secirvvs_implementation_with_directives) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // directive begin is satisfied + // t0=0: delay not enforced, t_start=0, smooth window [-1,0] -> full effect at t=0 + // duration=5 -> t_end=5, t_end_damping=6: back to 1.0 at t=6 npis.set_implementation_delay(mio::SimulationTime(2.0)); // not used as t0=0 npis.set_directive_begin(mio::SimulationTime(0.0)); model.parameters.get>() = npis; @@ -801,43 +817,47 @@ TEST(DynamicNPIs, secirvvs_implementation_with_directives) sim_2.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 1.0); // lifted at t_end+1=6 // directive begin satisfied but directive end cuts NPI earlier + // t_start=0, t_end=min(3,5)=3, t_end_damping=4: 1.0 at t=4 npis.set_directive_end(mio::SimulationTime(3.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_3(model, 0.0); - sim_3.advance(4.0); + sim_3.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); // lifted at t_end+1=4 // directive begin satisfied (with delay > 0 as t0 = 1) + // t_start=3, t_start_damping=4: 0.5 at t=4; t_end=8, t_end_damping=9: 1.0 at t=9 npis.set_implementation_delay(mio::SimulationTime(2.0)); npis.set_directive_begin(mio::SimulationTime(0.0)); npis.set_directive_end(mio::SimulationTime(1000000.)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_4(model, 1.0); - sim_4.advance(4.0); + sim_4.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_4 = sim_4.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(7.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // ramp start + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // full effect + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 0.5); // still active + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(9.0))(0, 0), 1.0); // lifted at t_end+1=9 // directive begin satisfied but directive end cuts NPI earlier (with delay > 0 as t0 = 1) + // t_end=min(4,8)=4, t_end_damping=5: 1.0 at t=5 npis.set_directive_end(mio::SimulationTime(4.0)); model.parameters.get>() = npis; mio::osecirvvs::Simulation> sim_5(model, 1.0); - sim_5.advance(4.0); + sim_5.advance(6.0); mio::ContactMatrixGroup const& contact_matrix_sim_5 = sim_5.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), + 0.5); // full effect at t_start+1=4 + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted at t_end+1=5 } TEST(DynamicNPIs, osecirts_delayed_implementation) @@ -871,7 +891,7 @@ TEST(DynamicNPIs, osecirts_delayed_implementation) EXPECT_EQ( model.parameters.get>().get_cont_freq_mat()[0].get_dampings().size(), 0); - // start with t0 = 0.0 so dynamicNPIs are active from the start + // start with t0 = 0.0: delay not enforced, t_start=0, smooth window [-1,0] -> full effect at t=0 mio::osecirts::Simulation> sim(model, 0.0); sim.advance(3.0); mio::ContactMatrixGroup const& contact_matrix = @@ -880,25 +900,26 @@ TEST(DynamicNPIs, osecirts_delayed_implementation) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - // second simulation with t0 = 1.0, so the NPIs are implemented at t0 + delay = 3.0 + // second simulation with t0 = 1.0: t_start=3, t_start_damping=4, smooth window [3,4] + // -> at t=3 still 1.0, at t=4 full effect 0.5 npis.set_implementation_delay(mio::SimulationTime(2.0)); model.parameters.get>() = npis; mio::osecirts::Simulation> sim_2(model, 1.0); - sim_2.advance(4.0); + sim_2.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_2 = sim_2.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // ramp start + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // full effect - // third simulation; NPIs are implemented at t0 + delay = 11.0 + // third simulation: delay=10 -> t_start=11, t_start_damping=12 npis.set_implementation_delay(mio::SimulationTime(10.0)); model.parameters.get>() = npis; mio::osecirts::Simulation> sim_3_secirts(model, 1.0); sim_3_secirts.advance(4.0); mio::ContactMatrixGroup const& contact_matrix_sim_3_secirts = sim_3_secirts.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_3_secirts.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_3_secirts.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 0.5); + EXPECT_EQ(contact_matrix_sim_3_secirts.get_matrix_at(mio::SimulationTime(11.0))(0, 0), 1.0); // ramp start + EXPECT_EQ(contact_matrix_sim_3_secirts.get_matrix_at(mio::SimulationTime(12.0))(0, 0), 0.5); // full effect } TEST(DynamicNPIs, osecirts_implementation_with_directives) @@ -941,6 +962,8 @@ TEST(DynamicNPIs, osecirts_implementation_with_directives) EXPECT_EQ(contact_matrix.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // directive begin is satisfied (t0=0, so delay is not enforced) + // t_start=0, smooth window [-1,0] -> full effect at t=0 + // duration=5 -> t_end=5, t_end_damping=6: back to 1.0 at t=6 npis.set_implementation_delay(mio::SimulationTime(2.0)); // not used as t0=0 npis.set_directive_begin(mio::SimulationTime(0.0)); model.parameters.get>() = npis; @@ -950,41 +973,45 @@ TEST(DynamicNPIs, osecirts_implementation_with_directives) sim_2.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_2.get_matrix_at(mio::SimulationTime(6.0))(0, 0), 1.0); // lifted at t_end+1=6 // directive begin is satisfied but directive end ends the NPI earlier + // t_start=0, t_end=min(3,5)=3, t_end_damping=4: 1.0 at t=4 npis.set_directive_end(mio::SimulationTime(3.0)); model.parameters.get>() = npis; mio::osecirts::Simulation> sim_3(model, 0.0); - sim_3.advance(4.0); + sim_3.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_3 = sim_3.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(0.0))(0, 0), 0.5); EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_3.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); // lifted at t_end+1=4 // directive begin is satisfied (now with delay>0 as t0=1) + // t_start=3, t_start_damping=4: 0.5 at t=4; t_end=8, t_end_damping=9: 1.0 at t=9 npis.set_implementation_delay(mio::SimulationTime(2.0)); npis.set_directive_begin(mio::SimulationTime(0.0)); npis.set_directive_end(mio::SimulationTime(1000000.)); model.parameters.get>() = npis; mio::osecirts::Simulation> sim_4(model, 1.0); - sim_4.advance(4.0); + sim_4.advance(5.0); mio::ContactMatrixGroup const& contact_matrix_sim_4 = sim_4.get_model().parameters.template get>(); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(2.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(7.0))(0, 0), 0.5); - EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 1.0); // lifted after duration + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 1.0); // ramp start + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // full effect + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 0.5); // still active + EXPECT_EQ(contact_matrix_sim_4.get_matrix_at(mio::SimulationTime(9.0))(0, 0), 1.0); // lifted at t_end+1=9 // directive end ends the NPI earlier (now with delay>0 as t0=1) + // t_end=min(4,8)=4, t_end_damping=5: 1.0 at t=5 npis.set_directive_end(mio::SimulationTime(4.0)); model.parameters.get>() = npis; mio::osecirts::Simulation> sim_5(model, 1.0); - sim_5.advance(4.0); + sim_5.advance(6.0); mio::ContactMatrixGroup const& contact_matrix_sim_5 = sim_5.get_model().parameters.template get>(); EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(1.0))(0, 0), 1.0); - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(3.0))(0, 0), 0.5); // starts lifting then - EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 1.0); + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(4.0))(0, 0), + 0.5); // full effect at t_start+1=4 + EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted at t_end+1=5 } From 470a7e71e1e1fe5af84f5d2144ea6f17752c5771 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:35:22 +0200 Subject: [PATCH 17/19] backward consistency test --- cpp/tests/data/results_osecirvvs.h5 | Bin 33312 -> 33312 bytes cpp/tests/test_dynamic_npis.cpp | 45 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/cpp/tests/data/results_osecirvvs.h5 b/cpp/tests/data/results_osecirvvs.h5 index 25780abf67c9f06f259bfd98f9aba1ea1316e938..d8f196ea466c9e84cf3e7b7263d94ab98c9a1264 100644 GIT binary patch delta 7916 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 diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index c3224d6080..12d725f0b5 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -631,6 +631,51 @@ TEST(DynamicNPIs, secir_implementation_with_directives) EXPECT_EQ(contact_matrix_sim_5.get_matrix_at(mio::SimulationTime(5.0))(0, 0), 1.0); // lifted at t_end+1=5 } +TEST(DynamicNPIs, secir_backward_consistency_with_predefined) +{ + // Verify the core reproducibility guarantee of the +1 offset: + // A simulation with dynamic NPIs (threshold exceeded at t=1, delay=2 -> t_start=3) + // must produce the same contact matrix as a simulation with predefined dampings + // placed at t_start_damping = t_start+1 = 4 and t_end_damping = t_end+1 = 9. + mio::osecir::Model model(1); + model.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedSymptoms}] = 10; + model.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecir::InfectionState::Susceptible}, 100); + + mio::ContactMatrixGroup& cm = model.parameters.get>(); + cm[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 1.0)); + + mio::DynamicNPIs npis; + npis.set_threshold(0.05 * 50'000, {mio::DampingSampling{0.5, + mio::DampingLevel(0), + mio::DampingType(0), + mio::SimulationTime(0), + {0}, + Eigen::VectorXd::Ones(1)}}); + npis.set_duration(mio::SimulationTime(5.0)); + npis.set_base_value(50'000); + npis.set_implementation_delay(mio::SimulationTime(2.0)); + model.parameters.get>() = npis; + + // t0=1: threshold exceeded at t=1, delay=2 -> t_start=3, t_start_damping=4, + // duration=5 -> t_end=8, t_end_damping=9 + mio::osecir::Simulation> sim(model, 1.0); + sim.advance(10.0); + + // Equivalent predefined dampings: place damping at t_start_damping=4 and restore at t_end_damping=9. + // smoother_cosine uses window [t-1, t], so damping at t=4 gives window [3,4]. + mio::ContactMatrixGroup cm_expected(1, 1); + cm_expected[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 1.0)); + cm_expected[0].add_damping(0.5, mio::DampingLevel(0), mio::DampingType(0), mio::SimulationTime(4.0)); + cm_expected[0].add_damping(0.0, mio::DampingLevel(0), mio::DampingType(0), mio::SimulationTime(9.0)); + + auto const& cm_dynamic = sim.get_model().parameters.get>().get_cont_freq_mat(); + for (double t : {1.0, 2.0, 3.0, 3.5, 4.0, 5.0, 7.0, 8.0, 8.5, 9.0, 10.0}) { + EXPECT_DOUBLE_EQ(cm_dynamic.get_matrix_at(mio::SimulationTime(t))(0, 0), + cm_expected.get_matrix_at(mio::SimulationTime(t))(0, 0)) + << "Mismatch at t=" << t; + } +} + TEST(DynamicNPIs, secirvvs_threshold_safe) { mio::osecirvvs::Model model(1); From 67e70b9dfe1510bb89f1e45508741b06e22f5cec Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:37:10 +0200 Subject: [PATCH 18/19] add is_expiry_renewal to avoid dips if dynamic npis is extended --- cpp/models/ode_secir/model.h | 28 ++++++++++++----- cpp/models/ode_secirts/model.h | 28 ++++++++++++----- cpp/models/ode_secirvvs/model.h | 28 ++++++++++++----- cpp/tests/test_dynamic_npis.cpp | 55 +++++++++++++++++++++++++++++++-- 4 files changed, 112 insertions(+), 27 deletions(-) diff --git a/cpp/models/ode_secir/model.h b/cpp/models/ode_secir/model.h index 8c0b33abe3..0185dae3d7 100644 --- a/cpp/models/ode_secir/model.h +++ b/cpp/models/ode_secir/model.h @@ -305,7 +305,7 @@ class Simulation : public BaseT if (t > 0) { delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } - else { // DynamicNPIs for t=0 are 'misused' to be 'from-start NPIs'. I.e., do not enforce delay. + else { // DynamicNPIs for t=0 are treated as 'from-start NPIs'. I.e., do not enforce delay. delay_npi_implementation = 0; } @@ -320,16 +320,28 @@ class Simulation : public BaseT (exceeded_threshold->first > m_dynamic_npi.first || t > FP(m_dynamic_npi.second))) { // old npi was weaker or is expired - if (t + delay_npi_implementation < direc_end) { - auto t_start = SimulationTime(t + delay_npi_implementation); + // Keep-alive: if the NPI expired but the threshold is still exceeded at the + // same level, renew immediately without delay to avoid a gap. + // Apply implementation delay only if stronger NPI needed. + bool is_expiry_renewal = + (t > FP(m_dynamic_npi.second)) && !(exceeded_threshold->first > m_dynamic_npi.first); + FP effective_delay = is_expiry_renewal ? FP(0) : delay_npi_implementation; + if (t + effective_delay < direc_end) { + auto t_start = SimulationTime(t + effective_delay); // set the end to the minimum of start+duration and the end of the directive auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - // For t_start > 0: shift dampings by +1 so the smooth transition window - // [t_start, t_start+1] lies in the future, consistent with predefined dampings. - // For t_start = 0: window [-1, 0] is in the past, so keep as is. - auto t_start_damping = - (FP(t_start) > FP(0)) ? SimulationTime(FP(t_start) + FP(1)) : t_start; + // For new NPIs (t_start > 0): shift t_start_damping by +1 so the smooth + // transition window [t_start, t_start+1] lies in the future, consistent with + // predefined dampings. For t_start = 0 (global t0): the window [-1, 0] + // is in the past and no shift is needed. + // For keep-alive renewals (is_expiry_renewal): do not shift t_start_damping. + // Since t_start == t_end_damping_old, the new start damping has the same + // (time, level, type) as the previous entry. + // Therefore, the contact matrix stays constant at the NPI level with no dip. + auto t_start_damping = (!is_expiry_renewal && FP(t_start) > FP(0)) + ? SimulationTime(FP(t_start) + FP(1)) + : t_start; auto t_end_damping = (FP(t_start) > FP(0)) ? SimulationTime(FP(t_end) + FP(1)) : t_end; implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, t_start_damping, t_end_damping, [](auto& g) { diff --git a/cpp/models/ode_secirts/model.h b/cpp/models/ode_secirts/model.h index c7c48ed7ad..b58bb28f4d 100644 --- a/cpp/models/ode_secirts/model.h +++ b/cpp/models/ode_secirts/model.h @@ -775,7 +775,7 @@ class Simulation : public BaseT delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } else { - // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. + // DynamicNPIs for t=0 are treated as 'from-start NPIs'. I.e., do not enforce delay. delay_npi_implementation = 0; } @@ -790,18 +790,30 @@ class Simulation : public BaseT (exceeded_threshold->first > m_dynamic_npi.first || t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired - if (t + delay_npi_implementation < direc_end) { - auto t_start = SimulationTime(t + delay_npi_implementation); + // Keep-alive: if the NPI expired but the threshold is still exceeded at the + // same level, renew immediately without delay to avoid a gap. + // Apply implementation delay only if stronger NPI needed. + bool is_expiry_renewal = + (t > FP(m_dynamic_npi.second)) && !(exceeded_threshold->first > m_dynamic_npi.first); + FP effective_delay = is_expiry_renewal ? FP(0) : delay_npi_implementation; + if (t + effective_delay < direc_end) { + auto t_start = SimulationTime(t + effective_delay); // set the end to the minimum of start+duration and the end of the directive auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - // For t_start > 0: shift dampings by +1 so the smooth transition window - // [t_start, t_start+1] lies in the future, consistent with predefined dampings. - // For t_start = 0: window [-1, 0] is in the past, so keep as is. - auto t_start_damping = - (FP(t_start) > FP(0)) ? SimulationTime(FP(t_start) + FP(1)) : t_start; + // For new NPIs (t_start > 0): shift t_start_damping by +1 so the smooth + // transition window [t_start, t_start+1] lies in the future, consistent with + // predefined dampings. For t_start = 0 (global t0): the window [-1, 0] + // is in the past and no shift is needed. + // For keep-alive renewals (is_expiry_renewal): do not shift t_start_damping. + // Since t_start == t_end_damping_old, the new start damping has the same + // (time, level, type) as the previous entry. + // Therefore, the contact matrix stays constant at the NPI level with no dip. + auto t_start_damping = (!is_expiry_renewal && FP(t_start) > FP(0)) + ? SimulationTime(FP(t_start) + FP(1)) + : t_start; auto t_end_damping = (FP(t_start) > FP(0)) ? SimulationTime(FP(t_end) + FP(1)) : t_end; implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, t_start_damping, t_end_damping, [](auto& g) { diff --git a/cpp/models/ode_secirvvs/model.h b/cpp/models/ode_secirvvs/model.h index 605f30314d..5184846c88 100644 --- a/cpp/models/ode_secirvvs/model.h +++ b/cpp/models/ode_secirvvs/model.h @@ -696,7 +696,7 @@ class Simulation : public BaseT if (t > 0) { delay_npi_implementation = FP(dyn_npis.get_implementation_delay()); } - else { // DynamicNPIs for t=0 are 'misused' to be from-start NPIs. I.e., do not enforce delay. + else { // DynamicNPIs for t=0 are treated as 'from-start NPIs'. I.e., do not enforce delay. delay_npi_implementation = 0; } if (t == 0) { @@ -715,18 +715,30 @@ class Simulation : public BaseT (exceeded_threshold->first > m_dynamic_npi.first || t > FP(m_dynamic_npi.second))) { //old npi was weaker or is expired - if (t + delay_npi_implementation < direc_end) { - auto t_start = SimulationTime(t + delay_npi_implementation); + // Keep-alive: if the NPI expired but the threshold is still exceeded at the + // same level, renew immediately without delay to avoid a gap. + // Apply implementation delay only if stronger NPI needed. + bool is_expiry_renewal = + (t > FP(m_dynamic_npi.second)) && !(exceeded_threshold->first > m_dynamic_npi.first); + FP effective_delay = is_expiry_renewal ? FP(0) : delay_npi_implementation; + if (t + effective_delay < direc_end) { + auto t_start = SimulationTime(t + effective_delay); // set the end to the minimum of start+duration and the end of the directive auto t_end = SimulationTime(min(direc_end, FP(t_start + dyn_npis.get_duration()))); this->get_model().parameters.get_start_commuter_detection() = (FP)t_start; this->get_model().parameters.get_end_commuter_detection() = (FP)t_end; m_dynamic_npi = std::make_pair(exceeded_threshold->first, t_end); - // For t_start > 0: shift dampings by +1 so the smooth transition window - // [t_start, t_start+1] lies in the future, consistent with predefined dampings. - // For t_start = 0: window [-1, 0] is in the past, so keep as is. - auto t_start_damping = - (FP(t_start) > FP(0)) ? SimulationTime(FP(t_start) + FP(1)) : t_start; + // For new NPIs (t_start > 0): shift t_start_damping by +1 so the smooth + // transition window [t_start, t_start+1] lies in the future, consistent with + // predefined dampings. For t_start = 0 (global t0): the window [-1, 0] + // is in the past and no shift is needed. + // For keep-alive renewals (is_expiry_renewal): do not shift t_start_damping. + // Since t_start == t_end_damping_old, the new start damping has the same + // (time, level, type) as the previous entry. + // Therefore, the contact matrix stays constant at the NPI level with no dip. + auto t_start_damping = (!is_expiry_renewal && FP(t_start) > FP(0)) + ? SimulationTime(FP(t_start) + FP(1)) + : t_start; auto t_end_damping = (FP(t_start) > FP(0)) ? SimulationTime(FP(t_end) + FP(1)) : t_end; implement_dynamic_npis(contact_patterns.get_cont_freq_mat(), exceeded_threshold->second, t_start_damping, t_end_damping, [](auto& g) { diff --git a/cpp/tests/test_dynamic_npis.cpp b/cpp/tests/test_dynamic_npis.cpp index 12d725f0b5..0b494999a7 100644 --- a/cpp/tests/test_dynamic_npis.cpp +++ b/cpp/tests/test_dynamic_npis.cpp @@ -657,9 +657,12 @@ TEST(DynamicNPIs, secir_backward_consistency_with_predefined) model.parameters.get>() = npis; // t0=1: threshold exceeded at t=1, delay=2 -> t_start=3, t_start_damping=4, - // duration=5 -> t_end=8, t_end_damping=9 + // duration=5 -> t_end=8, t_end_damping=9. + // Advance only to t=8 so that the keep-alive renewal (which acts at t=9 when t > t_end=8) + // never triggers. The damping list then stays identical to the predefined dampings for all + // time points, including t=9 (restore window [8,9] fully captured in the list). mio::osecir::Simulation> sim(model, 1.0); - sim.advance(10.0); + sim.advance(8.0); // Equivalent predefined dampings: place damping at t_start_damping=4 and restore at t_end_damping=9. // smoother_cosine uses window [t-1, t], so damping at t=4 gives window [3,4]. @@ -669,13 +672,59 @@ TEST(DynamicNPIs, secir_backward_consistency_with_predefined) cm_expected[0].add_damping(0.0, mio::DampingLevel(0), mio::DampingType(0), mio::SimulationTime(9.0)); auto const& cm_dynamic = sim.get_model().parameters.get>().get_cont_freq_mat(); - for (double t : {1.0, 2.0, 3.0, 3.5, 4.0, 5.0, 7.0, 8.0, 8.5, 9.0, 10.0}) { + for (double t : {1.0, 2.0, 3.0, 3.5, 4.0, 5.0, 7.0, 8.0, 8.5, 9.0}) { EXPECT_DOUBLE_EQ(cm_dynamic.get_matrix_at(mio::SimulationTime(t))(0, 0), cm_expected.get_matrix_at(mio::SimulationTime(t))(0, 0)) << "Mismatch at t=" << t; } } +TEST(DynamicNPIs, secir_expiry_keep_alive) +{ + // Verify keep-alive: when the NPI expires but the threshold is still exceeded, + // the NPI is renewed immediately with no delay gap and no dip between expiry and renewal. + // + // Timeline (delay=2, duration=5, t0=1): + // 1st NPI: t_start=3, t_start_damping=4, t_end=8, t_end_damping=9 + // At t=9 (expiry): keep-alive acts, t_start=9, t_start_damping=9 (no +1!), + // t_end=14, t_end_damping=15 -> list collapses to [(t=4,0.5),(t=15,0.0)] + mio::osecir::Model model(1); + model.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedSymptoms}] = 10; + model.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecir::InfectionState::Susceptible}, 100); + + mio::ContactMatrixGroup& cm = model.parameters.get>(); + cm[0] = mio::ContactMatrix(Eigen::MatrixXd::Constant(1, 1, 1.0)); + + mio::DynamicNPIs npis; + npis.set_threshold(0.05 * 50'000, {mio::DampingSampling{0.5, + mio::DampingLevel(0), + mio::DampingType(0), + mio::SimulationTime(0), + {0}, + Eigen::VectorXd::Ones(1)}}); + npis.set_duration(mio::SimulationTime(5.0)); + npis.set_base_value(50'000); + npis.set_implementation_delay(mio::SimulationTime(2.0)); + model.parameters.get>() = npis; + + // Stop at t=14 so the 2nd keep-alive (which would act at t=15) does not trigger. + mio::osecir::Simulation> sim(model, 1.0); + sim.advance(14.0); + + auto const& cm_dyn = sim.get_model().parameters.get>().get_cont_freq_mat(); + + // Damping list after keep-alive collapses to [(t=4, 0.5), (t=15, 0.0)]: + EXPECT_DOUBLE_EQ(cm_dyn.get_matrix_at(mio::SimulationTime(4.0))(0, 0), 0.5); // 1st NPI active + EXPECT_DOUBLE_EQ(cm_dyn.get_matrix_at(mio::SimulationTime(8.0))(0, 0), 0.5); // still active + // no dip at t=9 as keep-alive overwrote the restore entry, contact stays at 0.5. + // (Without this fix the restore would restore to 1.0 at t=9.) + EXPECT_DOUBLE_EQ(cm_dyn.get_matrix_at(mio::SimulationTime(9.0))(0, 0), 0.5); + EXPECT_DOUBLE_EQ(cm_dyn.get_matrix_at(mio::SimulationTime(10.0))(0, 0), 0.5); // still constant + EXPECT_DOUBLE_EQ(cm_dyn.get_matrix_at(mio::SimulationTime(14.0))(0, 0), 0.5); // still active + // Restore window [14, 15]: complete at t=15. + EXPECT_DOUBLE_EQ(cm_dyn.get_matrix_at(mio::SimulationTime(15.0))(0, 0), 1.0); +} + TEST(DynamicNPIs, secirvvs_threshold_safe) { mio::osecirvvs::Model model(1); From f6b5751a6eddd65335a01f33f386c20d1aec5810 Mon Sep 17 00:00:00 2001 From: HenrZu <69154294+HenrZu@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:47:52 +0200 Subject: [PATCH 19/19] updated testdata --- cpp/tests/data/results_osecirvvs.h5 | Bin 33312 -> 33312 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/cpp/tests/data/results_osecirvvs.h5 b/cpp/tests/data/results_osecirvvs.h5 index d8f196ea466c9e84cf3e7b7263d94ab98c9a1264..34707a720b3b2f38e5bb4570703f897d1835ec07 100644 GIT binary patch delta 12267 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 delta 12265 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�h`7N*6osUuqAm|mz*~T!k-*X>bxhQKZ{&`E~A?WcQ{x@ z;(zbyZ?Yo8^ai2p3k+ztjqWr7Pqevnl<+6UFmrDfYyWP>Y`fp7fMm_qM5k{Rpd7w& z(p|9)a*j8#sswj{(PvLy+PE&@E{Yp`py`axhO{Poul0lN;~bekRwIz29ui9sEn^^? zQI_cPx)Y53m(;4io(GPn*%B|PIpJnq@Sk^|jzPQv^JBN8EGW(`d6R0N4hfZNY6l)5 zM~{EU@J`ikfGg*_Q?#;6Kx6O5dfR&mQW9^l#~*8k(OA8x4WCl*xIp&uhGrR%?G$s; zK5GIazUb#xr!3pRIJH*k`&v7Af47}1f9i~i?W0WD(E!w4{3+^MJp#=xC9YqL#-YW& zJ=+&E^vhK5=8Kr2OL+CGERl%I`U?gjVRk&H;7}Vx(j5`JO3De6Z@rpv4@qF$B(NdKgcJ(HbZ%I-9!5Q{ z4BS?JQlht-N`lT~`*56&NB2P+mU3O?Yp?z|1)P3^8LvC1;JMMqs$8i$Fig8V`BJ$6 zRHH(Bt?~881wvl|Rh<3+U&;SN`eTgU%@b zRnFkGodF=4zDgDj8-->^E{CU`1=`jIW z@7%4J{JR19qmn-zBLwhmPdH6g(*s9&p6}^CYJjPKF@DRx7$5&H#+S5}`Q9dQ?|>sgO5a>O{6%Fdc_0ZT$=ekg@|ERVt&wp=birPG(@a`WEy(_h@!$N5@xTB77(e*3@Z2qTI^>r8%`~5h6j5`v&|6n- zL#BS~Ufwd+9s0mxF+i@WOMf=?Mvad*$J^L1?`9?91xrD4e5y#7j6!GXa4T zQA`b(TK+D+&{M}aHuY?F)#mLO^nAC=I+Q&KG8-~K&q}nx!^_vx7XwluC6;{U!*UUvuKp%lJy#5W z1uJ{DdFtG2gv%O=BsJE~Z^UymImSlG(;y8}K3 z$;Q{%e}}W3=D@dwcw4%7^ zf(%9b{BMN~f{ib~1nr42c;)=ftwwADS`KgIm_M~cB5Df*TKrg}dhJ8*!qt8dNo9GG z!_f_8&8hcGdg`#A!|cg&X$Poz+&FamZX+mOx!Q9In=RG+{}^BAUyMKbFUIHf9qf6% zL5`;S$VEa={(|_qlb_r#t^qOLZTpOKSlgP1R6*FO~(Ly&R6 z{VC=+i2NewAYc9&%)L75vS@PQ%IIWnGg}^z1ss!cur2_F;aLx&stb~n`ZBMaIY+wmt! z929-94T*@!f>7MI&kTCmz?R2f(RmvK?n!|TrCTn@b2Bupw+{O#;sq~Vy7U;_X+1Fg z^cpset2R&bw6{YCr#=RLJB3FDnnW)#f^R2m(zHsoVEWKzu&!jpCbC+YI$J6IO(4fV zK<~F-1?2f(ms_wi#Q9&0uk$a)U-=i~r&v!cscMs=&iEG6S=~*@HR*2EYnX={IlNDC zmpVartAJ7&UkmE|o^7h>wLr_4_r`ZS9~|KpCxO!Y1MoDdS~x{UgWe*qQ!QNv2>HAh z=^x7g`Z-_ZmYW4@BbEso<}OG=v#sjs$RPa9d+#}*GzLdPM~lHWk2Up+GRMEzg6t ze=&agzZk#wUyNUT=t|Npt9{7zw%Tlv!$!6zN~W@Y%|JzN75{KX8*r8<&~8yz!F@#o zbJ@#Ppv%7hVQMTFiiNB@)H5QWy2@4e4NVlV;aRRF+)9D;x*C(NycE!J*x2D|NCyqz zbl&ZJ;)0GeUB!D~86Lhn zym?2Xau@a{1A^SBj9_g~RrtekapYOEdWqCb5XD4`@0ES#MK({wS1M^)QLW~+bBDbc z2`KHV---L#j3}Wp^_!UXIAkB`WM~K*08dk!-&=hBVAEl;Wm~lh_*Sbxm)La}et+6U zyI>P08*h>IXNDp@_9y8sKGSgFTNKZ}<~)QfGHl?(m*7KBDR;c5KdQtvau&JhM=XcSJztsa*FBnQ&*mgt8y*smS za~7eAg5*`D#2U!!9f}_%S%+|gb*G}PP^9(a9$hOoIJ{QNdb>V%9>$w9XG*A+fjZ1N z;g_2~x*Vqcy3Z*96$l@cDxIT1(o!j5ov+Byk)bAyQ|Vjasi5liLU)ex>Z4fXr(jLgt2zZ=P{RFsL3A?=T%5`A#6h7 z?rd$n{2URSu!+Fbd-u)cGidD{tJ{lbC(%!jjlzOEL>s50gxhk3({$Jfh>|v6>w0!8 z9PHn;zKClDGJDO6O6F-O7LC@5bDsk=Rilq0mSMOAjB4b4h6Ef!>sIbEOxD_`* zSt-v>a()A7d#1addN2VcefBbu8rUZuRr^D?r9aJrJRiN%4Iv^TOL=d{oje2YoIkL? z__P3hT4T(Q53azeYPRk`yg#CBKla1WHUI^)cqK+Elb}EOq@`c4{{fyvH^#KKRfwl) zIr;X%Hq0st-lB+KgZuA4UNm_=NnCa2V68}C5k7EG08P;k9F?9uj{IEMBOASmTrNim z3oYhtrIKARGXElREu;q2&e_l09M5;b^kZP_J@)71iwUS*dz4z|Jq1tS8b2CC zMC9k(#rJA>23}TMi7O8*zz?lZqd3hK(2VG!aIp194%Jqw4G#m*Rq^Br_9uVA>(la~ z$CTblapN80wDNars0+V9h7mx zCMr7!HGFEp1{8dVJBW!RcIGImjH9{P%E658SFM|yw2`5`pR*L*vA;lj@ZEga!Y??> z+J25vyaWDzB}P{;SHgO(MW@!=O3)kknKhss07g4gI zB4q93GE06@i3{L$N$^K3)o z=JAMjlQ!55k?AQuRR)r&Mqk^T%OD)bU6tk32i*`~Zof4M)~1T}nZHJ0%QIMd4TMy+2K%RH!QM#br{DY_h#%*r2#{igW!f^w-REmKCa;|o zQ^y~;GwsE*+zvdbOqjA-zKR72JEj{raWf!>K)I+{on25Y_99$+n7t0q>Tf20pIL{| zsMHO&fM)oU>n~VfPz)j>XOH_0euMA1XAbq>>xTC${Eqo*eQ?`*UYc!g07!MHGcPz2 zk?Z@YSLntp9GPYI*Ed>(-NU9|wI;B=lX|%*(Goi+z1JV@mCC4El#g8*o?QN%W z0!w+)-?la}T8%+V2!Htg{u~q>zE(wfWdcU-Tcv6`5UKiVj6%AL@G`fHh4>9(k zM1;?|O7}f*mH=(s*|OxHu&41)U`3q(cFPn}Og7B?Q7QZE`6AN*RCdS{`A988`Wche z<~wr`-ljL0nKA?hDUmI~ebW$euEXQV=qN1n<^)%!bwFnP>3+VQd7Lzl-19=V8r*6x z|DMHzT3mYh;x%3VqbM$pY_IXE8o>xy+4Ah&0q`WHz>r7 z*gSbTTvvn>*2+7)oWh1afO$#A$%BZc)D6s&$k2!6yOY8Pu_->kl5)%BB$OP}qH*Ay zf}?ixPAZ|5&~YN}JVE?rDlG4)ajs(@&@M4PUMJnGgD>YR1s0PU!R%0%4`p-<91r8` zCQTwDx5|Pow!&E$YP`v$*RlvyG!GkOZm)u5`Pg9V1FSMx8r-@`05aLD|mfttH_ zOpbk-0M`=1vxgXaV6%u^=V!(kOr3a7AF0#_{0b?A_mbEY{jNpGZcEjZxuiE4 zXL)Di-j;PL&W(OrpwW~Cb^Tg8DbG)XC_5Pk0(?nOovz#;K@I6!5XkmHE+G4fYqV{C-JM2SOxl%Zm{WaB`MF zA3>f)L`o_xRekld;H$)R;K}$R{0+R9Y;Cs+b8mG<7EJunIx)XXLE^p>a4TG`5Ij`F?-LSy(44k(X@&d}yUmY76{F^0%wj!H0=bTZZ$s)B1 h<3&+KMEqV??pgQG!n=X