diff --git a/cpp/benchmarks/abm.cpp b/cpp/benchmarks/abm.cpp index a9a179ae1d..9f8e1f2f4d 100644 --- a/cpp/benchmarks/abm.cpp +++ b/cpp/benchmarks/abm.cpp @@ -111,20 +111,9 @@ mio::abm::Simulation<> make_simulation(size_t num_persons, std::initializer_list return mio::abm::TestingCriteria(random_ages, random_states); }; - model.get_testing_strategy().add_testing_scheme( - mio::abm::LocationType::School, - mio::abm::TestingScheme(random_criteria(), mio::abm::days(3), mio::abm::TimePoint(0), - mio::abm::TimePoint(0) + mio::abm::days(10), {}, 0.5)); - model.get_testing_strategy().add_testing_scheme( - mio::abm::LocationType::Work, - mio::abm::TestingScheme(random_criteria(), mio::abm::days(3), mio::abm::TimePoint(0), - mio::abm::TimePoint(0) + mio::abm::days(10), {}, 0.5)); - model.get_testing_strategy().add_testing_scheme( - mio::abm::LocationType::Home, - mio::abm::TestingScheme(random_criteria(), mio::abm::days(3), mio::abm::TimePoint(0), - mio::abm::TimePoint(0) + mio::abm::days(10), {}, 0.5)); - model.get_testing_strategy().add_testing_scheme( - mio::abm::LocationType::SocialEvent, + model.get_testing_strategy().add_scheme( + {mio::abm::LocationType::School, mio::abm::LocationType::Work, mio::abm::LocationType::SocialEvent, + mio::abm::LocationType::Home}, mio::abm::TestingScheme(random_criteria(), mio::abm::days(3), mio::abm::TimePoint(0), mio::abm::TimePoint(0) + mio::abm::days(10), {}, 0.5)); diff --git a/cpp/examples/abm_history_object.cpp b/cpp/examples/abm_history_object.cpp index 70e8939f60..28c883b892 100644 --- a/cpp/examples/abm_history_object.cpp +++ b/cpp/examples/abm_history_object.cpp @@ -137,7 +137,7 @@ int main() auto testing_criteria_work = mio::abm::TestingCriteria(); auto testing_scheme_work = mio::abm::TestingScheme(testing_criteria_work, validity_period, start_date, end_date, test_parameters, probability); - model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, testing_scheme_work); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::Work, testing_scheme_work); // Assign infection state to each person. // The infection states are chosen randomly. diff --git a/cpp/examples/abm_minimal.cpp b/cpp/examples/abm_minimal.cpp index b2e9c06ada..a78e0dddf7 100644 --- a/cpp/examples/abm_minimal.cpp +++ b/cpp/examples/abm_minimal.cpp @@ -113,8 +113,8 @@ int main() auto test_parameters = model.parameters.get()[test_type]; auto testing_criteria_work = mio::abm::TestingCriteria(); auto testing_scheme_work = mio::abm::TestingScheme(testing_criteria_work, validity_period, start_date, end_date, - test_parameters, probability); - model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, testing_scheme_work); + test_parameters, probability); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::Work, testing_scheme_work); // Assign infection state to each person. // The infection states are chosen randomly with the following distribution diff --git a/cpp/models/abm/model.cpp b/cpp/models/abm/model.cpp index 2b19d8af1a..d95691e301 100755 --- a/cpp/models/abm/model.cpp +++ b/cpp/models/abm/model.cpp @@ -123,10 +123,11 @@ void Model::perform_mobility(TimePoint t, TimeSpan dt) 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)) { + // The person cannot move if he has a positive test result + if (!m_testing_strategy.run_and_check(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 @@ -190,7 +191,7 @@ void Model::perform_mobility(TimePoint t, TimeSpan dt) continue; } // skip the trip if the performed TestingStrategy is positive - if (!m_testing_strategy.run_strategy(personal_rng, person, target_location, t)) { + if (!m_testing_strategy.run_and_check(personal_rng, person, target_location, t)) { continue; } // all requirements are met, move to target location @@ -297,8 +298,6 @@ void Model::compute_exposure_caches(TimePoint t, TimeSpan dt) void Model::begin_step(TimePoint t, TimeSpan dt) { - m_testing_strategy.update_activity_status(t); - if (!m_is_local_population_cache_valid) { build_compute_local_population_cache(); m_is_local_population_cache_valid = true; diff --git a/cpp/models/abm/model.h b/cpp/models/abm/model.h index 452210da29..0c6b320e25 100644 --- a/cpp/models/abm/model.h +++ b/cpp/models/abm/model.h @@ -341,22 +341,6 @@ class Model return m_id; } - /** - * @brief Add a TestingScheme to the set of schemes that are checked for testing at all Locations that have - * the LocationType. - * @param[in] loc_type LocationId key for TestingScheme to be added. - * @param[in] scheme TestingScheme to be added. - */ - void add_testing_scheme(const LocationType& loc_type, const TestingScheme& scheme); - - /** - * @brief Remove a TestingScheme from the set of schemes that are checked for testing at all Locations that have - * the LocationType. - * @param[in] loc_type LocationId key for TestingScheme to be added. - * @param[in] scheme TestingScheme to be added. - */ - void remove_testing_scheme(const LocationType& loc_type, const TestingScheme& scheme); - /** * @brief Get a reference to a Person from this Model. * @param[in] person_id A Person's PersonId. diff --git a/cpp/models/abm/testing_strategy.cpp b/cpp/models/abm/testing_strategy.cpp index 1a91d9aa47..22ca75c4c4 100644 --- a/cpp/models/abm/testing_strategy.cpp +++ b/cpp/models/abm/testing_strategy.cpp @@ -43,26 +43,6 @@ bool TestingCriteria::operator==(const TestingCriteria& other) const return m_ages == other.m_ages && m_infection_states == other.m_infection_states; } -void TestingCriteria::add_age_group(const AgeGroup age_group) -{ - m_ages.set(static_cast(age_group), true); -} - -void TestingCriteria::remove_age_group(const AgeGroup age_group) -{ - m_ages.set(static_cast(age_group), false); -} - -void TestingCriteria::add_infection_state(const InfectionState infection_state) -{ - m_infection_states.set(static_cast(infection_state), true); -} - -void TestingCriteria::remove_infection_state(const InfectionState infection_state) -{ - m_infection_states.set(static_cast(infection_state), false); -} - bool TestingCriteria::evaluate(const Person& p, TimePoint t) const { // An empty vector of ages or none bitset of #InfectionStates% means that no condition on the corresponding property is set. @@ -79,6 +59,7 @@ TestingScheme::TestingScheme(const TestingCriteria& testing_criteria, TimeSpan v , m_test_parameters(test_parameters) , m_probability(probability) { + assert(start_date <= end_date && "Start date must be before or equal to end date"); } bool TestingScheme::operator==(const TestingScheme& other) const @@ -91,23 +72,28 @@ bool TestingScheme::operator==(const TestingScheme& other) const //To be adjusted and also TestType should be static. } -bool TestingScheme::is_active() const +bool TestingScheme::is_active(TimePoint t) const { - return m_is_active; + return (m_start_date <= t && t < m_end_date); } -void TestingScheme::update_activity_status(TimePoint t) +bool TestingScheme::run_and_test(PersonalRandomNumberGenerator& rng, Person& person, TimePoint t) const { - m_is_active = (m_start_date <= t && t <= m_end_date); -} + if (!is_active(t)) { // If the scheme is not active, do nothing; early return + return false; + } -bool TestingScheme::run_scheme(PersonalRandomNumberGenerator& rng, Person& person, TimePoint t) const -{ auto test_result = person.get_test_result(m_test_parameters.type); // If the agent has a test result valid until now, use the result directly if ((test_result.time_of_testing > TimePoint(std::numeric_limits::min())) && (test_result.time_of_testing + m_validity_period >= t)) { - return !test_result.result; + return test_result.result; // If the test is positive, the entry is not allowed, and vice versa + } + if (person.get_compliance(InterventionType::Testing) < + 1.0 && // Dont need to draw a random number if the person is compliant either way + !person.is_compliant( + rng, InterventionType::Testing)) { // If the person is not compliant with the testing intervention + return true; } // Otherwise, the time_of_testing in the past (i.e. the agent has already performed it). if (m_testing_criteria.evaluate(person, t - m_test_parameters.required_time)) { @@ -115,98 +101,79 @@ bool TestingScheme::run_scheme(PersonalRandomNumberGenerator& rng, Person& perso if (random < m_probability) { bool result = person.get_tested(rng, t - m_test_parameters.required_time, m_test_parameters); person.add_test_result(t, m_test_parameters.type, result); - return !result; + return result; // If the test is positive, the entry is not allowed, and vice versa } } - return true; + // If the test is not performed, the entry is allowed + return false; } -TestingStrategy::TestingStrategy(const std::vector& location_to_schemes_map) - : m_location_to_schemes_map(location_to_schemes_map.begin(), location_to_schemes_map.end()) +TestingStrategy::TestingStrategy(const std::vector& location_to_schemes_id, + const std::vector& location_to_schemes_type) + : m_testing_schemes_at_location_id(location_to_schemes_id.begin(), location_to_schemes_id.end()) + , m_testing_schemes_at_location_type(location_to_schemes_type.begin(), location_to_schemes_type.end()) { } -void TestingStrategy::add_testing_scheme(const LocationType& loc_type, const LocationId& loc_id, - const TestingScheme& scheme) +void TestingStrategy::add_scheme(const LocationId& loc_id, const TestingScheme& scheme) { - auto iter_schemes = - std::find_if(m_location_to_schemes_map.begin(), m_location_to_schemes_map.end(), [&](const auto& p) { - return p.type == loc_type && p.id == loc_id; - }); - if (iter_schemes == m_location_to_schemes_map.end()) { - //no schemes for this location yet, add a new list with one scheme - m_location_to_schemes_map.push_back({loc_type, loc_id, std::vector(1, scheme)}); - } - else { - //add scheme to existing vector if the scheme doesn't exist yet - auto& schemes = iter_schemes->schemes; - if (std::find(schemes.begin(), schemes.end(), scheme) == schemes.end()) { - schemes.push_back(scheme); - } + if (loc_id.get() >= m_testing_schemes_at_location_id.size()) { + m_testing_schemes_at_location_id.resize(loc_id.get() + 1); } + m_testing_schemes_at_location_id[loc_id.get()].schemes.push_back(scheme); } -void TestingStrategy::remove_testing_scheme(const LocationType& loc_type, const LocationId& loc_id, - const TestingScheme& scheme) +void TestingStrategy::add_scheme(const LocationType& loc_type, const TestingScheme& scheme) { - auto iter_schemes = - std::find_if(m_location_to_schemes_map.begin(), m_location_to_schemes_map.end(), [&](const auto& p) { - return p.type == loc_type && p.id == loc_id; - }); - if (iter_schemes != m_location_to_schemes_map.end()) { - //remove the scheme from the list - auto& schemes_vector = iter_schemes->schemes; - auto last = std::remove(schemes_vector.begin(), schemes_vector.end(), scheme); - schemes_vector.erase(last, schemes_vector.end()); - //delete the list of schemes for this location if no schemes left - if (schemes_vector.empty()) { - m_location_to_schemes_map.erase(iter_schemes); - } + if ((size_t)loc_type >= m_testing_schemes_at_location_type.size()) { + m_testing_schemes_at_location_type.resize((size_t)loc_type + 1); } + m_testing_schemes_at_location_type[(size_t)loc_type].schemes.push_back(scheme); } -void TestingStrategy::update_activity_status(TimePoint t) +bool TestingStrategy::run_and_check(PersonalRandomNumberGenerator& rng, Person& person, const Location& location, + TimePoint t) { - for (auto& [_type, _id, testing_schemes] : m_location_to_schemes_map) { - for (auto& scheme : testing_schemes) { - scheme.update_activity_status(t); - } - } -} + // Early return if no scheme defined for this location or type + auto loc_id = location.get_id().get(); + auto loc_type = static_cast(location.get_type()); -bool TestingStrategy::run_strategy(PersonalRandomNumberGenerator& rng, Person& person, const Location& location, - TimePoint t) -{ - // A Person is always allowed to go home and this is never called if a person is not discharged from a hospital or ICU. - if (location.get_type() == mio::abm::LocationType::Home) { - return true; + bool has_id_schemes = + loc_id < m_testing_schemes_at_location_id.size() && !m_testing_schemes_at_location_id[loc_id].schemes.empty(); + + bool has_type_schemes = loc_type < m_testing_schemes_at_location_type.size() && + !m_testing_schemes_at_location_type[loc_type].schemes.empty(); + + if (!has_id_schemes && !has_type_schemes) { + return true; // No applicable 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; + bool entry_allowed = true; // Assume entry is allowed unless a scheme denies it + // Check schemes for specific location id + if (has_id_schemes) { + for (const auto& scheme : m_testing_schemes_at_location_id[loc_id].schemes) { + if (scheme.run_and_test(rng, person, t)) { + entry_allowed = false; // Deny entry + } + } } - // 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 = - std::find_if(m_location_to_schemes_map.begin(), m_location_to_schemes_map.end(), [&](const auto& 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 - 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); - })) { - return false; + // Check schemes for location type + if (has_type_schemes) { + for (const auto& scheme : m_testing_schemes_at_location_type[loc_type].schemes) { + if (scheme.run_and_test(rng, person, t)) { + entry_allowed = false; // Deny entry } } } - return true; + + // If the location is a home, entry is always allowed regardless of testing, no early return here because we still need to test + if (location.get_type() == LocationType::Home) { + return true; + } + else { + return entry_allowed; + } } } // namespace abm diff --git a/cpp/models/abm/testing_strategy.h b/cpp/models/abm/testing_strategy.h index eb9b59670b..adae629210 100644 --- a/cpp/models/abm/testing_strategy.h +++ b/cpp/models/abm/testing_strategy.h @@ -62,31 +62,6 @@ class TestingCriteria */ bool operator==(const TestingCriteria& other) const; - /** - * @brief Add an AgeGroup to the set of AgeGroup%s that are either allowed or required to be tested. - * @param[in] age_group AgeGroup to be added. - */ - void add_age_group(const AgeGroup age_group); - - /** - * @brief Remove an AgeGroup from the set of AgeGroup%s that are either allowed or required to be tested. - * @param[in] age_group AgeGroup to be removed. - */ - void remove_age_group(const AgeGroup age_group); - - /** - * @brief Add an #InfectionState to the set of #InfectionState%s that are either allowed or required to be tested. - * @param[in] infection_state #InfectionState to be added. - */ - void add_infection_state(const InfectionState infection_state); - - /** - * @brief Remove an #InfectionState from the set of #InfectionState%s that are either allowed or required to be - * tested. - * @param[in] infection_state #InfectionState to be removed. - */ - void remove_infection_state(const InfectionState infection_state); - /** * @brief Check if a Person and a Location meet all the required properties to get tested. * @param[in] p Person to be checked. @@ -133,22 +108,16 @@ class TestingScheme * @brief Gets the activity status of the scheme. * @return Whether the TestingScheme is currently active. */ - bool is_active() const; - - /** - * @brief Checks if the scheme is active at a given time and updates activity status. - * @param[in] t TimePoint to be updated at. - */ - void update_activity_status(TimePoint t); + bool is_active(TimePoint t) const; /** * @brief Runs the TestingScheme and potentially tests a Person. * @param[inout] rng PersonalRandomNumberGenerator of the Person being tested. * @param[in] person Person to check. * @param[in] t TimePoint when to run the scheme. - * @return If the person is allowed to enter the Location by the scheme. + * @return Whether the Person is assumed to have a positive test result (could be not complying to the test). */ - bool run_scheme(PersonalRandomNumberGenerator& rng, Person& person, TimePoint t) const; + bool run_and_test(PersonalRandomNumberGenerator& rng, Person& person, TimePoint t) const; /// This method is used by the default serialization feature. auto default_serialize() @@ -159,21 +128,37 @@ class TestingScheme .add("start_date", m_start_date) .add("end_date", m_end_date) .add("test_params", m_test_parameters) - .add("probability", m_probability) - .add("is_active", m_is_active); + .add("probability", m_probability); } private: friend DefaultFactory; TestingScheme() = default; + /** + * @brief Gets the start date of the scheme. + * @return The start date of the scheme. + */ + mio::abm::TimePoint get_start_date() const + { + return m_start_date; + } + + /** + * @brief Gets the end date of the scheme. + * @return The end date of the scheme. + */ + mio::abm::TimePoint get_end_date() const + { + return m_end_date; + } + TestingCriteria m_testing_criteria; ///< TestingCriteria of the scheme. TimeSpan m_validity_period; ///< The valid TimeSpan of the test. TimePoint m_start_date; ///< Starting date of the scheme. TimePoint m_end_date; ///< Ending date of the scheme. TestParameters m_test_parameters; ///< Parameters of the test. ScalarType m_probability; ///< Probability of performing the test. - bool m_is_active = false; ///< Whether the scheme is currently active. }; /** @@ -183,92 +168,78 @@ class TestingStrategy { public: /** - * @brief List of testing schemes for a given LocationType and LocationId. - * A LocalStrategy with id of value LocationId::invalid_id() is used for all Locations with LocationType type. + * @brief Vector of testing schemes used as an entry for either LocationId or LocationType in TestingStrategy. + * @details The index of the vector is either corresponding to the LocationId or LocationType in the according TestingStrategy vector. */ struct LocalStrategy { - LocationType type; - LocationId id; std::vector schemes; /// This method is used by the default serialization feature. auto default_serialize() { - return Members("LocalStrategy").add("type", type).add("id", id).add("schemes", schemes); + return Members("LocalStrategy").add("schemes", schemes); } }; /** * @brief Create a TestingStrategy. - * @param[in] testing_schemes Vector of TestingSchemes that are checked for testing. + * @param[in] testing_schemes Vector of TestingSchemes that are checked for testing. + * The first vector is for LocationId and the second for LocationType. + * The index of the vector is the LocationId or LocationType and the value is the vektor of TestingScheme(s). */ TestingStrategy() = default; - explicit TestingStrategy(const std::vector& location_to_schemes_map); + explicit TestingStrategy(const std::vector& location_to_schemes_id, + const std::vector& location_to_schemes_type); /** - * @brief Add a TestingScheme to the set of schemes that are checked for testing at a certain Location. - * A TestingScheme with loc_id of value LocationId::invalid_id() is used for all Locations of the given type. - * @param[in] loc_type LocationType key for TestingScheme to be remove. + * @brief Add a TestingScheme to the set of schemes that are checked for testing at a certain Location for a specific id. * @param[in] loc_id LocationId key for TestingScheme to be added. * @param[in] scheme TestingScheme to be added. */ - void add_testing_scheme(const LocationType& loc_type, const LocationId& loc_id, const TestingScheme& scheme); - + void add_scheme(const LocationId& loc_id, const TestingScheme& scheme); /** - * @brief Add a TestingScheme to the set of schemes that are checked for testing at a certain LocationType. - * A TestingScheme with loc_id of value LocationId::invalid_id() is used for all Locations of the given type. - * @param[in] loc_type LocationId key for TestingScheme to be added. + * @brief Add a TestingScheme to the set of schemes that are checked for testing at a certain Location. + * @param[in] loc_type LocationType key for TestingScheme to add. * @param[in] scheme TestingScheme to be added. */ - void add_testing_scheme(const LocationType& loc_type, const TestingScheme& scheme) - { - add_testing_scheme(loc_type, LocationId::invalid_id(), scheme); - } + void add_scheme(const LocationType& loc_type, const TestingScheme& scheme); /** - * @brief Remove a TestingScheme from the set of schemes that are checked for testing at a certain Location. - * @param[in] loc_type LocationType key for TestingScheme to be remove. - * @param[in] loc_id LocationId key for TestingScheme to be remove. - * @param[in] scheme TestingScheme to be removed. - */ - void remove_testing_scheme(const LocationType& loc_type, const LocationId& loc_id, const TestingScheme& scheme); - - /** - * @brief Remove a TestingScheme from the set of schemes that are checked for testing at a certain Location. - * A TestingScheme with loc_id of value LocationId::invalid_id() is used for all Locations of the given type. - * @param[in] loc_type LocationType key for TestingScheme to be remove. - * @param[in] scheme TestingScheme to be removed. + * @brief Add a TestingScheme to the set of schemes that are checked for testing at a certain Location. + * @param[in] loc_type Vector of LocationType key for TestingScheme to add. + * @param[in] scheme TestingScheme to be added. */ - void remove_testing_scheme(const LocationType& loc_type, const TestingScheme& scheme) + void add_scheme(const std::vector& loc_type, const TestingScheme& scheme) { - remove_testing_scheme(loc_type, LocationId::invalid_id(), scheme); + for (auto& type : loc_type) { + add_scheme(type, scheme); + } } /** - * @brief Checks if the given TimePoint is within the interval of start and end date of each TestingScheme and then - * changes the activity status for each TestingScheme accordingly. - * @param t TimePoint to check the activity status of each TestingScheme. - */ - void update_activity_status(const TimePoint t); - - /** - * @brief Runs the TestingStrategy and potentially tests a Person. + * @brief Runs the TestingStrategy and potentially tests a Person when entering. + * @details The TestingStrategy runs the TestingSchemes in the order they are added but first IDs and then types. + * It also decides if one can enter, if there are no positive tests, home is always allowed. * @param[inout] rng PersonalRandomNumberGenerator of the Person being tested. * @param[in] person Person to check. * @param[in] location Location to check. * @param[in] t TimePoint when to run the strategy. - * @return If the Person is allowed to enter the Location. */ - bool run_strategy(PersonalRandomNumberGenerator& rng, Person& person, const Location& location, TimePoint t); + bool run_and_check(PersonalRandomNumberGenerator& rng, Person& person, const Location& location, TimePoint t); /// This method is used by the default serialization feature. auto default_serialize() { - return Members("TestingStrategy").add("schemes", m_location_to_schemes_map); + return Members("TestingStrategy") + .add("schemes_id", m_testing_schemes_at_location_id) + .add("schemes_type", m_testing_schemes_at_location_type); } private: - std::vector m_location_to_schemes_map; ///< Set of schemes that are checked for testing. + std::vector + m_testing_schemes_at_location_id; ///< Set of schemes that are checked for testing in specific locations. + std::vector + m_testing_schemes_at_location_type; ///< Set of schemes that are checked for testing in overall locations types }; } // namespace abm diff --git a/cpp/models/graph_abm/graph_abmodel.h b/cpp/models/graph_abm/graph_abmodel.h index 05490bcbe6..45bb27fa91 100644 --- a/cpp/models/graph_abm/graph_abmodel.h +++ b/cpp/models/graph_abm/graph_abmodel.h @@ -113,7 +113,7 @@ class GraphABModel : public abm::Model return false; } // the Person cannot move if the performed TestingStrategy is positive - if (!m_testing_strategy.run_strategy(personal_rng, person, target_location, t)) { + if (!m_testing_strategy.run_and_check(personal_rng, person, target_location, t)) { return false; } // update worn mask to target location's requirements @@ -173,7 +173,7 @@ class GraphABModel : public abm::Model continue; } // skip the trip if the performed TestingStrategy is positive - if (!Base::m_testing_strategy.run_strategy(personal_rng, person, target_location, t)) { + if (!Base::m_testing_strategy.run_and_check(personal_rng, person, target_location, t)) { continue; } // all requirements are met, move to target location diff --git a/cpp/simulations/abm.cpp b/cpp/simulations/abm.cpp index ab0df1be70..8d47677e5c 100644 --- a/cpp/simulations/abm.cpp +++ b/cpp/simulations/abm.cpp @@ -338,7 +338,7 @@ void create_assign_locations(mio::abm::Model& model) auto testing_scheme = mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params, probability.draw_sample()); - model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::SocialEvent, testing_scheme); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::SocialEvent, testing_scheme); // Add hospital and ICU with 5 maximum contacs. // For the number of agents in this example we assume a capacity of 584 persons (80 beds per 10000 residents in @@ -432,7 +432,7 @@ void create_assign_locations(mio::abm::Model& model) validity_period = mio::abm::days(7); auto testing_scheme_school = mio::abm::TestingScheme(testing_criteria_school, validity_period, start_date, end_date, test_params, probability.draw_sample()); - model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::School, testing_scheme_school); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::School, testing_scheme_school); auto test_at_work = std::vector{mio::abm::LocationType::Work}; auto testing_criteria_work = mio::abm::TestingCriteria(); @@ -440,7 +440,7 @@ void create_assign_locations(mio::abm::Model& model) assign_uniform_distribution(probability, 0.1, 0.5); auto testing_scheme_work = mio::abm::TestingScheme(testing_criteria_work, validity_period, start_date, end_date, test_params, probability.draw_sample()); - model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, testing_scheme_work); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::Work, testing_scheme_work); } /** diff --git a/cpp/tests/test_abm_model.cpp b/cpp/tests/test_abm_model.cpp index dbf398632a..1899b5ff8d 100644 --- a/cpp/tests/test_abm_model.cpp +++ b/cpp/tests/test_abm_model.cpp @@ -612,41 +612,29 @@ TEST_F(TestModelTestingCriteria, testAddingAndUpdatingAndRunningTestingSchemes) person.set_assigned_location(mio::abm::LocationType::Work, work_id, model.get_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 start_date = mio::abm::TimePoint(0) + mio::abm::days(1); + const auto end_date = mio::abm::TimePoint(0) + mio::abm::days(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 testing_criteria = mio::abm::TestingCriteria( + {}, {mio::abm::InfectionState::InfectedSymptoms, mio::abm::InfectionState::InfectedNoSymptoms}); auto testing_scheme = mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params_pcr, probability); - model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, testing_scheme); - EXPECT_EQ(model.get_testing_strategy().run_strategy(rng_person, person, work, current_time), + model.get_testing_strategy().add_scheme(mio::abm::LocationType::Work, testing_scheme); + EXPECT_EQ(model.get_testing_strategy().run_and_check(rng_person, person, work, current_time), true); // no active testing scheme -> person can enter - current_time = mio::abm::TimePoint(30); - model.get_testing_strategy().update_activity_status(current_time); + current_time = mio::abm::TimePoint(0) + mio::abm::days(2); ScopedMockDistribution>>> mock_uniform_dist; EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) - .Times(testing::Exactly(5)) - .WillOnce(testing::Return(0.7)) // Person complies with testing + .Times(testing::Exactly(3)) .WillOnce(testing::Return(0.5)) // Probability for testing (is performed) .WillOnce(testing::Return(0.4)) // Test result is positive - .WillOnce(testing::Return(0.0)) // Draw for isolation compliance (doesn't matter in this test) - .WillOnce( - testing::Return(0.7)); // Person complies with testing (even though there is not testing strategy left) - EXPECT_EQ(model.get_testing_strategy().run_strategy(rng_person, person, work, current_time), - false); // Testing scheme active and restricts entry - - // Try to re-add the same testing scheme and confirm it doesn't duplicate, then remove it. - model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, - testing_scheme); //doesn't get added because of == operator - model.get_testing_strategy().remove_testing_scheme(mio::abm::LocationType::Work, testing_scheme); - EXPECT_EQ(model.get_testing_strategy().run_strategy(rng_person, person, work, current_time), - true); // no more testing_schemes + .WillOnce(testing::Return(0.0)); // Draw for isolation compliance (doesn't matter in this test) + EXPECT_EQ(model.get_testing_strategy().run_and_check(rng_person, person, work, current_time), + false); // Testing scheme active but person complies with testing } /** @@ -879,7 +867,7 @@ TEST_F(TestModel, mobilityRulesWithAppliedNPIs) 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); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::Work, testing_scheme); ScopedMockDistribution>>> mock_exponential_dist; @@ -995,7 +983,7 @@ TEST_F(TestModel, mobilityTripWithAppliedNPIs) 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); + model.get_testing_strategy().add_scheme(mio::abm::LocationType::Work, testing_scheme); ScopedMockDistribution>>> mock_exponential_dist; diff --git a/cpp/tests/test_abm_serialization.cpp b/cpp/tests/test_abm_serialization.cpp index 386cfe276f..9df47c8fb2 100644 --- a/cpp/tests/test_abm_serialization.cpp +++ b/cpp/tests/test_abm_serialization.cpp @@ -148,7 +148,6 @@ TEST(TestAbmSerialization, TestingScheme) reference_json["end_date"]["seconds"] = Json::Int(i++); reference_json["test_params"] = test_parameters; reference_json["probability"] = Json::Value((double)i++); - reference_json["is_active"] = Json::Value((bool)0); test_json_serialization(reference_json); } @@ -156,16 +155,13 @@ TEST(TestAbmSerialization, TestingScheme) TEST(TestAbmSerialization, TestingStrategy) { // See test_json_serialization for info on this test. - - unsigned i = 1; // counter s.t. members have different values - Json::Value local_strategy; - local_strategy["id"] = Json::UInt(i++); local_strategy["schemes"] = Json::Value(Json::arrayValue); - local_strategy["type"] = Json::UInt(i++); Json::Value reference_json; - reference_json["schemes"][0] = local_strategy; + + reference_json["schemes_id"][0] = local_strategy; + reference_json["schemes_type"][0] = local_strategy; test_json_serialization(reference_json); } @@ -257,19 +253,20 @@ TEST(TestAbmSerialization, Model) Json::Value abm_parameters = mio::serialize_json(mio::abm::Parameters(i++)).value(); Json::Value reference_json; - reference_json["cemetery_id"] = Json::UInt(i++); - reference_json["location_types"] = Json::UInt(i++); - reference_json["locations"] = Json::Value(Json::arrayValue); - reference_json["parameters"] = abm_parameters; - reference_json["persons"] = Json::Value(Json::arrayValue); - reference_json["rng"]["counter"] = Json::UInt(i++); - reference_json["rng"]["key"] = Json::UInt(i++); - reference_json["rng"]["seeds"] = json_uint_array({i++, i++, i++, i++, i++, i++}); - reference_json["testing_strategy"]["schemes"] = Json::Value(Json::arrayValue); - reference_json["trip_list"]["index"] = Json::UInt(i++); - reference_json["trip_list"]["trips_weekday"] = Json::Value(Json::arrayValue); - reference_json["trip_list"]["trips_weekend"] = Json::Value(Json::arrayValue); - reference_json["use_mobility_rules"] = Json::Value(false); + reference_json["cemetery_id"] = Json::UInt(i++); + reference_json["location_types"] = Json::UInt(i++); + reference_json["locations"] = Json::Value(Json::arrayValue); + reference_json["parameters"] = abm_parameters; + reference_json["persons"] = Json::Value(Json::arrayValue); + reference_json["rng"]["counter"] = Json::UInt(i++); + reference_json["rng"]["key"] = Json::UInt(i++); + reference_json["rng"]["seeds"] = json_uint_array({i++, i++, i++, i++, i++, i++}); + reference_json["testing_strategy"]["schemes_id"] = Json::Value(Json::arrayValue); + reference_json["testing_strategy"]["schemes_type"] = Json::Value(Json::arrayValue); + reference_json["trip_list"]["index"] = Json::UInt(i++); + reference_json["trip_list"]["trips_weekday"] = Json::Value(Json::arrayValue); + reference_json["trip_list"]["trips_weekend"] = Json::Value(Json::arrayValue); + reference_json["use_mobility_rules"] = Json::Value(false); test_json_serialization(reference_json); } diff --git a/cpp/tests/test_abm_testing_strategy.cpp b/cpp/tests/test_abm_testing_strategy.cpp index 8c8fb5de06..8e934ba077 100644 --- a/cpp/tests/test_abm_testing_strategy.cpp +++ b/cpp/tests/test_abm_testing_strategy.cpp @@ -35,37 +35,26 @@ TEST_F(TestTestingCriteria, addRemoveAndEvaluateTestCriteria) mio::abm::TimePoint t{0}; // Initialize testing criteria with no age group or infection state set. - auto testing_criteria = mio::abm::TestingCriteria(); + auto testing_criteria_empty = mio::abm::TestingCriteria(); // Empty criteria should evaluate to true. - EXPECT_EQ(testing_criteria.evaluate(person, t), true); + EXPECT_EQ(testing_criteria_empty.evaluate(person, t), true); // Add infection states to the criteria. - testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedSymptoms); - testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedNoSymptoms); - - // Add an incorrect age group and evaluate. - testing_criteria.add_age_group(age_group_35_to_59); + std::vector test_infection_states = {mio::abm::InfectionState::InfectedSymptoms, + mio::abm::InfectionState::InfectedNoSymptoms}; + std::vector test_age_groups = {age_group_35_to_59}; + auto testing_criteria_false_ag = mio::abm::TestingCriteria(test_age_groups, test_infection_states); // Age group mismatch, should evaluate to false. - EXPECT_EQ(testing_criteria.evaluate(person, t), false); - // Remove the incorrect age group and evaluate again. - testing_criteria.remove_age_group(age_group_35_to_59); - EXPECT_EQ(testing_criteria.evaluate(person, t), true); - // Remove the infection state and check evaluation. - testing_criteria.remove_infection_state(mio::abm::InfectionState::InfectedSymptoms); - EXPECT_EQ(testing_criteria.evaluate(person, t), false); - - // Add the infection state again and verify. - testing_criteria.add_infection_state(mio::abm::InfectionState::InfectedSymptoms); - - // Test equality of testing criteria. - auto testing_criteria_manual = mio::abm::TestingCriteria( - std::vector({age_group_15_to_34}), - std::vector({mio::abm::InfectionState::InfectedNoSymptoms})); - EXPECT_EQ(testing_criteria == testing_criteria_manual, false); - // Modify manual criteria to match. - testing_criteria_manual.add_infection_state(mio::abm::InfectionState::InfectedSymptoms); - testing_criteria_manual.remove_age_group(age_group_15_to_34); - EXPECT_EQ(testing_criteria == testing_criteria_manual, true); + EXPECT_EQ(testing_criteria_false_ag.evaluate(person, t), false); + + // Add age groups to the criteria. + test_age_groups.push_back(age_group_15_to_34); + auto testing_criteria_right_ag = mio::abm::TestingCriteria(test_age_groups, test_infection_states); + // Now it should evaluate to true. + EXPECT_EQ(testing_criteria_right_ag.evaluate(person, t), true); + + // Test inequality of testing criteria. + EXPECT_EQ(testing_criteria_right_ag == testing_criteria_false_ag, false); } using TestTestingScheme = RandomNumberTest; @@ -84,8 +73,8 @@ TEST_F(TestTestingScheme, runScheme) std::vector testing_criterias = {testing_criteria1}; auto validity_period = mio::abm::days(1); - const auto start_date = mio::abm::TimePoint(0); - const auto end_date = mio::abm::TimePoint(60 * 60 * 24 * 3); + const auto start_date = mio::abm::TimePoint(0) + mio::abm::seconds(1); + const auto end_date = mio::abm::TimePoint(0) + mio::abm::days(3); const auto probability = 0.8; const auto test_params_pcr = mio::abm::TestParameters{0.9, 0.99, mio::abm::hours(48), mio::abm::TestType::PCR}; @@ -97,16 +86,11 @@ TEST_F(TestTestingScheme, runScheme) mio::abm::TestingScheme(testing_criteria1, validity_period, start_date, end_date, test_params_pcr, probability); // Check the initial activity status. - EXPECT_EQ(testing_scheme1.is_active(), false); - testing_scheme1.update_activity_status(mio::abm::TimePoint(10)); - EXPECT_EQ(testing_scheme1.is_active(), true); + EXPECT_EQ(testing_scheme1.is_active(mio::abm::TimePoint(0)), false); + EXPECT_EQ(testing_scheme1.is_active(mio::abm::TimePoint(10)), true); // Deactivate the scheme after the end date. - testing_scheme1.update_activity_status(mio::abm::TimePoint(60 * 60 * 24 * 3 + 200)); - EXPECT_EQ(testing_scheme1.is_active(), false); - - // Reactivate the scheme. - testing_scheme1.update_activity_status(mio::abm::TimePoint(0)); + EXPECT_EQ(testing_scheme1.is_active(end_date + mio::abm::seconds(200)), false); // Setup a second scheme with different infection states. std::vector test_infection_states2 = {mio::abm::InfectionState::Recovered}; @@ -135,12 +119,12 @@ TEST_F(TestTestingScheme, runScheme) .WillOnce(testing::Return(0.7)) // Person 2 got test .WillOnce(testing::Return(0.5)); // Person 2 tested negative and can enter - EXPECT_EQ(testing_scheme1.run_scheme(rng_person1, person1, start_date), - false); // Person tests and tests positive - EXPECT_EQ(testing_scheme2.run_scheme(rng_person2, person2, start_date), - true); // Person tests and tests negative - EXPECT_EQ(testing_scheme1.run_scheme(rng_person1, person1, start_date), - false); // Person doesn't test but used the last result (false to enter) + EXPECT_EQ(testing_scheme1.run_and_test(rng_person1, person1, start_date), + true); // Person tests and tests positive + EXPECT_EQ(testing_scheme2.run_and_test(rng_person2, person2, start_date), + false); // Person tests and tests negative + EXPECT_EQ(testing_scheme1.run_and_test(rng_person1, person1, start_date), + true); // Person doesn't test but used the last result (false to enter) } /** @@ -160,12 +144,10 @@ TEST_F(TestTestingScheme, initAndRunTestingStrategy) auto testing_criteria1 = mio::abm::TestingCriteria({}, test_infection_states); auto testing_scheme1 = mio::abm::TestingScheme(testing_criteria1, validity_period, start_date, end_date, test_params_pcr, probability); - testing_scheme1.update_activity_status(mio::abm::TimePoint(0)); std::vector test_infection_states2 = {mio::abm::InfectionState::Recovered}; 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); // 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 = @@ -179,24 +161,439 @@ TEST_F(TestTestingScheme, initAndRunTestingStrategy) // Mock uniform distribution to control random behavior in testing. ScopedMockDistribution>>> mock_uniform_dist; EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) - .Times(testing::Exactly((8))) - .WillOnce(testing::Return(0.7)) // Person 1 complies to testing + .Times(testing::Exactly((5))) .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 + .WillOnce(testing::Return(0.5)); // Test of Person 2 is negative 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); - EXPECT_EQ(test_strategy.run_strategy(rng_person1, person1, loc_work, start_date), + mio::abm::TestingStrategy(std::vector{}, + std::vector{}); + test_strategy.add_scheme(mio::abm::LocationType::Work, testing_scheme1); + test_strategy.add_scheme(mio::abm::LocationType::Work, testing_scheme2); + EXPECT_EQ(test_strategy.run_and_check(rng_person1, person1, loc_work, start_date), false); // Person tests and tests positive - EXPECT_EQ(test_strategy.run_strategy(rng_person2, person2, loc_work, start_date), + EXPECT_EQ(test_strategy.run_and_check(rng_person2, person2, loc_work, start_date), true); // Person tests and tests negative - EXPECT_EQ(test_strategy.run_strategy(rng_person1, person1, loc_work, start_date), + EXPECT_EQ(test_strategy.run_and_check(rng_person1, person1, loc_work, start_date), false); // Person doesn't test but used the last result (false to enter) } + +/** + * @brief Test for edge cases in TestingCriteria. + */ +TEST_F(TestTestingCriteria, testingCriteriaEdgeCases) +{ + // Create test locations and persons with different age groups and infection states + mio::abm::Location home(mio::abm::LocationType::Home, 0, num_age_groups); + + // Test with various infection states + auto person_exposed = + make_test_person(this->get_rng(), home, age_group_15_to_34, mio::abm::InfectionState::Exposed); + auto person_symptoms = + make_test_person(this->get_rng(), home, age_group_15_to_34, mio::abm::InfectionState::InfectedSymptoms); + auto person_no_symptoms = + make_test_person(this->get_rng(), home, age_group_15_to_34, mio::abm::InfectionState::InfectedNoSymptoms); + auto person_recovered = + make_test_person(this->get_rng(), home, age_group_15_to_34, mio::abm::InfectionState::Recovered); + + mio::abm::TimePoint t{0}; + + // Test with only infection states criteria + std::vector test_infection_states = {mio::abm::InfectionState::InfectedSymptoms, + mio::abm::InfectionState::InfectedNoSymptoms}; + auto testing_criteria_infection = mio::abm::TestingCriteria({}, test_infection_states); + + // Should match only infected persons + EXPECT_EQ(testing_criteria_infection.evaluate(person_exposed, t), false); + EXPECT_EQ(testing_criteria_infection.evaluate(person_symptoms, t), true); + EXPECT_EQ(testing_criteria_infection.evaluate(person_no_symptoms, t), true); + EXPECT_EQ(testing_criteria_infection.evaluate(person_recovered, t), false); + + // Test with only age group criteria + std::vector test_age_groups = {age_group_15_to_34, age_group_35_to_59}; + auto testing_criteria_age = mio::abm::TestingCriteria(test_age_groups, {}); + + // Create persons with different age groups + auto person_young = + make_test_person(this->get_rng(), home, age_group_5_to_14, mio::abm::InfectionState::Susceptible); + auto person_adult = + make_test_person(this->get_rng(), home, age_group_15_to_34, mio::abm::InfectionState::Susceptible); + auto person_older = + make_test_person(this->get_rng(), home, age_group_35_to_59, mio::abm::InfectionState::Susceptible); + auto person_senior = + make_test_person(this->get_rng(), home, age_group_60_to_79, mio::abm::InfectionState::Susceptible); + + // Should match only specified age groups + EXPECT_EQ(testing_criteria_age.evaluate(person_young, t), false); + EXPECT_EQ(testing_criteria_age.evaluate(person_adult, t), true); + EXPECT_EQ(testing_criteria_age.evaluate(person_older, t), true); + EXPECT_EQ(testing_criteria_age.evaluate(person_senior, t), false); + + // Test with both age and infection state criteria + auto testing_criteria_both = mio::abm::TestingCriteria(test_age_groups, test_infection_states); + + // Should match only when both criteria are met + auto person_adult_infected = + make_test_person(this->get_rng(), home, age_group_15_to_34, mio::abm::InfectionState::InfectedSymptoms); + auto person_young_infected = + make_test_person(this->get_rng(), home, age_group_5_to_14, mio::abm::InfectionState::InfectedSymptoms); + auto person_adult_recovered = + make_test_person(this->get_rng(), home, age_group_15_to_34, mio::abm::InfectionState::Recovered); + + EXPECT_EQ(testing_criteria_both.evaluate(person_adult_infected, t), true); + EXPECT_EQ(testing_criteria_both.evaluate(person_young_infected, t), false); + EXPECT_EQ(testing_criteria_both.evaluate(person_adult_recovered, t), false); +} + +/** + * @brief Test for TestingScheme time-related functionality. + */ +TEST_F(TestTestingScheme, testingSchemeTimeValidity) +{ + auto testing_criteria = mio::abm::TestingCriteria(); + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(100); + const auto end_date = mio::abm::TimePoint(500); + const auto probability = 0.8; + const auto test_params = mio::abm::TestParameters{0.9, 0.99, mio::abm::hours(48), mio::abm::TestType::PCR}; + + auto testing_scheme = + mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params, probability); + + // Test boundary conditions of is_active method + EXPECT_EQ(testing_scheme.is_active(mio::abm::TimePoint(99)), false); // Just before start + EXPECT_EQ(testing_scheme.is_active(mio::abm::TimePoint(100)), true); // At start + EXPECT_EQ(testing_scheme.is_active(mio::abm::TimePoint(300)), true); // Middle + EXPECT_EQ(testing_scheme.is_active(mio::abm::TimePoint(499)), true); // Just before end + EXPECT_EQ(testing_scheme.is_active(mio::abm::TimePoint(500)), false); // At end + EXPECT_EQ(testing_scheme.is_active(mio::abm::TimePoint(501)), false); // After end +} + +/** + * @brief Test for TestingScheme result caching behavior. + */ +TEST_F(TestTestingScheme, testingSchemeResultCaching) +{ + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(0) + mio::abm::days(5); + const auto probability = 0.8; + const auto test_params_pcr = mio::abm::TestParameters{0.9, 0.99, mio::abm::hours(2), mio::abm::TestType::PCR}; + + auto testing_criteria = mio::abm::TestingCriteria({}, {}); + auto testing_scheme = + mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params_pcr, probability); + + // Create test person and location + mio::abm::Location loc_home(mio::abm::LocationType::Home, 0, num_age_groups); + auto person = make_test_person(this->get_rng(), loc_home, age_group_15_to_34, + mio::abm::InfectionState::InfectedNoSymptoms, start_date); + auto rng = mio::abm::PersonalRandomNumberGenerator(person); + + // Mock uniform distribution to control test results + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .WillOnce(testing::Return(0.7)) // Test is performed + .WillOnce(testing::Return(0.8)) // First test is positive + .WillOnce(testing::Return(0.5)); // Isolation compliance + + // First test at t=10 + auto t1 = mio::abm::TimePoint(10); + bool result1 = testing_scheme.run_and_test(rng, person, t1); + EXPECT_EQ(result1, true); // We expect the test to be positive + + // Test result should be cached for validity period + auto t2 = t1 + mio::abm::hours(12); // Within validity period + bool result2 = testing_scheme.run_and_test(rng, person, t2); + EXPECT_EQ(result2, true); // Should use cached result without calling RNG + + // After validity period expires, a new test should be performed + // But we didn't mock additional RNG calls, so this would fail if it tries to run a new test +} + +/** + * @brief Test for different test types and parameters. + */ +TEST_F(TestTestingScheme, differentTestTypes) +{ + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(0) + mio::abm::days(5); + const auto probability = 1.0; // Always test + + // Create PCR test parameters with high accuracy but long wait time + const auto test_params_pcr = mio::abm::TestParameters{0.95, 0.99, mio::abm::hours(24), mio::abm::TestType::PCR}; + + // Create rapid test parameters with lower accuracy (we need to set this to 24 hours to avoid the person getting healthy randomly) + const auto test_params_rapid = mio::abm::TestParameters{0.8, 0.9, mio::abm::hours(24), mio::abm::TestType::Antigen}; + + auto testing_criteria = mio::abm::TestingCriteria(); + + // Create test schemes for both test types + auto testing_scheme_pcr = + mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params_pcr, probability); + + auto testing_scheme_rapid = mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, + test_params_rapid, probability); + + // Create test persons with different infection states + mio::abm::Location loc_home(mio::abm::LocationType::Home, 0, num_age_groups); + auto person_infected = + make_test_person(this->get_rng(), loc_home, age_group_15_to_34, mio::abm::InfectionState::InfectedNoSymptoms, + start_date - test_params_pcr.required_time); + auto rng_infected = mio::abm::PersonalRandomNumberGenerator(person_infected); + + auto person_healthy = + make_test_person(this->get_rng(), loc_home, age_group_15_to_34, mio::abm::InfectionState::Susceptible, + start_date - test_params_pcr.required_time); + auto rng_healthy = mio::abm::PersonalRandomNumberGenerator(person_healthy); + + // Mock uniform distribution to control test results + // Mock uniform distribution to control test results for PCR test with infected person + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::Exactly(3)) + .WillOnce(testing::Return(0.7)) // PCR test is performed + .WillOnce(testing::Return(0.05)) // PCR test correctly identifies infection (< sensitivity 0.95) + .WillOnce(testing::Return(0.5)); // Person complies to isolation + + // Test PCR test with infected person + bool pcr_infected_result = testing_scheme_pcr.run_and_test(rng_infected, person_infected, start_date); + EXPECT_EQ(pcr_infected_result, true); // PCR should detect infection + + // Reset mock for PCR test with healthy person + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::Exactly(2)) + .WillOnce(testing::Return(0.7)) // PCR test is performed + .WillOnce(testing::Return(0.98)); // PCR test correctly identifies no infection (< specificity 0.99) + + // Test PCR test with healthy person + bool pcr_healthy_result = testing_scheme_pcr.run_and_test(rng_healthy, person_healthy, start_date); + EXPECT_EQ(pcr_healthy_result, false); // PCR should correctly identify no infection + + // Reset mock for rapid test with infected person + + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::Exactly(3)) + .WillOnce(testing::Return(0.7)) // Rapid test is performed + .WillOnce(testing::Return(0.0)) // Rapid test correctly identifies infection (< sensitivity 0.8) + .WillOnce(testing::Return(0.1)); // Infected person complies for isolation + + // Test rapid test with infected person + bool rapid_infected_result = testing_scheme_rapid.run_and_test(rng_infected, person_infected, start_date); + EXPECT_EQ(rapid_infected_result, true); // Rapid test should detect infection +} + +/** + * @brief Test for combining multiple testing schemes in TestingStrategy. + */ +TEST_F(TestTestingScheme, multipleSchemesCombination) +{ + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(500); + const auto probability = 0.8; + const auto test_params = mio::abm::TestParameters{0.9, 0.99, mio::abm::hours(2), mio::abm::TestType::PCR}; + + // Create testing schemes for different age groups + std::vector children_age_groups = {age_group_0_to_4, age_group_5_to_14}; + std::vector adult_age_groups = {age_group_15_to_34, age_group_35_to_59}; + std::vector senior_age_groups = {age_group_60_to_79, age_group_80_plus}; + + auto testing_criteria_children = mio::abm::TestingCriteria(children_age_groups, {}); + auto testing_criteria_adults = mio::abm::TestingCriteria(adult_age_groups, {}); + auto testing_criteria_seniors = mio::abm::TestingCriteria(senior_age_groups, {}); + + auto testing_scheme_children = mio::abm::TestingScheme(testing_criteria_children, validity_period, start_date, + end_date, test_params, probability); + auto testing_scheme_adults = mio::abm::TestingScheme(testing_criteria_adults, validity_period, start_date, end_date, + test_params, probability); + auto testing_scheme_seniors = mio::abm::TestingScheme(testing_criteria_seniors, validity_period, start_date, + end_date, test_params, probability); + + // Create TestingStrategy + mio::abm::TestingStrategy test_strategy; + + // Add schemes to different location types + test_strategy.add_scheme(mio::abm::LocationType::School, testing_scheme_children); + test_strategy.add_scheme(mio::abm::LocationType::Work, testing_scheme_adults); + test_strategy.add_scheme(mio::abm::LocationType::SocialEvent, testing_scheme_seniors); + + // Also add multiple location types at once + std::vector public_locations = {mio::abm::LocationType::BasicsShop, + mio::abm::LocationType::SocialEvent}; + test_strategy.add_scheme(public_locations, testing_scheme_adults); + + // Create locations + mio::abm::Location loc_home(mio::abm::LocationType::Home, 0, num_age_groups); + mio::abm::Location loc_school(mio::abm::LocationType::School, 1, num_age_groups); + mio::abm::Location loc_work(mio::abm::LocationType::Work, 2, num_age_groups); + mio::abm::Location loc_shop(mio::abm::LocationType::BasicsShop, 3, num_age_groups); + + // Create persons of different age groups + auto child = make_test_person(this->get_rng(), loc_home, age_group_5_to_14, mio::abm::InfectionState::Susceptible, + start_date); + auto adult = make_test_person(this->get_rng(), loc_home, age_group_35_to_59, mio::abm::InfectionState::Susceptible, + start_date); + + auto rng_child = mio::abm::PersonalRandomNumberGenerator(child); + auto rng_adult = mio::abm::PersonalRandomNumberGenerator(adult); + + // Mock uniform distribution to control test results + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::Exactly(5)) + .WillOnce(testing::Return(0.7)) // Child gets tested at school + .WillOnce(testing::Return(0.95)) // Child tests negative + .WillOnce(testing::Return(0.7)) // Adult gets tested at work + .WillOnce(testing::Return(0.999)) // Adult tests (false) positive + .WillOnce(testing::Return(0.5)); // Adult complies to isolation + + // Test child at school - should pass the test + bool school_result = test_strategy.run_and_check(rng_child, child, loc_school, start_date); + EXPECT_EQ(school_result, true); // Child should be allowed to enter school + + // Test adult at work - should fail the test + bool work_result = test_strategy.run_and_check(rng_adult, adult, loc_work, start_date); + EXPECT_EQ(work_result, false); // Adult should not be allowed to enter work + + // Test adult at shop - should use cached result without testing again + bool shop_result = test_strategy.run_and_check(rng_adult, adult, loc_shop, start_date); + EXPECT_EQ(shop_result, false); // Adult should not be allowed to enter shop either +} + +/** + * @brief Test for TestingStrategy handling of home location. + */ +TEST_F(TestTestingScheme, testingStrategyHomeAccess) +{ + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(500); + const auto probability = 0.8; + const auto test_params = mio::abm::TestParameters{0.9, 0.99, mio::abm::hours(2), mio::abm::TestType::PCR}; + + // Create a testing scheme for all infection states + std::vector all_states = { + mio::abm::InfectionState::InfectedSymptoms, mio::abm::InfectionState::InfectedNoSymptoms, + mio::abm::InfectionState::Exposed, mio::abm::InfectionState::Recovered, mio::abm::InfectionState::Susceptible}; + + auto testing_criteria = mio::abm::TestingCriteria({}, all_states); + auto testing_scheme = + mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params, probability); + + // Create TestingStrategy and add scheme to Home location type + mio::abm::TestingStrategy test_strategy; + test_strategy.add_scheme(mio::abm::LocationType::Home, testing_scheme); + + // Create home location and infected person + mio::abm::Location loc_home(mio::abm::LocationType::Home, 0, num_age_groups); + auto person = make_test_person(this->get_rng(), loc_home, age_group_15_to_34, + mio::abm::InfectionState::InfectedSymptoms, start_date); + auto rng = mio::abm::PersonalRandomNumberGenerator(person); + + // Mock uniform distribution to control random behavior in testing + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::Exactly(3)) + .WillOnce(testing::Return(0.7)) // Person gets tested + .WillOnce(testing::Return(0.05)) // Test is positive + .WillOnce(testing::Return(0.5)); // Person complies to isolation + + // Even though a person tests positive, they should always be allowed to enter home + bool result = test_strategy.run_and_check(rng, person, loc_home, start_date); + EXPECT_EQ(result, true); // Person should be allowed to enter home regardless of test result +} + +/** + * @brief Test for TestingStrategy with location-specific schemes. + */ +TEST_F(TestTestingScheme, locationSpecificSchemes) +{ + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(500); + const auto probability = 0.8; + const auto test_params = mio::abm::TestParameters{0.9, 0.99, mio::abm::hours(2), mio::abm::TestType::PCR}; + + // Create a testing scheme + auto testing_criteria = mio::abm::TestingCriteria(); + auto testing_scheme = + mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params, probability); + + // Create TestingStrategy + mio::abm::TestingStrategy test_strategy; + + // Add scheme to a specific location by ID + mio::abm::LocationId specific_shop_id(42); + test_strategy.add_scheme(specific_shop_id, testing_scheme); + + // Create locations with different IDs but same type + mio::abm::Location shop1(mio::abm::LocationType::BasicsShop, 42, num_age_groups); // Has the specific ID + mio::abm::Location shop2(mio::abm::LocationType::BasicsShop, 43, num_age_groups); // Different ID + + // Create a test person + auto person = + make_test_person(this->get_rng(), shop1, age_group_15_to_34, mio::abm::InfectionState::Susceptible, start_date); + auto rng = mio::abm::PersonalRandomNumberGenerator(person); + + // Mock uniform distribution to control test results + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::Exactly(3)) + .WillOnce(testing::Return(0.7)) // Person gets tested at shop1 + .WillOnce(testing::Return(0.995)) // Test is positive + .WillOnce(testing::Return(0.5)); // Person complies to isolation + + // Test at shop with specific ID - should run the scheme + bool result1 = test_strategy.run_and_check(rng, person, shop1, start_date); + EXPECT_EQ(result1, false); // Person should not be allowed to enter after positive test + + // Test at shop with different ID - no scheme should run + // No need to mock RNG calls here as no test should be performed + bool result2 = test_strategy.run_and_check(rng, person, shop2, start_date); + EXPECT_EQ(result2, true); // Person should be allowed to enter as no test is required +} + +TEST_F(TestTestingScheme, testCompliance) +{ + auto validity_period = mio::abm::days(1); + const auto start_date = mio::abm::TimePoint(0); + const auto end_date = mio::abm::TimePoint(500); + const auto probability = 0.8; + const auto test_params = mio::abm::TestParameters{0.9, 0.99, mio::abm::hours(2), mio::abm::TestType::PCR}; + + // Create a testing scheme + auto testing_criteria = mio::abm::TestingCriteria(); + auto testing_scheme = + mio::abm::TestingScheme(testing_criteria, validity_period, start_date, end_date, test_params, probability); + + // Create TestingStrategy + mio::abm::TestingStrategy test_strategy; + + // Add scheme to a specific location by ID + mio::abm::LocationId specific_shop_id(42); + test_strategy.add_scheme(specific_shop_id, testing_scheme); + + // Create locations with different IDs but same type + mio::abm::Location shop1(mio::abm::LocationType::BasicsShop, 42, num_age_groups); // Has the specific ID + + // Create a test person + auto person = + make_test_person(this->get_rng(), shop1, age_group_15_to_34, mio::abm::InfectionState::Susceptible, start_date); + auto rng = mio::abm::PersonalRandomNumberGenerator(person); + person.set_compliance(mio::abm::InterventionType::Testing, 0.1); // Set compliance for testing + + // Mock uniform distribution to control test results + ScopedMockDistribution>>> mock_uniform_dist; + EXPECT_CALL(mock_uniform_dist.get_mock(), invoke) + .Times(testing::Exactly(1)) + .WillOnce(testing::Return(0.2)); // Person is not compliant for testing + + // Test at shop with specific ID - should run the scheme + bool result1 = test_strategy.run_and_check(rng, person, shop1, start_date); + EXPECT_EQ(result1, false); // Person should not be allowed to enter after not complying to the test +} diff --git a/pycode/memilio-simulation/memilio/simulation/bindings/models/abm.cpp b/pycode/memilio-simulation/memilio/simulation/bindings/models/abm.cpp index 48a2cfa19a..19194358e6 100644 --- a/pycode/memilio-simulation/memilio/simulation/bindings/models/abm.cpp +++ b/pycode/memilio-simulation/memilio/simulation/bindings/models/abm.cpp @@ -161,8 +161,7 @@ PYBIND11_MODULE(_simulation_abm, m) .def(py::init(), py::arg("testing_criteria"), py::arg("testing_validity_period"), py::arg("start_date"), - py::arg("end_date"), py::arg("test_parameters"), py::arg("probability")) - .def_property_readonly("active", &mio::abm::TestingScheme::is_active); + py::arg("end_date"), py::arg("test_parameters"), py::arg("probability")); pymio::bind_class(m, "ProtectionEvent") .def(py::init(), py::arg("type"), py::arg("time")) @@ -170,7 +169,8 @@ PYBIND11_MODULE(_simulation_abm, m) .def_readwrite("time", &mio::abm::ProtectionEvent::time); pymio::bind_class(m, "TestingStrategy") - .def(py::init&>()); + .def(py::init&, + const std::vector&>()); pymio::bind_class(m, "Location") .def_property_readonly("type", &mio::abm::Location::get_type) diff --git a/pycode/memilio-simulation/memilio/simulation_test/test_abm.py b/pycode/memilio-simulation/memilio/simulation_test/test_abm.py index 2e3df1bd04..1b7d229a20 100644 --- a/pycode/memilio-simulation/memilio/simulation_test/test_abm.py +++ b/pycode/memilio-simulation/memilio/simulation_test/test_abm.py @@ -57,13 +57,6 @@ def test_locations(self): home.infection_parameters.MaximumContacts = 10 self.assertEqual(home.infection_parameters.MaximumContacts, 10) - testing_inf_states = [] - testing_crit = abm.TestingCriteria( - testing_ages, testing_inf_states) - testing_scheme = abm.TestingScheme(testing_crit, abm.days( - 1), t0, t0 + abm.days(1), model.parameters.TestData[abm.TestType.Antigen], 1.0) - # initially false, will only active once simulation starts - self.assertEqual(testing_scheme.active, False) def test_persons(self): """ """