diff --git a/cpp/benchmarks/abm.cpp b/cpp/benchmarks/abm.cpp index 4d104caca5..06566c2296 100644 --- a/cpp/benchmarks/abm.cpp +++ b/cpp/benchmarks/abm.cpp @@ -61,9 +61,9 @@ mio::abm::Simulation make_simulation(size_t num_persons, std::initializer_list::get_instance()(prng, pct_mask_values); - person.set_mask_preferences({size_t(mio::abm::LocationType::Count), mask_value}); + auto pct_compliance_values = std::array{0.05 /*0*/, 0.2 /*0.25*/, 0.5 /*0.5*/, 0.2 /*0.75*/, 0.05 /*1*/}; + auto compliance_value = 0.25 * mio::DiscreteDistribution::get_instance()(prng, pct_compliance_values); + person.set_compliance(mio::abm::InterventionType::Mask, compliance_value); } //masks at locations @@ -71,9 +71,10 @@ mio::abm::Simulation make_simulation(size_t num_persons, std::initializer_list::get_instance()(model.get_rng()) < pct_require_mask; - loc.set_npi_active(requires_mask); + if (loc.get_type() != mio::abm::LocationType::Home && + mio::UniformDistribution::get_instance()(model.get_rng()) < pct_require_mask) { + loc.set_required_mask(mio::abm::MaskType::Community); + } } //testing schemes diff --git a/cpp/models/abm/intervention_type.h b/cpp/models/abm/intervention_type.h new file mode 100644 index 0000000000..0f40e85d0f --- /dev/null +++ b/cpp/models/abm/intervention_type.h @@ -0,0 +1,45 @@ +/* +* Copyright (C) 2020-2024 MEmilio +* +* Authors: Khoa Nguyen +* +* Contact: Martin J. Kuehn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#ifndef MIO_ABM_INTERVENTION_TYPE_H +#define MIO_ABM_INTERVENTION_TYPE_H + +#include + +namespace mio +{ +namespace abm +{ + +/** + * @brief Type of an Intervention. + */ +enum class InterventionType : std::uint32_t +{ + Mask, + Testing, + Isolation, + + Count +}; +} // namespace abm +} // namespace mio + +#endif diff --git a/cpp/models/abm/location.cpp b/cpp/models/abm/location.cpp index 81d803471e..e2cf026140 100644 --- a/cpp/models/abm/location.cpp +++ b/cpp/models/abm/location.cpp @@ -18,7 +18,7 @@ * limitations under the License. */ #include "abm/location_type.h" -#include "abm/mask_type.h" +#include "abm/intervention_type.h" #include "abm/location.h" #include "abm/random_events.h" @@ -32,8 +32,7 @@ Location::Location(LocationType loc_type, LocationId loc_id, size_t num_agegroup , m_id(loc_id) , m_parameters(num_agegroups) , m_cells(num_cells) - , m_required_mask(MaskType::Community) - , m_npi_active(false) + , m_required_mask(MaskType::None) { assert(num_cells > 0 && "Number of cells has to be larger than 0."); } diff --git a/cpp/models/abm/location.h b/cpp/models/abm/location.h index 1ecf45ac78..921dbd148f 100644 --- a/cpp/models/abm/location.h +++ b/cpp/models/abm/location.h @@ -214,22 +214,12 @@ class Location } /** - * @brief Get the information whether NPIs are active at this Location. - * If true requires e.g. Mask%s when entering a Location. - * @return True if NPIs are active at this Location. + * @brief Get the information whether masks are required to enter this Location. + * @return True if masks are required to enter this Location. */ - bool get_npi_active() const + bool is_mask_required() const { - return m_npi_active; - } - - /** - * @brief Activate or deactivate NPIs at this Location. - * @param[in] new_status Status of NPIs. - */ - void set_npi_active(bool new_status) - { - m_npi_active = new_status; + return m_required_mask != MaskType::None; } /** @@ -286,7 +276,6 @@ class Location LocalInfectionParameters m_parameters; ///< Infection parameters for the Location. std::vector m_cells{}; ///< A vector of all Cell%s that the Location is divided in. MaskType m_required_mask; ///< Least secure type of Mask that is needed to enter the Location. - bool m_npi_active; ///< If true requires e.g. Mask%s to enter the Location. GeographicalLocation m_geographical_location; ///< Geographical location (longitude and latitude) of the Location. }; diff --git a/cpp/models/abm/mask.cpp b/cpp/models/abm/mask.cpp index 0dcd2af772..7793302645 100644 --- a/cpp/models/abm/mask.cpp +++ b/cpp/models/abm/mask.cpp @@ -27,16 +27,21 @@ namespace mio { namespace abm { -Mask::Mask(MaskType type) +Mask::Mask(MaskType type, TimePoint t) : m_type(type) - , m_time_used(TimeSpan(0)) + , m_time_first_usage(t) { } -void Mask::change_mask(MaskType new_mask_type) +void Mask::change_mask(MaskType new_mask_type, TimePoint t) { - m_type = new_mask_type; - m_time_used = TimeSpan(0); + m_type = new_mask_type; + m_time_first_usage = t; +} + +const TimeSpan Mask::get_time_used(TimePoint curr_time) const +{ + return curr_time - m_time_first_usage; } } // namespace abm diff --git a/cpp/models/abm/mask.h b/cpp/models/abm/mask.h index 2b9048925b..fb05bba051 100644 --- a/cpp/models/abm/mask.h +++ b/cpp/models/abm/mask.h @@ -38,8 +38,9 @@ class Mask /** * @brief Construct a new Mask of a certain type. * @param[in] type The type of the Mask. + * @param[in] t The TimePoint of the Mask's initial usage. */ - Mask(MaskType type); + Mask(MaskType type, TimePoint t); /** * @brief Get the MaskType of this Mask. @@ -51,30 +52,20 @@ class Mask /** * @brief Get the length of time this Mask has been used. + * @param[in] curr_time The current TimePoint. */ - const TimeSpan& get_time_used() const - { - return m_time_used; - } - - /** - * @brief Increase the time this Mask was used by a timestep. - * @param[in] dt The length of the timestep. - */ - void increase_time_used(TimeSpan dt) - { - m_time_used += dt; - } + const TimeSpan get_time_used(TimePoint curr_time) const; /** * @brief Change the type of the Mask and reset the time it was used. * @param[in] new_mask_type The type of the new Mask. + * @param[in] t The TimePoint of mask change. */ - void change_mask(MaskType new_mask_type); + void change_mask(MaskType new_mask_type, TimePoint t); private: MaskType m_type; ///< Type of the Mask. - TimeSpan m_time_used; ///< Length of time the Mask has been used. + TimePoint m_time_first_usage; ///< TimePoint of the Mask's initial usage. }; } // namespace abm } // namespace mio diff --git a/cpp/models/abm/mask_type.h b/cpp/models/abm/mask_type.h index 8477e1d354..f52547482a 100644 --- a/cpp/models/abm/mask_type.h +++ b/cpp/models/abm/mask_type.h @@ -33,7 +33,8 @@ namespace abm */ enum class MaskType : std::uint32_t { - Community = 0, + None, + Community, Surgical, FFP2, diff --git a/cpp/models/abm/mobility_data.h b/cpp/models/abm/mobility_data.h index be536ac324..c81bc49332 100644 --- a/cpp/models/abm/mobility_data.h +++ b/cpp/models/abm/mobility_data.h @@ -33,7 +33,7 @@ namespace abm */ enum class TransportMode : uint32_t { - Bike = 0, + Bike, CarDriver, CarPassenger, PublicTransport, @@ -47,7 +47,7 @@ enum class TransportMode : uint32_t */ enum class ActivityType : uint32_t { - Workplace = 0, + Workplace, Education, Shopping, Leisure, diff --git a/cpp/models/abm/model.cpp b/cpp/models/abm/model.cpp index 3e690556c6..4c8ba1f7f5 100755 --- a/cpp/models/abm/model.cpp +++ b/cpp/models/abm/model.cpp @@ -20,6 +20,7 @@ #include "abm/model.h" #include "abm/location_id.h" #include "abm/location_type.h" +#include "abm/intervention_type.h" #include "abm/person.h" #include "abm/location.h" #include "abm/mobility_rules.h" @@ -98,25 +99,42 @@ void Model::perform_mobility(TimePoint t, TimeSpan dt) auto personal_rng = PersonalRandomNumberGenerator(m_rng, person); auto try_mobility_rule = [&](auto rule) -> bool { - //run mobility rule and check if change of location can actually happen + // run mobility rule and check if change of location can actually happen auto target_type = rule(personal_rng, person, t, dt, parameters); const Location& target_location = get_location(find_location(target_type, person_id)); const LocationId current_location = person.get_location(); - if (m_testing_strategy.run_strategy(personal_rng, person, target_location, t)) { - if (target_location.get_id() != current_location && - get_number_persons(target_location.get_id()) < target_location.get_capacity().persons) { - bool wears_mask = person.apply_mask_intervention(personal_rng, target_location); - if (wears_mask) { - change_location(person_id, target_location.get_id()); - } - return true; + + // the Person cannot move if they do not wear mask as required at targeted location + if (target_location.is_mask_required() && !person.is_compliant(personal_rng, InterventionType::Mask)) { + return false; + } + // the Person cannot move if the capacity of targeted Location is reached + if (target_location.get_id() == current_location || + get_number_persons(target_location.get_id()) >= target_location.get_capacity().persons) { + return false; + } + // the Person cannot move if the performed TestingStrategy is positive + if (!m_testing_strategy.run_strategy(personal_rng, person, target_location, t)) { + return false; + } + // update worn mask to target location's requirements + if (target_location.is_mask_required()) { + // if the current MaskProtection level is lower than required, the Person changes mask + if (parameters.get()[person.get_mask().get_type()] < + parameters.get()[target_location.get_required_mask()]) { + person.set_mask(target_location.get_required_mask(), t); } } - return false; + else { + person.set_mask(MaskType::None, t); + } + // all requirements are met, move to target location + change_location(person_id, target_location.get_id()); + return true; }; - //run mobility rules one after the other if the corresponding location type exists - //shortcutting of bool operators ensures the rules stop after the first rule is applied + // run mobility rules one after the other if the corresponding location type exists + // shortcutting of bool operators ensures the rules stop after the first rule is applied if (m_use_mobility_rules) { (has_locations({LocationType::Cemetery}) && try_mobility_rule(&get_buried)) || (has_locations({LocationType::Home}) && try_mobility_rule(&return_home_when_recovered)) || @@ -129,7 +147,7 @@ void Model::perform_mobility(TimePoint t, TimeSpan dt) (has_locations({LocationType::Home}) && try_mobility_rule(&go_to_quarantine)); } else { - //no daily routine mobility, just infection related + // no daily routine mobility, just infection related (has_locations({LocationType::Cemetery}) && try_mobility_rule(&get_buried)) || (has_locations({LocationType::Home}) && try_mobility_rule(&return_home_when_recovered)) || (has_locations({LocationType::Hospital}) && try_mobility_rule(&go_to_hospital)) || @@ -142,20 +160,37 @@ void Model::perform_mobility(TimePoint t, TimeSpan dt) bool weekend = t.is_weekend(); size_t num_trips = m_trip_list.num_trips(weekend); - if (num_trips != 0) { - while (m_trip_list.get_current_index() < num_trips && - m_trip_list.get_next_trip_time(weekend).seconds() < (t + dt).time_since_midnight().seconds()) { - auto& trip = m_trip_list.get_next_trip(weekend); - auto& person = get_person(trip.person_id); - auto personal_rng = PersonalRandomNumberGenerator(m_rng, person); - if (!person.is_in_quarantine(t, parameters) && person.get_infection_state(t) != InfectionState::Dead) { - auto& target_location = get_location(trip.destination); - if (m_testing_strategy.run_strategy(personal_rng, person, target_location, t)) { - person.apply_mask_intervention(personal_rng, target_location); - change_location(person.get_id(), target_location.get_id(), trip.trip_mode); - } + for (; m_trip_list.get_current_index() < num_trips && + m_trip_list.get_next_trip_time(weekend).seconds() < (t + dt).time_since_midnight().seconds(); + m_trip_list.increase_index()) { + auto& trip = m_trip_list.get_next_trip(weekend); + auto& person = get_person(trip.person_id); + auto personal_rng = PersonalRandomNumberGenerator(m_rng, person); + // skip the trip if the person is in quarantine or is dead + if (person.is_in_quarantine(t, parameters) || person.get_infection_state(t) == InfectionState::Dead) { + continue; + } + auto& target_location = get_location(trip.destination); + // skip the trip if the Person wears mask as required at targeted location + if (target_location.is_mask_required() && !person.is_compliant(personal_rng, InterventionType::Mask)) { + continue; + } + // skip the trip if the performed TestingStrategy is positive + if (!m_testing_strategy.run_strategy(personal_rng, person, target_location, t)) { + continue; + } + // all requirements are met, move to target location + change_location(person.get_id(), target_location.get_id(), trip.trip_mode); + // update worn mask to target location's requirements + if (target_location.is_mask_required()) { + // if the current MaskProtection level is lower than required, the Person changes mask + if (parameters.get()[person.get_mask().get_type()] < + parameters.get()[target_location.get_required_mask()]) { + person.set_mask(target_location.get_required_mask(), t); } - m_trip_list.increase_index(); + } + else { + person.set_mask(MaskType::None, t); } } if (((t).days() < std::floor((t + dt).days()))) { diff --git a/cpp/models/abm/parameters.h b/cpp/models/abm/parameters.h index f4ab449f45..524de6fd07 100644 --- a/cpp/models/abm/parameters.h +++ b/cpp/models/abm/parameters.h @@ -229,7 +229,12 @@ struct MaskProtection { using Type = CustomIndexArray, MaskType>; static Type get_default(AgeGroup /*size*/) { - return Type({MaskType::Count}, 1.); + Type defaut_value = Type(MaskType::Count, 0.0); + // Initial values according to http://dx.doi.org/10.15585/mmwr.mm7106e1 + defaut_value[MaskType::FFP2] = 0.83; + defaut_value[MaskType::Surgical] = 0.66; + defaut_value[MaskType::Community] = 0.56; + return defaut_value; } static std::string name() { diff --git a/cpp/models/abm/person.cpp b/cpp/models/abm/person.cpp index 6dbeb9cfdc..bbe3edfa25 100755 --- a/cpp/models/abm/person.cpp +++ b/cpp/models/abm/person.cpp @@ -36,12 +36,11 @@ Person::Person(mio::RandomNumberGenerator& rng, LocationType location_type, Loca : m_location(location_id) , m_location_type(location_type) , m_assigned_locations((uint32_t)LocationType::Count, LocationId::invalid_id()) - , m_quarantine_start(TimePoint(-(std::numeric_limits::max() / 2))) + , m_home_isolation_start(TimePoint(-(std::numeric_limits::max() / 2))) , m_age(age) , m_time_at_location(0) - , m_mask(Mask(MaskType::Community)) - , m_wears_mask(false) - , m_mask_compliance((uint32_t)LocationType::Count, 0.) + , m_mask(Mask(MaskType::None, TimePoint(-(std::numeric_limits::max() / 2)))) + , m_compliance((uint32_t)InterventionType::Count, 1.) , m_person_id(person_id) , m_cells{0} , m_last_transport_mode(TransportMode::Unknown) @@ -149,7 +148,7 @@ bool Person::goes_to_school(TimePoint t, const Parameters& params) const void Person::remove_quarantine() { - m_quarantine_start = TimePoint(-(std::numeric_limits::max() / 2)); + m_home_isolation_start = TimePoint(-(std::numeric_limits::max() / 2)); } bool Person::get_tested(PersonalRandomNumberGenerator& rng, TimePoint t, const TestParameters& params) @@ -158,7 +157,10 @@ bool Person::get_tested(PersonalRandomNumberGenerator& rng, TimePoint t, const T if (is_infected(t)) { // true positive if (random < params.sensitivity) { - m_quarantine_start = t; + // If the Person complies to isolation, start the quarantine. + if (is_compliant(rng, InterventionType::Isolation)) { + m_home_isolation_start = t; + } m_infections.back().set_detected(); return true; } @@ -174,7 +176,10 @@ bool Person::get_tested(PersonalRandomNumberGenerator& rng, TimePoint t, const T } // false positive else { - m_quarantine_start = t; + // If the Person complies to isolation, start the quarantine. + if (is_compliant(rng, InterventionType::Isolation)) { + m_home_isolation_start = t; + } return true; } } @@ -197,44 +202,13 @@ const std::vector& Person::get_cells() const ScalarType Person::get_mask_protective_factor(const Parameters& params) const { - if (m_wears_mask == false) { - return 0.; - } - else { - return params.get()[m_mask.get_type()]; - } + return params.get()[m_mask.get_type()]; } -bool Person::apply_mask_intervention(PersonalRandomNumberGenerator& rng, const Location& target) +bool Person::is_compliant(PersonalRandomNumberGenerator& rng, InterventionType intervention) const { - if (target.get_npi_active() == false) { - m_wears_mask = false; - if (get_mask_compliance(target.get_type()) > 0.) { - // draw if the person wears a mask even if not required - ScalarType wear_mask = UniformDistribution::get_instance()(rng); - if (wear_mask < get_mask_compliance(target.get_type())) { - m_wears_mask = true; - } - } - } - else { - m_wears_mask = true; - if (get_mask_compliance(target.get_type()) < 0.) { - // draw if a person refuses to wear the required mask - ScalarType wear_mask = UniformDistribution::get_instance()(rng, -1., 0.); - if (wear_mask > get_mask_compliance(target.get_type())) { - m_wears_mask = false; - } - return false; - } - if (m_wears_mask == true) { - - if (static_cast(m_mask.get_type()) < static_cast(target.get_required_mask())) { - m_mask.change_mask(target.get_required_mask()); - } - } - } - return true; + ScalarType compliance_check = UniformDistribution::get_instance()(rng); + return compliance_check <= get_compliance(intervention); } std::pair Person::get_latest_protection() const @@ -263,6 +237,11 @@ ScalarType Person::get_protection_factor(TimePoint t, VirusVariant virus, const t.days() - latest_protection.second.days()); } +void Person::set_mask(MaskType type, TimePoint t) +{ + m_mask.change_mask(type, t); +} + void Person::add_test_result(TimePoint t, TestType type, bool result) { // Remove outdated test results or replace the old result of the same type diff --git a/cpp/models/abm/person.h b/cpp/models/abm/person.h index 79994aadb1..b952361fe0 100755 --- a/cpp/models/abm/person.h +++ b/cpp/models/abm/person.h @@ -31,6 +31,7 @@ #include "abm/time.h" #include "abm/test_type.h" #include "abm/vaccine.h" +#include "abm/intervention_type.h" #include "abm/mask.h" #include "abm/mobility_data.h" #include "memilio/epidemiology/age_group.h" @@ -232,7 +233,7 @@ class Person */ bool is_in_quarantine(TimePoint t, const Parameters& params) const { - return t < m_quarantine_start + params.get(); + return t < m_home_isolation_start + params.get(); } /** @@ -290,51 +291,41 @@ class Person ScalarType get_mask_protective_factor(const Parameters& params) const; /** - * @brief For every #LocationType a Person has a compliance value between -1 and 1. - * -1 means that the Person never complies to any Mask duty at the given #LocationType. - * 1 means that the Person always wears a Mask a the #LocationType even if it is not required. - * @param[in] preferences The vector of Mask compliance values for all #LocationType%s. + * @brief For every #InterventionType a Person has a compliance value between 0 and 1. + * 0 means that the Person never complies to the Intervention. + * 1 means that the Person always complies to the Intervention. + * @param[in] intervention_type The #InterventionType. + * @param[in] value The compliance value. */ - void set_mask_preferences(std::vector preferences) + void set_compliance(InterventionType intervention_type, ScalarType value) { - m_mask_compliance = preferences; + m_compliance[static_cast(intervention_type)] = value; } /** - * @brief Get the Mask compliance of the Person for the current Location. - * @param[in] location The current Location of the Person. - * @return The probability that the Person does not comply to any Mask duty/wears a Mask even if it is not required. + * @brief Get the compliance of the Person for an Intervention. + * @param[in] intervention_type The #InterventionType. + * @return The probability that the Person complies to an Intervention. */ - ScalarType get_mask_compliance(LocationType location) const + ScalarType get_compliance(InterventionType intervention_type) const { - return m_mask_compliance[static_cast(location)]; + return m_compliance[static_cast(intervention_type)]; } /** - * @brief Checks whether the Person wears a Mask at the target Location. - * @param[inout] rng RandomNumberGenerator of the Person. - * @param[in] target The target Location. - * @return Whether a Person wears a Mask at the Location. - */ - bool apply_mask_intervention(PersonalRandomNumberGenerator& rng, const Location& target); - - /** - * @brief Decide if a Person is currently wearing a Mask. - * @param[in] wear_mask If true, the protection of the Mask is considered when computing the exposure rate. + * @brief Checks whether the Person complies an Intervention. + * @param[inout] rng PersonalRandomNumberGenerator of the Person. + * @param[in] intervention The #InterventionType. + * @return Checks whether the Person complies an Intervention. */ - void set_wear_mask(bool wear_mask) - { - m_wears_mask = wear_mask; - } + bool is_compliant(PersonalRandomNumberGenerator& rng, InterventionType intervention) const; /** - * @brief Get the information if the Person is currently wearing a Mask. - * @return True if the Person is currently wearing a Mask. + * @brief Change the mask to new type. + * @param[in] type The required #MaskType. + * @param[in] t The TimePoint of mask change. */ - bool get_wear_mask() const - { - return m_wears_mask; - } + void set_mask(MaskType type, TimePoint t); /** * @brief Get the multiplicative factor on how likely an #Infection is due to the immune system. @@ -442,7 +433,7 @@ class Person Person always visits the same Home or School etc. */ std::vector m_vaccinations; ///< Vector with all Vaccination%s the Person has received. std::vector m_infections; ///< Vector with all Infection%s the Person had. - TimePoint m_quarantine_start; ///< TimePoint when the Person started quarantine. + TimePoint m_home_isolation_start; ///< TimePoint when the Person started isolation at home. AgeGroup m_age; ///< AgeGroup the Person belongs to. TimeSpan m_time_at_location; ///< Time the Person has spent at its current Location so far. double m_random_workgroup; ///< Value to determine if the Person goes to work or works from home during lockdown. @@ -450,8 +441,8 @@ class Person double m_random_goto_work_hour; ///< Value to determine at what time the Person goes to work. double m_random_goto_school_hour; ///< Value to determine at what time the Person goes to school. Mask m_mask; ///< The Mask of the Person. - bool m_wears_mask = false; ///< Whether the Person currently wears a Mask. - std::vector m_mask_compliance; ///< Vector of Mask compliance values for all #LocationType%s. + std::vector + m_compliance; ///< Vector of compliance values for all #InterventionType%s. Values from 0 to 1. PersonId m_person_id; ///< Id of the Person. std::vector m_cells; ///< Vector with all Cell%s the Person visits at its current Location. mio::abm::TransportMode m_last_transport_mode; ///< TransportMode the Person used to get to its current Location. diff --git a/cpp/models/abm/test_type.h b/cpp/models/abm/test_type.h index ebe7b9b46e..ff9bb7cf09 100644 --- a/cpp/models/abm/test_type.h +++ b/cpp/models/abm/test_type.h @@ -33,7 +33,7 @@ namespace abm */ enum class TestType : std::uint32_t { - Generic = 0, + Generic, Antigen, PCR, diff --git a/cpp/models/abm/testing_strategy.cpp b/cpp/models/abm/testing_strategy.cpp index 8b52e36402..d4ee4ed5b2 100644 --- a/cpp/models/abm/testing_strategy.cpp +++ b/cpp/models/abm/testing_strategy.cpp @@ -181,8 +181,14 @@ bool TestingStrategy::run_strategy(PersonalRandomNumberGenerator& rng, Person& p if (location.get_type() == mio::abm::LocationType::Home) { return true; } - //lookup schemes for this specific location as well as the location type - //lookup in std::vector instead of std::map should be much faster unless for large numbers of schemes + + // If the Person does not comply to Testing where there is a testing scheme at the target location, it is not allowed to enter. + if (!person.is_compliant(rng, InterventionType::Testing)) { + return false; + } + + // Lookup schemes for this specific location as well as the location type + // Lookup in std::vector instead of std::map should be much faster unless for large numbers of schemes for (auto key : {std::make_pair(location.get_type(), location.get_id()), std::make_pair(location.get_type(), LocationId::invalid_id())}) { auto iter_schemes = @@ -190,8 +196,9 @@ bool TestingStrategy::run_strategy(PersonalRandomNumberGenerator& rng, Person& p return p.type == key.first && p.id == key.second; }); if (iter_schemes != m_location_to_schemes_map.end()) { - //apply all testing schemes that are found + // Apply all testing schemes that are found auto& schemes = iter_schemes->schemes; + // Whether the Person is allowed to enter or not depends on the test result(s). if (!std::all_of(schemes.begin(), schemes.end(), [&rng, &person, t](TestingScheme& ts) { return !ts.is_active() || ts.run_scheme(rng, person, t); })) { diff --git a/cpp/models/abm/vaccine.h b/cpp/models/abm/vaccine.h index 72da133516..0277415bc3 100644 --- a/cpp/models/abm/vaccine.h +++ b/cpp/models/abm/vaccine.h @@ -35,9 +35,9 @@ namespace abm */ enum class ExposureType : std::uint32_t { - NoProtection = 0, - NaturalInfection = 1, - GenericVaccine = 2, + NoProtection, + NaturalInfection, + GenericVaccine, Count //last!! }; diff --git a/cpp/models/abm/virus_variant.h b/cpp/models/abm/virus_variant.h index 18cb12d880..835693c1b1 100644 --- a/cpp/models/abm/virus_variant.h +++ b/cpp/models/abm/virus_variant.h @@ -36,7 +36,7 @@ namespace abm */ enum class VirusVariant : std::uint32_t { - Wildtype = 0, + Wildtype, Count // last!! }; diff --git a/cpp/tests/test_abm_location.cpp b/cpp/tests/test_abm_location.cpp index d67bb61605..aaab4b6137 100644 --- a/cpp/tests/test_abm_location.cpp +++ b/cpp/tests/test_abm_location.cpp @@ -28,7 +28,7 @@ TEST(TestLocation, initCell) { mio::abm::Location location(mio::abm::LocationType::PublicTransport, 0, 6, 2); - ASSERT_EQ(location.get_cells().size(), 2); + EXPECT_EQ(location.get_cells().size(), 2); } TEST(TestLocation, getId) @@ -103,9 +103,9 @@ TEST(TestLocation, computeSpacePerPersonRelative) home.set_capacity(0, 0, 2); // Capacity for Cell 3 auto cells = home.get_cells(); - ASSERT_EQ(cells[0].compute_space_per_person_relative(), 0.25); - ASSERT_EQ(cells[1].compute_space_per_person_relative(), 0.5); - ASSERT_EQ(cells[2].compute_space_per_person_relative(), 1.); + EXPECT_EQ(cells[0].compute_space_per_person_relative(), 0.25); + EXPECT_EQ(cells[1].compute_space_per_person_relative(), 0.5); + EXPECT_EQ(cells[2].compute_space_per_person_relative(), 1.); } TEST(TestLocation, interact) @@ -162,27 +162,17 @@ TEST(TestLocation, setCapacity) { mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); location.set_capacity(4, 200); - ASSERT_EQ(location.get_capacity().persons, (uint32_t)4); - ASSERT_EQ(location.get_capacity().volume, (uint32_t)200); + EXPECT_EQ(location.get_capacity().persons, (uint32_t)4); + EXPECT_EQ(location.get_capacity().volume, (uint32_t)200); } TEST(TestLocation, setRequiredMask) { mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); - ASSERT_EQ(location.get_required_mask(), mio::abm::MaskType::Community); + EXPECT_EQ(location.get_required_mask(), mio::abm::MaskType::None); location.set_required_mask(mio::abm::MaskType::FFP2); - ASSERT_EQ(location.get_required_mask(), mio::abm::MaskType::FFP2); -} - -TEST(TestLocation, setNPIActive) -{ - mio::abm::Location location(mio::abm::LocationType::Home, 0, num_age_groups); - location.set_npi_active(false); - ASSERT_FALSE(location.get_npi_active()); - - location.set_npi_active(true); - ASSERT_TRUE(location.get_npi_active()); + EXPECT_EQ(location.get_required_mask(), mio::abm::MaskType::FFP2); } TEST(TestLocation, getGeographicalLocation) @@ -191,5 +181,5 @@ TEST(TestLocation, getGeographicalLocation) mio::abm::GeographicalLocation geographical_location = {10.5100470359749, 52.2672785559812}; location.set_geographical_location(geographical_location); - ASSERT_EQ(location.get_geographical_location(), geographical_location); + EXPECT_EQ(location.get_geographical_location(), geographical_location); } diff --git a/cpp/tests/test_abm_masks.cpp b/cpp/tests/test_abm_masks.cpp index 5266d34f7a..5b06332799 100644 --- a/cpp/tests/test_abm_masks.cpp +++ b/cpp/tests/test_abm_masks.cpp @@ -23,35 +23,29 @@ TEST(TestMasks, init) { - auto mask = mio::abm::Mask(mio::abm::MaskType::Count); - ASSERT_EQ(mask.get_time_used().seconds(), 0.); + auto t = mio::abm::TimePoint(0); + auto mask = mio::abm::Mask(mio::abm::MaskType::Count, t); + EXPECT_EQ(mask.get_time_used(t).seconds(), 0.); } TEST(TestMasks, getType) { - auto mask = mio::abm::Mask(mio::abm::MaskType::Community); + auto t = mio::abm::TimePoint(0); + auto mask = mio::abm::Mask(mio::abm::MaskType::Community, t); auto type = mask.get_type(); - ASSERT_EQ(type, mio::abm::MaskType::Community); -} - -TEST(TestMasks, increaseTimeUsed) -{ - auto mask = mio::abm::Mask(mio::abm::MaskType::Community); - auto dt = mio::abm::hours(2); - mask.increase_time_used(dt); - ASSERT_EQ(mask.get_time_used(), mio::abm::hours(2)); + EXPECT_EQ(type, mio::abm::MaskType::Community); } TEST(TestMasks, changeMask) { - auto mask = mio::abm::Mask(mio::abm::MaskType::Community); - mask.increase_time_used(mio::abm::hours(2)); - ASSERT_EQ(mask.get_type(), mio::abm::MaskType::Community); - ASSERT_EQ(mask.get_time_used(), mio::abm::hours(2)); + auto t = mio::abm::TimePoint(2 * 60 * 60); + auto mask = mio::abm::Mask(mio::abm::MaskType::Community, mio::abm::TimePoint(0)); + EXPECT_EQ(mask.get_type(), mio::abm::MaskType::Community); + EXPECT_EQ(mask.get_time_used(t), mio::abm::hours(2)); - mask.change_mask(mio::abm::MaskType::Surgical); - ASSERT_EQ(mask.get_type(), mio::abm::MaskType::Surgical); - ASSERT_EQ(mask.get_time_used(), mio::abm::hours(0)); + mask.change_mask(mio::abm::MaskType::Surgical, t); + EXPECT_EQ(mask.get_type(), mio::abm::MaskType::Surgical); + EXPECT_EQ(mask.get_time_used(t), mio::abm::hours(0)); } TEST(TestMasks, maskProtection) @@ -75,14 +69,15 @@ TEST(TestMasks, maskProtection) //cache precomputed results auto dt = mio::abm::days(1); // susc_person1 wears a mask, default protection is 1 - susc_person1.set_wear_mask(true); + susc_person1.set_mask(mio::abm::MaskType::FFP2, t); // susc_person2 does not wear a mask - susc_person2.set_wear_mask(false); + susc_person2.set_mask(mio::abm::MaskType::None, t); //mock so person 2 will get infected ScopedMockDistribution>>> mock_exponential_dist; + EXPECT_CALL(mock_exponential_dist.get_mock(), invoke).WillOnce(testing::Return(1)); auto p1_rng = mio::abm::PersonalRandomNumberGenerator(rng, susc_person1); interact_testing(p1_rng, susc_person1, infection_location, {susc_person1, susc_person2, infected1}, t, dt, params); EXPECT_CALL(mock_exponential_dist.get_mock(), invoke).WillOnce(testing::Return(0.5)); @@ -90,6 +85,6 @@ TEST(TestMasks, maskProtection) interact_testing(p2_rng, susc_person2, infection_location, {susc_person1, susc_person2, infected1}, t, dt, params); // The person susc_person1 should have full protection against an infection, susc_person2 not - ASSERT_EQ(susc_person1.get_infection_state(t + dt), mio::abm::InfectionState::Susceptible); - ASSERT_EQ(susc_person2.get_infection_state(t + dt), mio::abm::InfectionState::Exposed); + EXPECT_EQ(susc_person1.get_infection_state(t + dt), mio::abm::InfectionState::Susceptible); + EXPECT_EQ(susc_person2.get_infection_state(t + dt), mio::abm::InfectionState::Exposed); } diff --git a/cpp/tests/test_abm_model.cpp b/cpp/tests/test_abm_model.cpp index 46232ba401..0cbdd6d810 100644 --- a/cpp/tests/test_abm_model.cpp +++ b/cpp/tests/test_abm_model.cpp @@ -432,24 +432,26 @@ TEST(TestModelTestingCriteria, testAddingAndUpdatingAndRunningTestingSchemes) auto& work = model.get_location(work_id); auto current_time = mio::abm::TimePoint(0); - auto pid = - add_test_person(model, home_id, age_group_15_to_34, mio::abm::InfectionState::InfectedSymptoms, current_time); + + auto test_time = mio::abm::minutes(30); + // Since tests are performed before current_time, the InfectionState of the Person has to take into account test_time + auto pid = add_test_person(model, home_id, age_group_15_to_34, mio::abm::InfectionState::InfectedSymptoms, + current_time - test_time); auto& person = model.get_person(pid); auto rng_person = mio::abm::PersonalRandomNumberGenerator(rng, person); person.set_assigned_location(mio::abm::LocationType::Home, home_id); person.set_assigned_location(mio::abm::LocationType::Work, work_id); + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(20); + const auto end_date = mio::abm::TimePoint(60 * 60 * 24 * 3); + const auto probability = 1.0; + const auto test_params_pcr = mio::abm::TestParameters{0.9, 0.99, test_time, mio::abm::TestType::Generic}; + auto testing_criteria = mio::abm::TestingCriteria(); testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedSymptoms); testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedNoSymptoms); - auto validity_period = mio::abm::days(1); - const auto start_date = mio::abm::TimePoint(20); - const auto end_date = mio::abm::TimePoint(60 * 60 * 24 * 3); - const auto probability = 1.0; - const auto test_params_pcr = - mio::abm::TestParameters{0.9, 0.99, mio::abm::minutes(30), mio::abm::TestType::Generic}; - auto testing_scheme = mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params_pcr, probability); @@ -559,3 +561,240 @@ TEST(TestModel, checkParameterConstraints) params.get() = mio::abm::TimePoint(-2); ASSERT_EQ(params.check_constraints(), true); } + +TEST(TestModel, mobilityRulesWithAppliedNPIs) +{ + using testing::Return; + // Test when the NPIs are applied, people can enter targeted location if they comply to the rules. + auto t = mio::abm::TimePoint(0) + mio::abm::hours(8); + auto dt = mio::abm::hours(1); + auto test_time = mio::abm::minutes(30); + auto model = mio::abm::Model(num_age_groups); + model.parameters + .get()[{mio::abm::VirusVariant::Wildtype, age_group_15_to_34}] = + 2 * dt.days(); + model.parameters.get().set_multiple({age_group_15_to_34, age_group_35_to_59}, true); + model.parameters.get()[age_group_5_to_14] = true; + + auto home_id = model.add_location(mio::abm::LocationType::Home); + auto work_id = model.add_location(mio::abm::LocationType::Work); + auto school_id = model.add_location(mio::abm::LocationType::School); + auto& work = model.get_location(work_id); + + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::AtLeast(16)) + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillRepeatedly(testing::Return(0.9)); // draw that satisfies all pre-conditions of NPIs + + // Since tests are performed before t, the InfectionState of all the Person have to take into account test_time + auto p_id_compliant_go_to_work = + add_test_person(model, home_id, age_group_15_to_34, mio::abm::InfectionState::Susceptible, t - test_time); + auto p_id_compliant_go_to_school = + add_test_person(model, home_id, age_group_5_to_14, mio::abm::InfectionState::Susceptible, t - test_time); + auto p_id_no_mask = + add_test_person(model, home_id, age_group_15_to_34, mio::abm::InfectionState::Susceptible, t - test_time); + auto p_id_no_test = add_test_person(model, home_id, age_group_15_to_34, + mio::abm::InfectionState::InfectedNoSymptoms, t - test_time); + auto p_id_no_isolation = add_test_person(model, home_id, age_group_15_to_34, + mio::abm::InfectionState::InfectedNoSymptoms, t - test_time); + + auto& p_compliant_go_to_work = model.get_person(p_id_compliant_go_to_work); + auto& p_compliant_go_to_school = model.get_person(p_id_compliant_go_to_school); + auto& p_no_mask = model.get_person(p_id_no_mask); + auto& p_no_test = model.get_person(p_id_no_test); + auto& p_no_isolation = model.get_person(p_id_no_isolation); + + p_compliant_go_to_work.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_compliant_go_to_work.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_compliant_go_to_work.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_compliant_go_to_school.set_assigned_location(mio::abm::LocationType::School, school_id); + p_compliant_go_to_school.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_no_mask.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_no_mask.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_no_test.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_no_test.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_no_isolation.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_no_isolation.set_assigned_location(mio::abm::LocationType::Home, home_id); + + auto testing_criteria = mio::abm::TestingCriteria( + {}, {mio::abm::InfectionState::InfectedSymptoms, mio::abm::InfectionState::InfectedNoSymptoms}); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(60 * 60 * 24 * 3); + const auto probability = 1; + const auto test_params = mio::abm::TestParameters{0.99, 0.99, test_time, mio::abm::TestType::Generic}; + const auto testing_frequency = mio::abm::days(1); + + auto testing_scheme = + mio::abm::TestingScheme(testing_criteria, testing_frequency, start_date, end_date, test_params, probability); + model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, testing_scheme); + + ScopedMockDistribution>>> + mock_exponential_dist; + EXPECT_CALL(mock_exponential_dist.get_mock(), invoke).WillRepeatedly(Return(1.)); + + work.set_required_mask(mio::abm::MaskType::FFP2); + p_no_mask.set_compliance(mio::abm::InterventionType::Mask, 0.4); + p_no_test.set_compliance(mio::abm::InterventionType::Testing, 0.4); + p_no_isolation.set_compliance(mio::abm::InterventionType::Isolation, 0.4); + + model.evolve(t, dt); + + // The complied person is allowed to be at work and wear the required mask + EXPECT_EQ(p_compliant_go_to_work.get_location(), work_id); + EXPECT_EQ(p_compliant_go_to_work.get_mask().get_type(), mio::abm::MaskType::FFP2); + + // The complied person is allowed to be at school and don't wear mask + EXPECT_EQ(p_compliant_go_to_school.get_location(), school_id); + EXPECT_EQ(p_compliant_go_to_school.get_mask().get_type(), mio::abm::MaskType::None); + + // The person, who does not wear mask, is not allowed to be in location + EXPECT_EQ(p_no_mask.get_mask().get_type(), mio::abm::MaskType::None); + EXPECT_NE(p_no_mask.get_location(), work_id); + + // The person, who does not want test, is not allowed to be in location + EXPECT_NE(p_no_test.get_location(), work_id); + + // The person does not want to isolate when the test is positive + EXPECT_FALSE(p_no_isolation.is_in_quarantine(t, model.parameters)); +} + +TEST(TestModel, mobilityTripWithAppliedNPIs) +{ + using testing::Return; + // Test when the NPIs are applied, people can enter targeted location if they comply to the rules. + auto t = mio::abm::TimePoint(0) + mio::abm::hours(8); + auto dt = mio::abm::hours(1); + auto test_time = mio::abm::minutes(30); + auto model = mio::abm::Model(num_age_groups); + model.parameters + .get()[{mio::abm::VirusVariant::Wildtype, age_group_15_to_34}] = + 2 * dt.days(); + model.parameters.get().set_multiple({age_group_15_to_34, age_group_35_to_59}, true); + model.parameters.get()[age_group_5_to_14] = true; + + auto home_id = model.add_location(mio::abm::LocationType::Home); + auto work_id = model.add_location(mio::abm::LocationType::Work); + auto school_id = model.add_location(mio::abm::LocationType::School); + auto& work = model.get_location(work_id); + + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::AtLeast(16)) + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillOnce(testing::Return(0.8)) // draw random work group + .WillOnce(testing::Return(0.8)) // draw random school group + .WillOnce(testing::Return(0.8)) // draw random work hour + .WillOnce(testing::Return(0.8)) // draw random school hour + .WillRepeatedly(testing::Return(0.9)); // draw that satisfies all pre-conditions of NPIs + + // Since tests are performed before t, the InfectionState of all the Person have to take into account test_time + auto p_id_compliant_go_to_work = + add_test_person(model, home_id, age_group_15_to_34, mio::abm::InfectionState::Susceptible, t - test_time); + auto p_id_compliant_go_to_school = + add_test_person(model, home_id, age_group_5_to_14, mio::abm::InfectionState::Susceptible, t - test_time); + auto p_id_no_mask = + add_test_person(model, home_id, age_group_15_to_34, mio::abm::InfectionState::Susceptible, t - test_time); + auto p_id_no_test = add_test_person(model, home_id, age_group_15_to_34, + mio::abm::InfectionState::InfectedNoSymptoms, t - test_time); + auto p_id_no_isolation = add_test_person(model, home_id, age_group_15_to_34, + mio::abm::InfectionState::InfectedNoSymptoms, t - test_time); + + auto& p_compliant_go_to_work = model.get_person(p_id_compliant_go_to_work); + auto& p_compliant_go_to_school = model.get_person(p_id_compliant_go_to_school); + auto& p_no_mask = model.get_person(p_id_no_mask); + auto& p_no_test = model.get_person(p_id_no_test); + auto& p_no_isolation = model.get_person(p_id_no_isolation); + + p_compliant_go_to_work.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_compliant_go_to_work.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_compliant_go_to_work.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_compliant_go_to_school.set_assigned_location(mio::abm::LocationType::School, school_id); + p_compliant_go_to_school.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_no_mask.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_no_mask.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_no_test.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_no_test.set_assigned_location(mio::abm::LocationType::Home, home_id); + p_no_isolation.set_assigned_location(mio::abm::LocationType::Work, work_id); + p_no_isolation.set_assigned_location(mio::abm::LocationType::Home, home_id); + + auto testing_criteria = mio::abm::TestingCriteria( + {}, {mio::abm::InfectionState::InfectedSymptoms, mio::abm::InfectionState::InfectedNoSymptoms}); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(60 * 60 * 24 * 3); + const auto probability = 1; + const auto test_params = mio::abm::TestParameters{0.99, 0.99, test_time, mio::abm::TestType::Generic}; + const auto testing_frequency = mio::abm::days(1); + + auto testing_scheme = + mio::abm::TestingScheme(testing_criteria, testing_frequency, start_date, end_date, test_params, probability); + model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, testing_scheme); + + ScopedMockDistribution>>> + mock_exponential_dist; + EXPECT_CALL(mock_exponential_dist.get_mock(), invoke).WillRepeatedly(Return(1.)); + + work.set_required_mask(mio::abm::MaskType::FFP2); + p_no_mask.set_compliance(mio::abm::InterventionType::Mask, 0.4); + p_no_test.set_compliance(mio::abm::InterventionType::Testing, 0.4); + p_no_isolation.set_compliance(mio::abm::InterventionType::Isolation, 0.4); + + // Using trip list + mio::abm::TripList& trip_list = model.get_trip_list(); + mio::abm::Trip trip1(p_compliant_go_to_work.get_id(), t, work_id, home_id); + mio::abm::Trip trip2(p_compliant_go_to_school.get_id(), t, school_id, home_id); + mio::abm::Trip trip3(p_no_mask.get_id(), t, work_id, home_id); + mio::abm::Trip trip4(p_no_test.get_id(), t, work_id, home_id); + mio::abm::Trip trip5(p_no_isolation.get_id(), t, work_id, home_id); + trip_list.add_trip(trip1); + trip_list.add_trip(trip2); + trip_list.add_trip(trip3); + trip_list.add_trip(trip4); + trip_list.add_trip(trip5); + model.use_mobility_rules(false); + model.evolve(t, dt); + + // The complied person is allowed to be at work and wear the required mask + EXPECT_EQ(p_compliant_go_to_work.get_location(), work_id); + EXPECT_EQ(p_compliant_go_to_work.get_mask().get_type(), mio::abm::MaskType::FFP2); + + // The complied person is allowed to be at school and don't wear mask + EXPECT_EQ(p_compliant_go_to_school.get_location(), school_id); + EXPECT_EQ(p_compliant_go_to_school.get_mask().get_type(), mio::abm::MaskType::None); + + // The person, who does not wear mask, is not allowed to be in location + EXPECT_EQ(p_no_mask.get_mask().get_type(), mio::abm::MaskType::None); + EXPECT_NE(p_no_mask.get_location(), work_id); + + // The person, who does not want test, is not allowed to be in location + EXPECT_NE(p_no_test.get_location(), work_id); + + // The person does not want to isolate when the test is positive + EXPECT_FALSE(p_no_isolation.is_in_quarantine(t, model.parameters)); +} diff --git a/cpp/tests/test_abm_person.cpp b/cpp/tests/test_abm_person.cpp index 0356d35384..f202fa2c2c 100644 --- a/cpp/tests/test_abm_person.cpp +++ b/cpp/tests/test_abm_person.cpp @@ -152,11 +152,13 @@ TEST(TestPerson, get_tested) ScopedMockDistribution>>> mock_uniform_dist_pcr; EXPECT_CALL(mock_uniform_dist_pcr.get_mock(), invoke) - .Times(4) - .WillOnce(Return(0.4)) - .WillOnce(Return(0.95)) - .WillOnce(Return(0.6)) - .WillOnce(Return(0.999)); + .Times(6) + .WillOnce(Return(0.4)) // Draw for agent's test true positive + .WillOnce(Return(0.8)) // Draw for is_compliant() return true + .WillOnce(Return(0.95)) // Draw for agent's test false negative + .WillOnce(Return(0.6)) // Draw for agent's test true negative + .WillOnce(Return(0.999)) // Draw for agent's test false negative + .WillOnce(Return(0.8)); // Draw for is_compliant() return true EXPECT_EQ(infected.get_tested(rng_infected, t, pcr_parameters), true); EXPECT_EQ(infected.is_in_quarantine(t, params), true); infected.remove_quarantine(); @@ -171,11 +173,13 @@ TEST(TestPerson, get_tested) ScopedMockDistribution>>> mock_uniform_dist_antigen; EXPECT_CALL(mock_uniform_dist_antigen.get_mock(), invoke) - .Times(4) - .WillOnce(Return(0.4)) - .WillOnce(Return(0.95)) - .WillOnce(Return(0.6)) - .WillOnce(Return(0.999)); + .Times(6) + .WillOnce(Return(0.4)) // Draw for agent's test true positive + .WillOnce(Return(0.8)) // Draw for is_compliant() return true + .WillOnce(Return(0.95)) // Draw for agent's test false negative + .WillOnce(Return(0.6)) // Draw for agent's test true negative + .WillOnce(Return(0.999)) // Draw for agent's test false negative + .WillOnce(Return(0.8)); // Draw for is_compliant() return true EXPECT_EQ(infected.get_tested(rng_infected, t, antigen_parameters), true); EXPECT_EQ(infected.get_tested(rng_infected, t, antigen_parameters), false); EXPECT_EQ(susceptible.get_tested(rng_suscetible, t, antigen_parameters), false); @@ -210,78 +214,41 @@ TEST(TestPerson, interact) EXPECT_EQ(person.get_time_at_location(), dt); } -TEST(TestPerson, applyMaskIntervention) -{ - auto rng = mio::RandomNumberGenerator(); - - mio::abm::Location home(mio::abm::LocationType::Home, 0, num_age_groups); - mio::abm::Location target(mio::abm::LocationType::Work, 0, num_age_groups); - auto person = make_test_person(home); - person.get_mask().change_mask(mio::abm::MaskType::Community); - auto rng_person = mio::abm::PersonalRandomNumberGenerator(rng, person); - - target.set_npi_active(false); - person.apply_mask_intervention(rng_person, target); - ASSERT_FALSE(person.get_wear_mask()); - - auto preferences = std::vector((uint32_t)mio::abm::LocationType::Count, 1.); - person.set_mask_preferences(preferences); - person.apply_mask_intervention(rng_person, target); - - ASSERT_TRUE(person.get_wear_mask()); - - target.set_npi_active(true); - target.set_required_mask(mio::abm::MaskType::Surgical); - preferences = std::vector((uint32_t)mio::abm::LocationType::Count, 0.); - person.set_mask_preferences(preferences); - person.apply_mask_intervention(rng_person, target); - - ASSERT_EQ(person.get_mask().get_type(), mio::abm::MaskType::Surgical); - ASSERT_TRUE(person.get_wear_mask()); - - preferences = std::vector((uint32_t)mio::abm::LocationType::Count, -1.); - person.set_mask_preferences(preferences); - person.apply_mask_intervention(rng_person, target); - - ASSERT_FALSE(person.get_wear_mask()); -} - TEST(TestPerson, setWearMask) { + auto t = mio::abm::TimePoint(0); mio::abm::Location location(mio::abm::LocationType::School, 0, num_age_groups); auto person = make_test_person(location); - person.set_wear_mask(false); - ASSERT_FALSE(person.get_wear_mask()); + person.set_mask(mio::abm::MaskType::None, t); + EXPECT_EQ(person.get_mask().get_type(), mio::abm::MaskType::None); - person.set_wear_mask(true); - ASSERT_TRUE(person.get_wear_mask()); + person.set_mask(mio::abm::MaskType::Community, t); + EXPECT_NE(person.get_mask().get_type(), mio::abm::MaskType::None); } TEST(TestPerson, getMaskProtectiveFactor) { + auto t = mio::abm::TimePoint(0); mio::abm::Location location(mio::abm::LocationType::School, 0, 6); auto person_community = make_test_person(location); - person_community.get_mask().change_mask(mio::abm::MaskType::Community); - person_community.set_wear_mask(true); + person_community.set_mask(mio::abm::MaskType::Community, t); auto person_surgical = make_test_person(location); - person_surgical.get_mask().change_mask(mio::abm::MaskType::Surgical); - person_surgical.set_wear_mask(true); + person_surgical.set_mask(mio::abm::MaskType::Surgical, t); auto person_ffp2 = make_test_person(location); - person_ffp2.get_mask().change_mask(mio::abm::MaskType::FFP2); - person_ffp2.set_wear_mask(true); + person_ffp2.set_mask(mio::abm::MaskType::FFP2, t); auto person_without = make_test_person(location); - person_without.set_wear_mask(false); + person_without.set_mask(mio::abm::MaskType::None, t); mio::abm::Parameters params = mio::abm::Parameters(num_age_groups); params.get()[{mio::abm::MaskType::Community}] = 0.5; params.get()[{mio::abm::MaskType::Surgical}] = 0.8; params.get()[{mio::abm::MaskType::FFP2}] = 0.9; - ASSERT_EQ(person_community.get_mask_protective_factor(params), 0.5); - ASSERT_EQ(person_surgical.get_mask_protective_factor(params), 0.8); - ASSERT_EQ(person_ffp2.get_mask_protective_factor(params), 0.9); - ASSERT_EQ(person_without.get_mask_protective_factor(params), 0.); + EXPECT_EQ(person_community.get_mask_protective_factor(params), 0.5); + EXPECT_EQ(person_surgical.get_mask_protective_factor(params), 0.8); + EXPECT_EQ(person_ffp2.get_mask_protective_factor(params), 0.9); + EXPECT_EQ(person_without.get_mask_protective_factor(params), 0.); } TEST(TestPerson, getLatestProtection) @@ -295,15 +262,15 @@ TEST(TestPerson, getLatestProtection) auto t = mio::abm::TimePoint(0); person.add_new_vaccination(mio::abm::ExposureType::GenericVaccine, t); auto latest_protection = person.get_latest_protection(); - ASSERT_EQ(latest_protection.first, mio::abm::ExposureType::GenericVaccine); - ASSERT_EQ(latest_protection.second.days(), t.days()); + EXPECT_EQ(latest_protection.first, mio::abm::ExposureType::GenericVaccine); + EXPECT_EQ(latest_protection.second.days(), t.days()); t = mio::abm::TimePoint(40 * 24 * 60 * 60); person.add_new_infection(mio::abm::Infection(prng, static_cast(0), age_group_15_to_34, params, t, mio::abm::InfectionState::Exposed)); latest_protection = person.get_latest_protection(); - ASSERT_EQ(latest_protection.first, mio::abm::ExposureType::NaturalInfection); - ASSERT_EQ(latest_protection.second.days(), t.days()); + EXPECT_EQ(latest_protection.first, mio::abm::ExposureType::NaturalInfection); + EXPECT_EQ(latest_protection.second.days(), t.days()); } TEST(Person, rng) @@ -340,3 +307,49 @@ TEST(Person, addAndGetTestResult) person.add_test_result(t, mio::abm::TestType::Generic, true); EXPECT_TRUE(person.get_test_result(mio::abm::TestType::Generic).result); } + +TEST(TestPerson, isCompliant) +{ + using testing::Return; + + // Initialize the random number generator + auto rng = mio::RandomNumberGenerator(); + + // Create locations + mio::abm::Location home(mio::abm::LocationType::Home, 0, num_age_groups); + + // Create test person and associated random number generator + auto person = make_test_person(home); + auto rng_person = mio::abm::PersonalRandomNumberGenerator(rng, person); + + // Test cases with a complete truth table for compliance levels + struct TestCase { + mio::abm::InterventionType intervention_type; + double compliance_level; + bool expected_compliance; + }; + + std::vector test_cases = { + {mio::abm::InterventionType::Mask, 1.0, true}, {mio::abm::InterventionType::Mask, 0.4, false}, + {mio::abm::InterventionType::Mask, 0.2, false}, {mio::abm::InterventionType::Mask, 0.9, true}, + {mio::abm::InterventionType::Mask, 0.5, false}, {mio::abm::InterventionType::Mask, 0.1, false}, + {mio::abm::InterventionType::Testing, 1.0, true}, {mio::abm::InterventionType::Testing, 0.4, false}, + {mio::abm::InterventionType::Testing, 0.2, false}, {mio::abm::InterventionType::Testing, 0.9, true}, + {mio::abm::InterventionType::Testing, 0.5, false}, {mio::abm::InterventionType::Testing, 0.1, false}, + {mio::abm::InterventionType::Isolation, 1.0, true}, {mio::abm::InterventionType::Isolation, 0.4, false}, + {mio::abm::InterventionType::Isolation, 0.2, false}, {mio::abm::InterventionType::Isolation, 0.9, true}, + {mio::abm::InterventionType::Isolation, 0.5, false}, {mio::abm::InterventionType::Isolation, 0.1, false}, + }; + + // Return mock values for all tests. + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke).Times(18).WillRepeatedly(Return(0.8)); + + for (const auto& test_case : test_cases) { + // Set the compliance level for the person + person.set_compliance(test_case.intervention_type, test_case.compliance_level); + + // Check if the person is compliant + EXPECT_EQ(person.is_compliant(rng_person, test_case.intervention_type), test_case.expected_compliance); + } +} diff --git a/cpp/tests/test_abm_testing_strategy.cpp b/cpp/tests/test_abm_testing_strategy.cpp index 73b16b496e..4f7cf5178c 100644 --- a/cpp/tests/test_abm_testing_strategy.cpp +++ b/cpp/tests/test_abm_testing_strategy.cpp @@ -29,27 +29,27 @@ TEST(TestTestingCriteria, addRemoveAndEvaluateTestCriteria) mio::abm::TimePoint t{0}; auto testing_criteria = mio::abm::TestingCriteria(); - ASSERT_EQ(testing_criteria.evaluate(person, t), true); + EXPECT_EQ(testing_criteria.evaluate(person, t), true); testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedSymptoms); testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedNoSymptoms); testing_criteria.add_age_group(age_group_35_to_59); - ASSERT_EQ(testing_criteria.evaluate(person, t), + EXPECT_EQ(testing_criteria.evaluate(person, t), false); // now it isn't empty and get's evaluated against age group testing_criteria.remove_age_group(age_group_35_to_59); - ASSERT_EQ(testing_criteria.evaluate(person, t), true); + EXPECT_EQ(testing_criteria.evaluate(person, t), true); testing_criteria.remove_infection_state(mio::abm::InfectionState::InfectedSymptoms); - ASSERT_EQ(testing_criteria.evaluate(person, t), false); + EXPECT_EQ(testing_criteria.evaluate(person, t), false); testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedSymptoms); auto testing_criteria_manual = mio::abm::TestingCriteria( std::vector({age_group_15_to_34}), std::vector({mio::abm::InfectionState::InfectedNoSymptoms})); - ASSERT_EQ(testing_criteria == testing_criteria_manual, false); + EXPECT_EQ(testing_criteria == testing_criteria_manual, false); testing_criteria_manual.add_infection_state(mio::abm::InfectionState::InfectedSymptoms); testing_criteria_manual.remove_age_group(age_group_15_to_34); - ASSERT_EQ(testing_criteria == testing_criteria_manual, true); + EXPECT_EQ(testing_criteria == testing_criteria_manual, true); } TEST(TestTestingScheme, runScheme) @@ -76,11 +76,11 @@ TEST(TestTestingScheme, runScheme) auto testing_scheme1 = mio::abm::TestingScheme(testing_criteria1, validity_period, start_date, end_date, test_params_pcr, probability); - ASSERT_EQ(testing_scheme1.is_active(), false); + EXPECT_EQ(testing_scheme1.is_active(), false); testing_scheme1.update_activity_status(mio::abm::TimePoint(10)); - ASSERT_EQ(testing_scheme1.is_active(), true); + EXPECT_EQ(testing_scheme1.is_active(), true); testing_scheme1.update_activity_status(mio::abm::TimePoint(60 * 60 * 24 * 3 + 200)); - ASSERT_EQ(testing_scheme1.is_active(), false); + EXPECT_EQ(testing_scheme1.is_active(), false); testing_scheme1.update_activity_status(mio::abm::TimePoint(0)); std::vector test_infection_states2 = {mio::abm::InfectionState::Recovered}; @@ -90,24 +90,28 @@ TEST(TestTestingScheme, runScheme) mio::abm::Location loc_home(mio::abm::LocationType::Home, 0, num_age_groups); mio::abm::Location loc_work(mio::abm::LocationType::Work, 0, num_age_groups); - auto person1 = make_test_person(loc_home, age_group_15_to_34, mio::abm::InfectionState::InfectedNoSymptoms); + // Since tests are performed before start_date, the InfectionState of all the Person have to take into account the test's required_time + auto person1 = make_test_person(loc_home, age_group_15_to_34, mio::abm::InfectionState::InfectedNoSymptoms, + start_date - test_params_pcr.required_time); auto rng_person1 = mio::abm::PersonalRandomNumberGenerator(rng, person1); - auto person2 = make_test_person(loc_home, age_group_15_to_34, mio::abm::InfectionState::Recovered); + auto person2 = make_test_person(loc_home, age_group_15_to_34, mio::abm::InfectionState::Recovered, + start_date - test_params_pcr.required_time); auto rng_person2 = mio::abm::PersonalRandomNumberGenerator(rng, person2); ScopedMockDistribution>>> mock_uniform_dist; EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) - .Times(testing::Exactly(4)) + .Times(testing::Exactly(5)) .WillOnce(testing::Return(0.7)) // Person 1 got test - .WillOnce(testing::Return(0.5)) // Person 1 tested positive and cannot enter + .WillOnce(testing::Return(0.7)) // Test is positive + .WillOnce(testing::Return(0.5)) // Person 1 complies to isolation .WillOnce(testing::Return(0.7)) // Person 2 got test .WillOnce(testing::Return(0.5)); // Person 2 tested negative and can enter - ASSERT_EQ(testing_scheme1.run_scheme(rng_person1, person1, start_date + test_params_pcr.required_time), + EXPECT_EQ(testing_scheme1.run_scheme(rng_person1, person1, start_date), false); // Person tests and tests positive - ASSERT_EQ(testing_scheme2.run_scheme(rng_person2, person2, start_date + test_params_pcr.required_time), + EXPECT_EQ(testing_scheme2.run_scheme(rng_person2, person2, start_date), true); // Person tests and tests negative - ASSERT_EQ(testing_scheme1.run_scheme(rng_person1, person1, start_date + test_params_pcr.required_time), + EXPECT_EQ(testing_scheme1.run_scheme(rng_person1, person1, start_date), false); // Person doesn't test but used the last result (false to enter) } @@ -130,27 +134,36 @@ TEST(TestTestingScheme, initAndRunTestingStrategy) auto testing_criteria2 = mio::abm::TestingCriteria({}, test_infection_states2); auto testing_scheme2 = mio::abm::TestingScheme(testing_criteria2, validity_period, start_date, end_date, test_params_pcr, probability); - + testing_scheme2.update_activity_status(mio::abm::TimePoint(0)); mio::abm::Location loc_work(mio::abm::LocationType::Work, 0); - auto person1 = make_test_person(loc_work, age_group_15_to_34, mio::abm::InfectionState::InfectedNoSymptoms); + // Since tests are performed before start_date, the InfectionState of all the Person have to take into account the test's required_time + auto person1 = make_test_person(loc_work, age_group_15_to_34, mio::abm::InfectionState::InfectedNoSymptoms, + start_date - test_params_pcr.required_time); auto rng_person1 = mio::abm::PersonalRandomNumberGenerator(rng, person1); - auto person2 = make_test_person(loc_work, age_group_15_to_34, mio::abm::InfectionState::Recovered); + auto person2 = make_test_person(loc_work, age_group_15_to_34, mio::abm::InfectionState::Recovered, + start_date - test_params_pcr.required_time); auto rng_person2 = mio::abm::PersonalRandomNumberGenerator(rng, person2); ScopedMockDistribution>>> mock_uniform_dist; EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) - .Times(testing::Exactly(2)) //only sampled twice, testing criteria don't apply to third test strategy run. - .WillOnce(testing::Return(0.7)) - .WillOnce(testing::Return(0.5)); + .Times(testing::Exactly((8))) + .WillOnce(testing::Return(0.7)) // Person 1 complies to testing + .WillOnce(testing::Return(0.7)) // Person 1 is tested for scheme 1 + .WillOnce(testing::Return(0.7)) // Test of Person 1 is positive + .WillOnce(testing::Return(0.7)) // Person 1 complies to isolation + .WillOnce(testing::Return(0.7)) // Person 2 complies to testing + .WillOnce(testing::Return(0.7)) // Person 2 is tested for scheme 2 + .WillOnce(testing::Return(0.5)) // Test of Person 2 is negative + .WillOnce(testing::Return(0.7)); // Person 1 complies to testing mio::abm::TestingStrategy test_strategy = mio::abm::TestingStrategy(std::vector{}); test_strategy.add_testing_scheme(mio::abm::LocationType::Work, testing_scheme1); test_strategy.add_testing_scheme(mio::abm::LocationType::Work, testing_scheme2); - ASSERT_EQ(test_strategy.run_strategy(rng_person1, person1, loc_work, start_date + test_params_pcr.required_time), + EXPECT_EQ(test_strategy.run_strategy(rng_person1, person1, loc_work, start_date), false); // Person tests and tests positive - ASSERT_EQ(test_strategy.run_strategy(rng_person2, person2, loc_work, start_date + test_params_pcr.required_time), + EXPECT_EQ(test_strategy.run_strategy(rng_person2, person2, loc_work, start_date), true); // Person tests and tests negative - ASSERT_EQ(test_strategy.run_strategy(rng_person1, person1, loc_work, start_date + test_params_pcr.required_time), + EXPECT_EQ(test_strategy.run_strategy(rng_person1, person1, loc_work, start_date), false); // Person doesn't test but used the last result (false to enter) }